4、Verilog实现延时消抖:20ms计数器设计、边沿检测模块、LED控制逻辑

好,咱们直接进入正题。前面讲了理论,现在要动手写代码了。延时消抖的核心,说白了就是「等一等,再确认」。你按下按键的瞬间,信号会跳来跳去,我等它稳定了再采样,这就是消抖的本质。

我个人习惯把消抖拆成三个模块:计数器边沿检测LED控制。这样分工明确,调试起来也方便。你想想看,如果所有逻辑揉在一起,出问题了都不知道是哪里崩的。

4.1 20ms计数器设计

为什么是20ms?因为机械按键的抖动时间一般在5~10ms,留点余量,20ms足够覆盖绝大多数情况。我在项目中遇到过一些劣质按键,抖动长达15ms,还好当时留了余量,不然就翻车了。

计数器的思路很简单:用系统时钟计数,计到20ms对应的数值就产生一个标志。假设系统时钟是50MHz,周期20ns,那么20ms需要计数的次数是:

20ms / 20ns = 1,000,000 次

嗯,这里要注意,计数器从0开始,所以最大值是999,999。代码长这样:

// 20ms 计数器模块
module debounce_counter (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       key_in,       // 原始按键输入
    output reg        key_debounced // 消抖后的输出
);

    // 20ms 计数最大值 (50MHz时钟)
    localparam MAX_CNT = 20_000_000 / 20 - 1; // 999,999

    reg [19:0] cnt;      // 20位计数器,够用
    reg        cnt_en;   // 计数使能

    // 计数器逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt <= 20'd0;
        end else if (cnt_en) begin
            if (cnt == MAX_CNT)
                cnt <= 20'd0;
            else
                cnt <= cnt + 1'b1;
        end else begin
            cnt <= 20'd0;
        end
    end

    // 计数使能:按键状态变化时启动计数
    // 这里先留空,后面结合边沿检测模块一起用

endmodule
小提示:计数器的位宽要算好。20位最大能计2^20=1,048,576,刚好够用。如果你用25MHz时钟,20ms只需要500,000次,19位就够了。别浪费资源。

4.2 边沿检测模块

边沿检测是消抖的关键。为什么要检测边沿?因为我们要知道按键「刚被按下」或者「刚被释放」的时刻,这时候才开始计时。如果一直检测电平,那按键按住不放的时候,计数器会一直跑,逻辑就乱了。

边沿检测的原理很简单:用两级寄存器打拍,然后比较前后两拍的值。上升沿就是前一级为0、后一级为1;下降沿反之。代码实现:

// 边沿检测模块
module edge_detect (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       signal_in,    // 待检测信号
    output reg        pos_edge,     // 上升沿
    output reg        neg_edge      // 下降沿
);

    reg signal_d1;  // 第一级打拍
    reg signal_d2;  // 第二级打拍

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            signal_d1 <= 1'b0;
            signal_d2 <= 1'b0;
        end else begin
            signal_d1 <= signal_in;
            signal_d2 <= signal_d1;
        end
    end

    // 边沿检测:前一级为0,后一级为1 → 上升沿
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pos_edge <= 1'b0;
            neg_edge <= 1'b0;
        end else begin
            pos_edge <= signal_d1 & ~signal_d2;  // 上升沿
            neg_edge <= ~signal_d1 & signal_d2;  // 下降沿
        end
    end

endmodule
注意:边沿检测信号只持续一个时钟周期,是脉冲信号。你不能把它当电平用,否则会出大问题。我曾经有个同事直接把边沿信号接到LED上,结果LED闪了一下就灭了,他找了半天bug...

4.3 LED控制逻辑

LED控制就简单了。消抖后的按键信号,每按一次,LED状态翻转一次。这里有个小坑:按键按下时输出低电平(常见设计),所以消抖后的信号需要取反再送给LED。

完整的LED控制逻辑:

// LED控制模块
module led_control (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       key_debounced, // 消抖后的按键信号
    output reg        led_out        // LED输出
);

    // 检测消抖信号的下降沿(按键按下)
    reg key_d1;
    wire key_falling;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            key_d1 <= 1'b1;  // 默认高电平(按键未按下)
        else
            key_d1 <= key_debounced;
    end

    assign key_falling = ~key_debounced & key_d1;

    // 每检测到一次下降沿,LED翻转
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            led_out <= 1'b0;  // 初始熄灭
        else if (key_falling)
            led_out <= ~led_out;
    end

endmodule
核心要点:消抖的本质是「延迟确认」。计数器负责计时,边沿检测负责触发计时,LED控制负责响应。三个模块各司其职,组合起来就是一个完整的按键消抖系统。

4.4 模块整合与仿真验证

把三个模块连起来,顶层模块大概长这样:

// 顶层模块
module key_debounce_top (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       key_in,
    output wire       led_out
);

    wire key_debounced;
    wire pos_edge, neg_edge;

    // 实例化边沿检测
    edge_detect u_edge (
        .clk       (clk),
        .rst_n     (rst_n),
        .signal_in (key_in),
        .pos_edge  (pos_edge),
        .neg_edge  (neg_edge)
    );

    // 实例化计数器(这里用下降沿触发)
    debounce_counter u_counter (
        .clk       (clk),
        .rst_n     (rst_n),
        .key_in    (key_in),
        .key_debounced (key_debounced)
    );

    // 实例化LED控制
    led_control u_led (
        .clk       (clk),
        .rst_n     (rst_n),
        .key_debounced (key_debounced),
        .led_out   (led_out)
    );

endmodule

写完之后一定要仿真。我建议你至少跑三种情况:

  • 正常按键:按下后稳定,看LED是否翻转
  • 抖动干扰:在按下瞬间加入毛刺,看消抖是否生效
  • 快速连按:间隔小于20ms的连续按键,看会不会漏掉
测试场景 输入信号 预期结果
正常按键 按下后保持低电平 LED翻转一次
抖动干扰 按下时加入10ms抖动 LED只翻转一次
快速连按 间隔15ms连续按下 第二次按下可能被忽略
避坑指南:我曾经在快速连按测试中翻过车。按键间隔小于20ms时,第二次按下会被计数器忽略。解决办法是改用「边沿触发+可重触发计数器」,每次检测到边沿就复位计数器重新计时。这个方案更复杂,但更可靠。

好了,这一章的内容就到这里。代码都给你了,建议你亲手敲一遍,然后跑仿真看看波形。只有自己动手,才能真正理解消抖的细节。下一章我们会讲如何优化这个设计,让它更省资源、更稳定。