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即可适配实际工程。 - 输出为单周期脉冲,适合直接作为状态机触发或中断标志。