3、软件消抖基础:延时消抖法(20ms延时)、状态机消抖法、计数器消抖法
按键消抖,说白了就是跟机械弹片“斗智斗勇”。
你按下一个按键,你以为只触发了一次,实际上在几毫秒内,它可能弹跳了十几次。如果不处理,你的LED就会像抽风一样乱闪。
我在项目中遇到过最夸张的一次,一个按键按下,计数器直接跳了30多次。从那以后,我对消抖这件事就特别上心。
3.1 延时消抖法(20ms延时)
这是最直观的方法。原理很简单:检测到按键按下后,先等20ms,再读一次电平。如果还是低电平,说明真的按下了。
为什么是20ms?其实这是个经验值。机械按键的弹跳时间一般在5-10ms,留点余量,20ms比较稳妥。
核心思路:检测到边沿 -> 延时等待 -> 再次确认 -> 输出有效信号
// 延时消抖法 - Verilog示例
reg [19:0] cnt_20ms;
reg key_reg0, key_reg1;
wire key_press;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_reg0 <= 1'b1;
key_reg1 <= 1'b1;
cnt_20ms <= 20'd0;
end else begin
key_reg0 <= key_in;
key_reg1 <= key_reg0;
if (key_reg1 && !key_reg0) // 检测到下降沿
cnt_20ms <= 20'd1_000_000; // 假设50MHz时钟,20ms计数
else if (cnt_20ms != 0)
cnt_20ms <= cnt_20ms - 1'b1;
end
end
assign key_press = (cnt_20ms == 20'd1) ? 1'b1 : 1'b0;
注意:延时消抖法有个致命问题——它会阻塞你的系统。在延时期间,CPU或状态机啥也干不了。所以它只适合按键数量少、系统不复杂的场景。
我曾经在一个8按键的键盘上用过这种方法,结果发现按下一个键,其他键全被“冻住”了。嗯,从那以后我就很少用纯延时法了。
3.2 状态机消抖法
状态机消抖,说白了就是用有限状态机来管理按键的状态变化。它比延时法灵活得多,也更容易扩展。
我习惯用4状态的状态机:空闲态、抖动态、稳定态、释放态。你想想看,每个状态对应按键的一个阶段,逻辑非常清晰。
| 状态 | 描述 | 触发条件 |
|---|---|---|
| IDLE | 等待按键按下 | 检测到低电平 -> 进入SHAKE |
| SHAKE | 等待抖动结束 | 20ms后电平稳定 -> 进入PRESS |
| PRESS | 按键已稳定按下 | 检测到高电平 -> 进入RELEASE |
| RELEASE | 等待释放完成 | 20ms后电平稳定 -> 回到IDLE |
// 状态机消抖法 - 核心代码
localparam IDLE = 2'b00;
localparam SHAKE = 2'b01;
localparam PRESS = 2'b10;
localparam RELEASE = 2'b11;
reg [1:0] state, next_state;
reg [19:0] timer;
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) next_state = SHAKE;
SHAKE: if (timer_done) next_state = PRESS;
PRESS: if (key_in) next_state = RELEASE;
RELEASE: if (timer_done) next_state = IDLE;
endcase
end
我的经验:状态机消抖法特别适合多按键场景。你可以把每个按键的状态机独立开来,互不干扰。我在一个16按键的矩阵键盘上用过,效果非常好。
3.3 计数器消抖法
计数器消抖法,说白了就是“连续采样,多数表决”。
原理很简单:每隔1ms采样一次按键电平,连续采样20次。如果20次中有16次以上是低电平,就认为按键真的按下了。
为什么会这样设计?因为弹跳是随机的,但真实按键信号是持续的。通过多次采样,可以过滤掉那些短暂的弹跳脉冲。
// 计数器消抖法 - 连续采样
reg [4:0] cnt; // 5位计数器,最大31
reg [3:0] sample_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 5'd0;
sample_cnt <= 4'd0;
end else if (cnt == 5'd19) begin // 每1ms采样一次
cnt <= 5'd0;
if (!key_in)
sample_cnt <= sample_cnt + 1'b1;
end else begin
cnt <= cnt + 1'b1;
end
end
// 当采样次数达到阈值时,输出有效信号
assign key_press = (sample_cnt >= 4'd12) ? 1'b1 : 1'b0;
关键参数:
- 采样间隔:1ms(根据时钟频率调整计数器值)
- 采样次数:20次(覆盖20ms的抖动窗口)
- 判定阈值:12-16次(根据按键质量调整)
我个人比较喜欢计数器法,因为它不阻塞系统,而且抗干扰能力强。我曾经在一个工业控制板上用过,现场电磁环境很差,但计数器法愣是没出过一次误触发。
避坑指南:计数器法的阈值不要设得太死。比如20次中要求20次全低,那按键稍微有点老化就识别不到了。我一般设成16/20,留点余量。
3.4 三种方法对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 延时消抖法 | 实现简单,代码量少 | 阻塞系统,效率低 | 单按键、低要求系统 |
| 状态机消抖法 | 灵活,可扩展,不阻塞 | 代码稍复杂 | 多按键、复杂系统 |
| 计数器消抖法 | 抗干扰强,不阻塞 | 需要调整阈值 | 工业环境、高可靠性 |
嗯,三种方法各有千秋。我个人建议:如果是做产品原型,用状态机法最稳妥;如果是做工业级设备,计数器法更靠谱;至于延时法...除非你只是做个实验,否则尽量别用。
记住一句话:按键消抖不是技术难题,而是工程经验。你做得多了,自然就知道哪种方法最适合你的项目。