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时,第二次按下会被计数器忽略。解决办法是改用「边沿触发+可重触发计数器」,每次检测到边沿就复位计数器重新计时。这个方案更复杂,但更可靠。
好了,这一章的内容就到这里。代码都给你了,建议你亲手敲一遍,然后跑仿真看看波形。只有自己动手,才能真正理解消抖的细节。下一章我们会讲如何优化这个设计,让它更省资源、更稳定。