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 状态机流程图

为了让你更直观地理解状态跳转,我画了一张流程图。嗯,画图比写代码还费劲,但值得。

IDLE 等待按下 PRESS_DEBOUNCE 按下消抖中 RELEASE_DEBOUNCE 释放消抖中 key_in=0 20ms到 & key_in=0 20ms到 & key_in=1 抖动,key_in=1 抖动,key_in=0 图例: 正常跳转 抖动跳转

3.5 仿真与验证要点

代码写完了,别急着上板。先仿真,这是FPGA开发的铁律。我见过太多人直接上板,然后对着示波器一脸懵。

仿真时重点关注:

  • 抖动模拟:在key_in上叠加一些随机的高频跳变,模拟真实按键抖动
  • 边界测试:测试刚好19ms、20ms、21ms的按键按下,看输出是否正确
  • 连续按键:快速按下、释放、再按下,看状态机能否正确处理

个人经验: 仿真时我习惯把20ms改成20us来跑,这样仿真时间短,能快速验证逻辑。等逻辑没问题了,再改回20ms做一次完整仿真。这叫“时间缩放法”,效率很高。

3.6 上板实战建议

仿真通过后,就可以上板了。这里给几个实用建议:

  • 管脚约束:确认按键管脚有没有上拉电阻。如果没有,记得在代码里使能内部上拉
  • 观察输出:把key_out接到LED上,按一下按键,看LED是否稳定亮灭
  • 调试技巧:如果LED闪烁不稳定,大概率是消抖时间不够。试着把20ms改成30ms或50ms

好了,这一节的内容就到这里。状态机消抖是FPGA开发中的基本功,掌握了它,后面处理更复杂的时序逻辑就会轻松很多。

可可精品资料库