4. 按键消抖模块设计(状态机实现)
按键消抖,说白了就是解决机械开关的「抖动」问题。我刚开始做FPGA时,第一次用按键控制LED,结果按一下灯闪了好几下——当时我还以为板子坏了。后来才知道,按键按下和释放的瞬间,金属触点会弹跳几次,产生多个脉冲。
这个抖动时间一般在5-20ms之间。你想想看,如果不用消抖,一次按键可能被识别成多次。所以,我们需要一个可靠的消抖方案。
4.1 状态转移图
我个人习惯用状态机来做消抖。为什么?因为状态机逻辑清晰,容易控制时序。我们先看状态转移图:
这个状态机有5个状态。核心思路是:检测到按键变化后,启动计数器等待20ms。如果20ms后电平稳定,才确认状态变化。嗯,这里要注意——如果计数过程中电平又跳回去了,说明是抖动,要回到初始状态重新等待。
4.2 三段式状态机编码
我写状态机一直用三段式。为什么?因为组合逻辑和时序逻辑分开,代码可读性好,也方便调试。三段式就是:
- 第一段:时序逻辑,描述状态跳转
- 第二段:组合逻辑,描述下一状态
- 第三段:时序逻辑,描述输出
来看看代码怎么写:
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:下一状态逻辑
always @(*) begin
case (state)
IDLE: begin
if (key_in == 1'b0)
next_state = PRESS_DETECT;
else
next_state = IDLE;
end
PRESS_DETECT: begin
if (cnt_full)
next_state = PRESS_CONFIRM;
else if (key_in == 1'b1)
next_state = IDLE; // 抖动,回到初始
else
next_state = PRESS_DETECT;
end
PRESS_CONFIRM: begin
if (key_in == 1'b1)
next_state = RELEASE_DETECT;
else
next_state = PRESS_CONFIRM;
end
RELEASE_DETECT: begin
if (cnt_full)
next_state = RELEASE_CONFIRM;
else if (key_in == 1'b0)
next_state = PRESS_CONFIRM; // 抖动,回到按下
else
next_state = RELEASE_DETECT;
end
RELEASE_CONFIRM: begin
if (key_in == 1'b0)
next_state = PRESS_DETECT;
else
next_state = RELEASE_CONFIRM;
end
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
key_out <= 1'b1;
else if (state == PRESS_CONFIRM && next_state == RELEASE_DETECT)
key_out <= 1'b0; // 按下确认,输出低电平
else if (state == RELEASE_CONFIRM && next_state == IDLE)
key_out <= 1'b1; // 释放确认,输出高电平
end
我的小技巧:第三段输出用「边沿检测」的方式写,只在状态切换的瞬间更新输出。这样能避免输出毛刺,我在好几个项目里都这么用,效果不错。
4.3 消抖计数器设计
计数器是消抖的核心。20ms怎么算?假设时钟是50MHz,周期20ns。20ms / 20ns = 1,000,000。所以计数器需要从0计到999,999。
我个人习惯用参数化设计,方便后期调整:
parameter CLK_FREQ = 50_000_000; // 50MHz
parameter DEBOUNCE_TIME = 20; // 20ms
localparam CNT_MAX = CLK_FREQ / 1000 * DEBOUNCE_TIME - 1;
localparam CNT_WIDTH = $clog2(CNT_MAX + 1);
reg [CNT_WIDTH-1:0] cnt;
reg cnt_full;
// 计数器逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 'd0;
else if (state == PRESS_DETECT || state == RELEASE_DETECT)
cnt <= cnt + 1'b1;
else
cnt <= 'd0;
end
// 计数满标志
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt_full <= 1'b0;
else if (cnt == CNT_MAX)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
end
避坑指南:我曾经犯过一个错误——计数器用组合逻辑产生cnt_full信号。结果仿真没问题,上板子就出bug。后来发现是组合逻辑产生了毛刺,导致状态机误跳转。记住,cnt_full一定要用时序逻辑打一拍。
4.4 完整模块整合
把上面三部分拼起来,就是一个完整的按键消抖模块。我一般还会加一个边沿检测,输出单脉冲:
module key_debounce (
input wire clk,
input wire rst_n,
input wire key_in,
output reg key_out,
output wire key_pulse
);
// ... 状态机、计数器代码放这里 ...
// 边沿检测,输出单周期脉冲
reg key_out_dly;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
key_out_dly <= 1'b1;
else
key_out_dly <= key_out;
end
assign key_pulse = ~key_out & key_out_dly; // 下降沿脉冲
endmodule
核心要点回顾:
- 状态机5个状态:IDLE → PRESS_DETECT → PRESS_CONFIRM → RELEASE_DETECT → RELEASE_CONFIRM
- 三段式编码:状态跳转、下一状态、输出逻辑分开写
- 计数器参数化:方便适配不同时钟频率和消抖时间
- 输出用边沿检测,避免毛刺
这个模块我用了好多年,从简单的按键控制到复杂的键盘矩阵,核心逻辑都没变过。你想想看,一个设计如果能经得起时间考验,说明它的思路是对的。
可可精品资料库