4、软件消抖方法:延时消抖法、循环采样消抖法、状态机消抖法
好,咱们接着聊软件消抖。硬件消抖虽然可靠,但毕竟要加电容电阻,板子空间紧张的时候真让人头疼。我个人的习惯是,只要MCU或FPGA的资源够用,优先考虑软件消抖——成本低、改起来也方便。
软件消抖的核心思路其实就一句话:避开抖动区间,只认稳定电平。具体怎么实现?主要有三种经典方法。我一个个说,都是我在项目里实际用过的。
4.1 延时消抖法
这个方法最直观,也最容易理解。说白了就是:检测到按键按下后,别急着确认,等个10~20ms再读一次。
为什么会这样?因为机械按键的抖动时间一般不超过10ms。你想想看,第一次检测到低电平(假设按下为低),这时候可能是抖动。等上20ms,抖动早过去了,这时候再读,如果还是低,那才是真的按下。
代码实现也很简单,以FPGA为例:
// 延时消抖法 - Verilog示例
reg key_reg1, key_reg2;
reg [19:0] cnt; // 20位计数器,时钟50MHz时约20ms
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_reg1 <= 1'b1;
key_reg2 <= 1'b1;
cnt <= 20'd0;
end else begin
// 第一次检测到按下
if (key_in == 1'b0 && key_reg1 == 1'b1) begin
cnt <= 20'd0; // 启动计时
key_reg1 <= 1'b0;
end else if (cnt == 20'd999_999) begin // 20ms到
key_reg2 <= key_in; // 再次读取
cnt <= 20'd0;
end else begin
cnt <= cnt + 1'b1;
end
end
end
assign key_out = key_reg2; // 消抖后的输出
嗯,这里要注意:延时期间CPU或FPGA不能干等着。如果是MCU,可以用定时器中断;如果是FPGA,就用计数器。别用delay()这种阻塞函数,否则整个系统都被你卡死了。
4.2 循环采样消抖法
延时消抖法有个小毛病:如果按键按下的时间刚好卡在延时结束的瞬间,还是可能误判。所以就有了更可靠的方法——连续多次采样,结果一致才确认。
我管它叫「投票法」。你想想看,抖动是随机的,但真实电平是稳定的。连续采样4次、8次甚至16次,如果每次结果都一样,那基本可以认定是真实按键动作了。
| 采样次数 | 抗抖能力 | 响应延迟 | 适用场景 |
|---|---|---|---|
| 4次 | 一般 | 约4ms | 普通消费电子 |
| 8次 | 较强 | 约8ms | 工业控制 |
| 16次 | 很强 | 约16ms | 高可靠性要求 |
采样间隔一般取1ms左右。为什么?因为按键抖动频率通常在100Hz~1kHz之间,1ms采样一次刚好能覆盖这个范围。
// 循环采样消抖法 - 连续8次采样一致才输出
reg [7:0] shift_reg; // 移位寄存器,存储最近8次采样值
always @(posedge clk_1ms or negedge rst_n) begin
if (!rst_n) begin
shift_reg <= 8'hFF; // 默认高电平(未按下)
key_out <= 1'b1;
end else begin
shift_reg <= {shift_reg[6:0], key_in}; // 每1ms移入新采样值
// 检查8次是否全部一致
if (shift_reg == 8'h00) begin
key_out <= 1'b0; // 确认按下
end else if (shift_reg == 8'hFF) begin
key_out <= 1'b1; // 确认释放
end
// 其他情况保持原值,不处理
end
end
4.3 状态机消抖法
前面两种方法都是「死等」的思路。状态机就不一样了——它用有限状态机来管理按键的各个阶段,效率更高,也更容易扩展功能(比如长按、双击检测)。
我个人特别喜欢状态机消抖法。为什么?因为它把按键的整个生命周期都管起来了:从空闲、到按下抖动、到稳定按下、再到释放抖动、最后回到空闲。每个状态做什么事,清清楚楚。
下面这张图是我设计的典型状态机流程:
状态机消抖法的代码实现稍微复杂一点,但逻辑非常清晰:
// 状态机消抖法 - 三段式FSM
localparam IDLE = 3'd0;
localparam PRESS_DEBOUNCE = 3'd1;
localparam PRESS_STABLE = 3'd2;
localparam RELEASE_DEBOUNCE= 3'd3;
localparam RELEASE_STABLE = 3'd4;
reg [2:0] state, next_state;
reg [19:0] timer; // 消抖计时器
reg [3:0] sample_cnt; // 采样计数
// 第一段:状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else state <= next_state;
end
// 第二段:次态逻辑
always @(*) begin
next_state = state;
case (state)
IDLE: if (key_in == 1'b0) next_state = PRESS_DEBOUNCE;
PRESS_DEBOUNCE: begin
if (timer_done && sample_consistent) next_state = PRESS_STABLE;
else if (key_in == 1'b1) next_state = IDLE; // 抖动回弹
end
PRESS_STABLE: if (key_in == 1'b1) next_state = RELEASE_DEBOUNCE;
RELEASE_DEBOUNCE: begin
if (timer_done && sample_consistent) next_state = RELEASE_STABLE;
else if (key_in == 1'b0) next_state = PRESS_STABLE; // 抖动回按
end
RELEASE_STABLE: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) key_out <= 1'b1;
else if (state == PRESS_STABLE && next_state == RELEASE_DEBOUNCE)
key_out <= 1'b0; // 确认按下,输出一个脉冲
else if (state == RELEASE_STABLE)
key_out <= 1'b1; // 确认释放
end
三种方法各有千秋。延时消抖法最简单,适合对响应速度要求不高的场合;循环采样法更可靠,适合工业环境;状态机法最灵活,适合功能复杂的系统。你根据实际需求选就行。
嗯,最后提醒一句:不管用哪种方法,采样时钟一定要稳定。我曾经在一个项目里用了RC振荡器做采样时钟,结果温度一变化,采样间隔飘了,消抖效果大打折扣。后来换成晶振,问题就解决了。