3. Verilog实现:基于状态机的按键消抖模块设计(20ms延时)
好,咱们直接进入正题。上一节我们聊了按键抖动的原理,说白了就是机械触点那几下“弹跳”。那怎么在FPGA里优雅地解决它?我的答案是——状态机。
为什么用状态机?你想想看,按键按下、释放、抖动、稳定,这本身就是一系列状态。用状态机来描述,逻辑清晰,代码可读性强,后期维护也方便。我个人习惯,凡是涉及时序控制的逻辑,优先考虑状态机。
3.1 状态机设计思路
我们先定义三个状态:
- IDLE:空闲状态,等待按键按下
- PRESS_DEBOUNCE:按下消抖状态,等待20ms稳定
- RELEASE_DEBOUNCE:释放消抖状态,等待20ms稳定
状态跳转逻辑很简单:
- 在IDLE状态,检测到按键为低电平(按下),跳转到PRESS_DEBOUNCE
- 在PRESS_DEBOUNCE,如果20ms内按键一直为低,则认为按下有效,输出脉冲,跳转到RELEASE_DEBOUNCE
- 在RELEASE_DEBOUNCE,如果20ms内按键一直为高,则认为释放完成,回到IDLE
嗯,这里要注意:任何状态下,如果检测到电平跳变,都要立即复位计数器,重新计时。这就是“消抖”的核心——只认稳定信号。
核心思想: 状态机 + 计数器 = 可靠的按键消抖。计数器负责计时,状态机负责决策。
3.2 20ms延时是怎么来的?
20ms这个数值不是拍脑袋定的。我记得最早看一些教材,有说10ms的,有说20ms的。我在项目中实测过,普通机械按键的抖动时间一般在5~15ms之间。取20ms,留点余量,既保证可靠,又不会让响应太迟钝。
那20ms在FPGA里怎么实现?假设我们的系统时钟是50MHz,周期20ns。20ms = 20,000,000ns。需要的计数次数 = 20,000,000 / 20 = 1,000,000。所以我们需要一个20位的计数器(2^20 = 1,048,576,够用)。
| 参数 | 数值 |
|---|---|
| 系统时钟频率 | 50 MHz |
| 时钟周期 | 20 ns |
| 消抖时间 | 20 ms |
| 计数器最大值 | 1,000,000 - 1 |
| 计数器位宽 | 20 bit |
小技巧: 如果你用的时钟频率不同,记得重新算一下计数器最大值。公式很简单:MAX_CNT = 消抖时间(秒) × 时钟频率(Hz) - 1。
3.3 Verilog代码实现
直接上代码。这是我个人比较喜欢的一种写法,三段式状态机,清晰明了。
module key_debounce (
input clk, // 50MHz 系统时钟
input rst_n, // 异步复位,低有效
input key_in, // 按键输入,按下为低
output reg key_out // 消抖后输出,按下为高脉冲
);
// 状态定义
localparam IDLE = 2'b00;
localparam PRESS_DEBOUNCE = 2'b01;
localparam RELEASE_DEBOUNCE = 2'b10;
// 计数器参数
localparam MAX_CNT = 20'd1_000_000 - 1; // 20ms @ 50MHz
// 信号定义
reg [1:0] state, next_state;
reg [19:0] cnt;
reg cnt_rst;
reg cnt_en;
// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑和输出逻辑
always @(*) begin
next_state = state;
key_out = 1'b0;
cnt_rst = 1'b0;
cnt_en = 1'b0;
case (state)
IDLE: begin
if (!key_in) begin // 检测到按下
next_state = PRESS_DEBOUNCE;
cnt_rst = 1'b1; // 复位计数器
end
end
PRESS_DEBOUNCE: begin
if (!key_in) begin // 按键仍为低
if (cnt == MAX_CNT) begin // 20ms到
next_state = RELEASE_DEBOUNCE;
key_out = 1'b1; // 输出有效脉冲
cnt_rst = 1'b1; // 复位计数器
end else begin
cnt_en = 1'b1; // 继续计数
end
end else begin // 按键抖动,提前变高
next_state = IDLE; // 回到空闲
cnt_rst = 1'b1;
end
end
RELEASE_DEBOUNCE: begin
if (key_in) begin // 按键已释放
if (cnt == MAX_CNT) begin // 20ms到
next_state = IDLE;
cnt_rst = 1'b1;
end else begin
cnt_en = 1'b1;
end
end else begin // 按键又按下,抖动
next_state = PRESS_DEBOUNCE; // 回到按下消抖
cnt_rst = 1'b1;
end
end
default: begin
next_state = IDLE;
cnt_rst = 1'b1;
end
endcase
end
// 第三段:计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 20'd0;
else if (cnt_rst)
cnt <= 20'd0;
else if (cnt_en)
cnt <= cnt + 1'b1;
end
endmodule
避坑指南: 我曾经在项目中犯过一个低级错误——计数器复位和使能信号没分开。结果状态跳转时计数器没清零,导致消抖时间忽长忽短。记住:状态跳转的瞬间,一定要把计数器复位!
3.4 状态机流程图
为了让你更直观地理解状态跳转,我画了一张流程图。嗯,画图比写代码还费劲,但值得。
3.5 仿真与验证要点
代码写完了,别急着上板。先仿真,这是FPGA开发的铁律。我见过太多人直接上板,然后对着示波器一脸懵。
仿真时重点关注:
- 抖动模拟:在key_in上叠加一些随机的高频跳变,模拟真实按键抖动
- 边界测试:测试刚好19ms、20ms、21ms的按键按下,看输出是否正确
- 连续按键:快速按下、释放、再按下,看状态机能否正确处理
个人经验: 仿真时我习惯把20ms改成20us来跑,这样仿真时间短,能快速验证逻辑。等逻辑没问题了,再改回20ms做一次完整仿真。这叫“时间缩放法”,效率很高。
3.6 上板实战建议
仿真通过后,就可以上板了。这里给几个实用建议:
- 管脚约束:确认按键管脚有没有上拉电阻。如果没有,记得在代码里使能内部上拉
- 观察输出:把key_out接到LED上,按一下按键,看LED是否稳定亮灭
- 调试技巧:如果LED闪烁不稳定,大概率是消抖时间不够。试着把20ms改成30ms或50ms
好了,这一节的内容就到这里。状态机消抖是FPGA开发中的基本功,掌握了它,后面处理更复杂的时序逻辑就会轻松很多。