2. 按键消抖

任务目标

机械按键在按下与释放瞬间会产生约5~20ms的抖动,导致电平多次跳变。本案例实现一个基于计数器延时采样的按键消抖模块,在按键稳定后产生一个单周期脉冲,确保后续逻辑只捕获一次有效按键事件。

实现方案

采用“延时确认”法:检测到按键电平变化后启动计数器,计数满20ms(假设50MHz时钟)后再次采样按键电平。若采样值与变化方向一致,则确认按键状态有效并输出脉冲。该方案无需复杂状态机,资源占用极低。

完整代码

module debounce #(
    parameter CLK_FREQ = 50_000_000,  // 输入时钟频率(Hz)
    parameter DEBOUNCE_MS = 20        // 消抖时间(ms)
)(
    input  wire clk,
    input  wire rst_n,
    input  wire key_in,               // 原始按键输入(低有效)
    output reg  key_pulse             // 消抖后单周期脉冲(高有效)
);

    // 计算计数上限
    localparam integer MAX_COUNT = CLK_FREQ * DEBOUNCE_MS / 1000 - 1;
    localparam integer CNT_WIDTH = $clog2(MAX_COUNT + 1);

    reg  key_in_d1;                   // 一级同步
    reg  key_in_d2;                   // 二级同步(消除亚稳态)
    reg  key_stable;                  // 当前稳定状态
    reg [CNT_WIDTH-1:0] cnt;
    reg cnt_en;

    // 两级同步,消除亚稳态
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_in_d1 <= 1'b1;
            key_in_d2 <= 1'b1;
        end else begin
            key_in_d1 <= key_in;
            key_in_d2 <= key_in_d1;
        end
    end

    // 检测电平变化,启动计数
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt_en <= 1'b0;
        else if (key_in_d2 != key_stable)
            cnt_en <= 1'b1;          // 检测到变化,开始计时
        else if (cnt == MAX_COUNT)
            cnt_en <= 1'b0;          // 计时完成
    end

    // 计数器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            cnt <= 0;
        else if (cnt_en) begin
            if (cnt == MAX_COUNT)
                cnt <= cnt;
            else
                cnt <= cnt + 1'b1;
        end else
            cnt <= 0;
    end

    // 计时结束后采样并更新稳定状态
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            key_stable <= 1'b1;      // 默认高(未按下)
            key_pulse  <= 1'b0;
        end else if (cnt == MAX_COUNT) begin
            key_stable <= key_in_d2; // 确认新状态
            key_pulse  <= 1'b1;      // 产生脉冲
        end else begin
            key_pulse  <= 1'b0;      // 其余时间脉冲为低
        end
    end

endmodule

测试平台(Testbench)

`timescale 1ns/1ps
module tb_debounce;

    reg clk = 0;
    reg rst_n = 0;
    reg key_in = 1;
    wire key_pulse;

    // 生成50MHz时钟
    always #10 clk = ~clk;

    debounce #(
        .CLK_FREQ(50_000_000),
        .DEBOUNCE_MS(20)
    ) uut (
        .clk(clk),
        .rst_n(rst_n),
        .key_in(key_in),
        .key_pulse(key_pulse)
    );

    // 模拟按键抖动
    initial begin
        // 复位
        #30 rst_n = 1;
        #20;

        // 模拟按下抖动:多次跳变后稳定为低
        key_in = 0; #5;
        key_in = 1; #3;
        key_in = 0; #4;
        key_in = 1; #2;
        key_in = 0; // 稳定按下
        #1_000_000; // 等待20ms消抖完成

        // 此时应看到key_pulse产生一个高脉冲

        // 模拟释放抖动
        key_in = 1; #5;
        key_in = 0; #3;
        key_in = 1; #4;
        key_in = 0; #2;
        key_in = 1; // 稳定释放
        #1_000_000;

        $finish;
    end

    // 监控脉冲输出
    always @(posedge key_pulse) begin
        $display("Time %0t: Key pulse detected!", $time);
    end

endmodule

运行与验证

仿真验证:

# 使用ModelSim/QuestaSim运行
vlib work
vlog debounce.v tb_debounce.v
vsim -c work.tb_debounce
run -all

通过条件:仿真输出中应出现两次“Key pulse detected!”消息——第一次对应按键按下(约1ms处),第二次对应释放(约2ms处)。每次脉冲宽度恰好为一个时钟周期(20ns)。若在抖动期间(前14ns内)没有产生任何脉冲,则说明消抖逻辑正确。

关键说明

  • 两级触发器同步用于消除跨时钟域亚稳态,是按键输入的必要处理。
  • 计数器仅在检测到电平变化时启动,稳定期间不计数,降低动态功耗。
  • 参数化设计支持不同时钟频率与消抖时间,修改CLK_FREQ和DEBOUNCE_MS即可适配实际工程。
  • 输出为单周期脉冲,适合直接作为状态机触发或中断标志。