2. 按键消抖模块

任务说明

机械按键在按下与释放时会产生约5~20ms的抖动,导致电平在高低之间多次跳变。本案例实现一个按键消抖模块,通过延时计数确认电平稳定后,再结合边沿检测输出单周期脉冲,确保每次按键动作只产生一次有效输出。

实现方案

采用“延时计数 + 边沿检测”的两级处理:

  • 当检测到按键电平变化时,启动计数器,在抖动窗口(20ms)内持续采样;
  • 若采样值在窗口内保持不变,则确认电平稳定,更新稳定后的按键状态;
  • 对稳定后的状态做上升沿检测,输出单周期脉冲 key_pulse。

完整代码

module key_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 CNT_MAX = CLK_FREQ / 1000 * DEBOUNCE_MS - 1;
    localparam integer CNT_WIDTH = $clog2(CNT_MAX + 1);

    reg  key_in_d1;
    reg  key_in_d2;
    wire key_changed;                 // 输入电平发生变化

    reg  [CNT_WIDTH-1:0] cnt;
    reg  cnt_en;                      // 计数使能
    reg  key_stable;                  // 稳定后的按键电平(低有效)

    // 两级同步,消除亚稳态
    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

    // 检测输入变化
    assign key_changed = (key_in_d2 != key_stable);

    // 消抖计数器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt     <= {CNT_WIDTH{1'b0}};
            cnt_en  <= 1'b0;
        end else if (key_changed) begin
            // 电平变化,重新开始计数
            cnt    <= {CNT_WIDTH{1'b0}};
            cnt_en <= 1'b1;
        end else if (cnt_en) begin
            if (cnt == CNT_MAX) begin
                cnt_en  <= 1'b0;      // 计数完成
                key_stable <= key_in_d2;  // 锁定稳定电平
            end else begin
                cnt <= cnt + 1'b1;
            end
        end
    end

    // 边沿检测:稳定状态由高到低(按下)产生脉冲
    reg key_stable_d;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            key_stable_d <= 1'b1;
        else
            key_stable_d <= key_stable;
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            key_pulse <= 1'b0;
        else
            key_pulse <= (~key_stable) & key_stable_d;  // 检测下降沿(按下)
    end

endmodule

测试平台

`timescale 1ns/1ps
module tb_key_debounce;

    reg  clk;
    reg  rst_n;
    reg  key_in;
    wire key_pulse;

    // 50MHz时钟
    localparam CLK_PERIOD = 20;
    always #(CLK_PERIOD/2) clk = ~clk;

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

    // 模拟机械抖动
    task press_key;
        begin
            // 按下过程:先抖动再稳定
            key_in = 1'b1;
            #100;
            key_in = 1'b0; #200;   // 抖动
            key_in = 1'b1; #150;
            key_in = 1'b0; #180;
            key_in = 1'b1; #120;
            key_in = 1'b0;         // 稳定按下
            #(25_000_000);         // 等待消抖完成(约25ms)
        end
    endtask

    initial begin
        clk = 0;
        rst_n = 0;
        key_in = 1'b1;
        #100;
        rst_n = 1;
        #100;

        press_key;                 // 第一次按下
        #1000;
        $display("key_pulse = %b (期望为1)", key_pulse);

        // 释放过程
        key_in = 1'b1;
        #(25_000_000);
        #1000;
        $display("key_pulse = %b (期望为0)", key_pulse);

        $finish;
    end

    // 监控脉冲
    always @(posedge key_pulse)
        $display("时间 %0t: 检测到按键脉冲", $time);

endmodule

运行与验证

  1. 仿真运行:在Vivado/ModelSim中编译两个文件,运行仿真。观察 key_pulse 信号。
  2. 通过条件:
  • 按下按键(含抖动)后,约20ms消抖窗口结束,key_pulse 输出一个时钟周期的高脉冲;
  • 释放按键时,key_pulse 保持低电平(本设计只检测按下动作);
  • 抖动期间 key_pulse 不产生任何毛刺脉冲。

关键点说明

  • 参数 CLK_FREQ 和 DEBOUNCE_MS 可根据实际时钟与按键特性调整,计数器位宽由 $clog2 自动计算。
  • 两级触发器同步用于消除亚稳态,是跨时钟域输入的标准处理方式。
  • 本模块输出的是“按下”脉冲;若需同时检测“释放”,可增加对上升沿的判断。