3. 软件消抖方案:延时消抖与循环采样
按键消抖这件事,硬件方案固然可靠,但很多时候我们没条件加RC电路,或者板子已经画好了才发现抖动问题。这时候,软件消抖就成了救命稻草。
我个人习惯把软件消抖分成两大类:延时消抖和循环采样。说白了,一个靠「等」,一个靠「看」。今天咱们就把这两种方案掰开揉碎了讲清楚。
3.1 延时消抖:最简单的「等一等」
延时消抖的思路很直白——检测到按键按下后,先别急着确认,等个10~20ms再读一次。如果还是低电平,那就说明真的按下了。
我在项目中遇到过不少新手,上来就写这样的代码:
// 错误示例:直接延时,阻塞CPU
if (KEY == 0) {
delay_ms(20); // 阻塞等待
if (KEY == 0) {
// 确认按下
}
}
嗯,这里要注意。这种写法在单片机裸机程序里勉强能用,但在硬件加速器或者FPGA里,阻塞延时是灾难。你想想看,FPGA里所有逻辑都是并行跑的,你让整个模块停下来等20ms?那其他数据流怎么办?
我曾经在一个图像处理加速器项目里,用了阻塞延时做按键消抖。结果按键是稳了,但图像帧率直接掉了30%。后来改成状态机+计数器,问题才解决。
正确的做法是用计数器代替延时。在FPGA里,我们通常这样实现:
// Verilog 延时消抖(非阻塞)
reg [19:0] cnt; // 20位计数器,20MHz时钟下约50ms
reg key_sync, key_debounced;
always @(posedge clk) begin
key_sync <= key_in; // 同步打拍
if (key_sync == 1'b0) begin // 检测到按下
if (cnt < 20'd400_000) // 等20ms
cnt <= cnt + 1'b1;
else begin
key_debounced <= 1'b0; // 确认按下
end
end else begin
cnt <= 20'd0;
key_debounced <= 1'b1;
end
end
这段代码的核心思想是:用计数器「数」出20ms,而不是让逻辑停下来等。这样其他模块该干嘛干嘛,互不干扰。
3.2 循环采样:更聪明的「多看几次」
延时消抖有个硬伤——它只采样两次。万一第一次采样刚好碰到噪声呢?万一按键本身老化、接触不良呢?
循环采样就是来解决这个问题的。它的做法是:连续采样N次,如果N次结果一致,才认为按键状态稳定了。
我一般推荐N取8~16次,采样间隔1~2ms。这样总耗时也就10~30ms,既保证了可靠性,又不会太迟钝。
延时消抖是「两点确认」,循环采样是「多点投票」。后者在噪声环境下明显更可靠。
来看看循环采样的状态机实现:
// 循环采样状态机
reg [3:0] sample_cnt; // 采样计数器
reg [7:0] shift_reg; // 移位寄存器,存储最近8次采样值
always @(posedge clk) begin
if (sample_cnt < 8) begin
// 每1ms采样一次
if (timer_1ms) begin
shift_reg <= {shift_reg[6:0], key_sync};
sample_cnt <= sample_cnt + 1'b1;
end
end else begin
// 检查8次采样是否一致
if (shift_reg == 8'b0000_0000) begin
key_debounced <= 1'b0; // 确认按下
end else if (shift_reg == 8'b1111_1111) begin
key_debounced <= 1'b1; // 确认释放
end
// 如果结果不一致,重新采样
sample_cnt <= 4'd0;
end
end
你可能会问:「为什么要用移位寄存器?」其实原因很简单——硬件里移位寄存器是免费的。你不需要像软件那样开一个数组,直接打8拍就完事了,零开销。
3.3 两种方案的对比
说了这么多,到底该用哪个?我整理了一张表,你一看就明白:
| 对比项 | 延时消抖 | 循环采样 |
|---|---|---|
| 实现复杂度 | 低(一个计数器搞定) | 中(需要移位寄存器+状态机) |
| 抗噪声能力 | 一般(只采两次) | 强(多次投票) |
| 响应延迟 | 固定(20ms) | 可变(取决于采样次数) |
| 资源消耗 | 极低(几十个LUT) | 低(几十到上百个LUT) |
| 适用场景 | 简单按键、资源紧张 | 噪声环境、可靠性要求高 |
我个人习惯是:普通消费电子产品用延时消抖就够了,工业控制或者车载设备,我必用循环采样。毕竟多花几十个LUT,换来的是少一次售后投诉,值。
3.4 核心逻辑流程图
下面这张图,把两种方案的决策逻辑画清楚了。你可以把它贴在工位上,写代码时瞄一眼:
3.5 实战中的几个坑
最后,分享几个我踩过的坑,希望能帮你省点调试时间:
- 采样间隔别太短——我曾经设过0.1ms采样一次,结果按键抖动和采样时钟混在一起,反而更不稳定。一般1~2ms比较合适。
- 注意同步打拍——按键信号进入FPGA前,一定要先打两拍同步。不然亚稳态会让你怀疑人生。
- 释放也要消抖——很多人只消抖按下,不消抖释放。结果按键松开时乱跳,导致误触发。记得按下和释放都要做同样的处理。
- 别用for循环——在FPGA里写for循环做延时,综合出来就是一堆LUT级联,时序很容易崩。老老实实用计数器。
如果你不确定采样次数该设多少,可以先设16次,然后用逻辑分析仪抓波形。观察按键稳定后,再逐步减少次数。我一般最终会定在8~12次之间。
好了,软件消抖就聊到这儿。这两种方案你选哪个,取决于你的资源预算和可靠性要求。记住一句话:没有最好的方案,只有最合适的方案。
可可精品资料库