4、状态机消抖法:有限状态机(FSM)设计、三态消抖状态机、状态转移图绘制
按键消抖这件事,前面我们聊了延时法和计数法。说实话,这两种方法对付一般的抖动都够用。但如果你追求更优雅、更可控、更省资源的方案,那状态机消抖法值得你花时间搞明白。
我个人习惯把状态机消抖法叫做“软硬兼施”的方案。它不像延时法那样死板地等时间,也不像计数法那样傻傻地数脉冲。它用有限状态机来“感知”按键的状态变化,说白了就是让FPGA学会“思考”——这个按键现在是稳定按下、稳定释放,还是正在抖动?
核心思想:将按键的输入信号抽象为几个离散状态,通过状态之间的跳转逻辑来滤除抖动。只有连续采样到稳定电平,才认为按键状态真正改变了。
4.1 三态消抖状态机:从“二值”到“三态”的进化
你可能会问:按键不就是按下和释放两个状态吗?为什么要搞三个?
嗯,这里要注意。传统的二值思维(0或1)在消抖场景下会出问题。因为抖动期间,电平在0和1之间来回跳变,你根本没法判断按键到底想干嘛。所以我们需要引入第三个状态——中间态,用来表示“正在抖动,尚未稳定”。
我设计的这个三态消抖状态机,包含以下三个状态:
- IDLE(空闲态):按键处于稳定释放状态,输出为高电平(假设按键按下为低)。
- PRESS_DEBOUNCE(按下消抖态):检测到按键可能被按下,正在等待抖动结束。如果连续采样到N次低电平,则确认按下。
- RELEASE_DEBOUNCE(释放消抖态):检测到按键可能被释放,正在等待抖动结束。如果连续采样到N次高电平,则确认释放。
你看,这个设计把“按下”和“释放”的消抖过程分开了。为什么这么做?因为按键按下和释放时的抖动特性可能不一样。我在项目中遇到过一种按键,按下时抖得厉害,释放时却很干脆。如果用一个统一的消抖逻辑,要么按下消抖不干净,要么释放响应太慢。分开处理,就能分别设置不同的采样次数。
我的经验:按下消抖的采样次数可以设大一点(比如20ms),释放消抖可以设小一点(比如5ms)。这样既能保证按下不误触,又能让释放响应更灵敏。当然,具体数值要根据你的按键硬件来调。
4.2 状态转移图:一图胜千言
搞状态机设计,我最怕的就是逻辑混乱。所以每次动手写代码前,我都会先画状态转移图。下面这张图是我用SVG画的,它清晰地展示了三态消抖状态机的完整工作流程。
这张图你看懂了吗?我简单解释一下:
- 系统上电后,默认在IDLE状态。此时按键输出为高电平(释放状态)。
- 一旦检测到key_in为低电平(按键按下),立即跳转到PRESS_DEBOUNCE状态。
- 在PRESS_DEBOUNCE中,如果采样到高电平,说明是抖动,跳回IDLE。如果连续N次都采样到低电平,说明按键真的按下了,跳转到RELEASE_DEBOUNCE状态,同时输出一个脉冲。
- 在RELEASE_DEBOUNCE中,逻辑类似。如果采样到低电平,说明是抖动,跳回PRESS_DEBOUNCE。如果连续N次都采样到高电平,说明按键真的释放了,跳回IDLE。
注意:这里有一个容易踩的坑。我曾经在PRESS_DEBOUNCE状态里,检测到一次高电平就直接跳回IDLE。结果发现按键按下后,稍微抖一下就回到IDLE了,根本进不了释放消抖。后来我改成“连续采样到高电平才跳回”,问题就解决了。记住:消抖的核心是“连续采样”,不是“单次采样”。
4.3 Verilog代码实现:把状态机写进FPGA
光说不练假把式。下面是我在实际项目中用过的三态消抖状态机代码。我把它简化了一下,方便你理解核心逻辑。
module debounce_fsm (
input wire clk, // 系统时钟,假设50MHz
input wire rst_n, // 异步复位,低有效
input wire key_in, // 按键输入(按下为0)
output reg key_out, // 消抖后输出(按下为0)
output reg key_pulse // 按下脉冲(按下瞬间输出一个时钟周期的高电平)
);
// 状态定义
localparam IDLE = 2'b00;
localparam PRESS_DEBOUNCE = 2'b01;
localparam RELEASE_DEBOUNCE = 2'b10;
// 消抖参数:假设需要20ms的消抖时间,50MHz时钟对应1_000_000个周期
// 这里为了仿真方便,设小一点
localparam DEBOUNCE_CNT_MAX = 100_000;
reg [1:0] state, next_state;
reg [16:0] cnt; // 计数器,用于连续采样
reg [1: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: begin
if (key_in == 1'b0)
next_state = PRESS_DEBOUNCE;
end
PRESS_DEBOUNCE: begin
if (cnt == DEBOUNCE_CNT_MAX && key_in == 1'b0)
next_state = RELEASE_DEBOUNCE;
else if (key_in == 1'b1)
next_state = IDLE;
end
RELEASE_DEBOUNCE: begin
if (cnt == DEBOUNCE_CNT_MAX && key_in == 1'b1)
next_state = IDLE;
else if (key_in == 1'b0)
next_state = PRESS_DEBOUNCE;
end
default: next_state = IDLE;
endcase
end
// 计数器逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 0;
else begin
case (state)
IDLE: cnt <= 0;
PRESS_DEBOUNCE, RELEASE_DEBOUNCE: begin
if (cnt == DEBOUNCE_CNT_MAX)
cnt <= cnt;
else
cnt <= cnt + 1;
end
default: cnt <= 0;
endcase
end
end
// 输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_out <= 1'b1;
key_pulse <= 1'b0;
end else begin
case (state)
IDLE: begin
key_out <= 1'b1;
key_pulse <= 1'b0;
end
PRESS_DEBOUNCE: begin
key_out <= 1'b1; // 未确认按下前,输出保持高
key_pulse <= 1'b0;
end
RELEASE_DEBOUNCE: begin
key_out <= 1'b0; // 确认按下后,输出低
key_pulse <= 1'b1; // 产生一个脉冲
end
default: begin
key_out <= 1'b1;
key_pulse <= 1'b0;
end
endcase
end
end
endmodule
这段代码里,我用了三段式状态机写法。为什么用三段式?因为组合逻辑和时序逻辑分开,代码更清晰,综合出来的电路也更稳定。你想想看,如果所有逻辑都揉在一个always块里,调试起来多痛苦。
避坑指南:我曾经在次态逻辑里直接用了非阻塞赋值,结果仿真时状态跳来跳去,完全不对。后来才想起来,组合逻辑必须用阻塞赋值。这个坑我踩过一次,之后就再也没犯过。你也记住:组合逻辑用=,时序逻辑用<=。
4.4 状态机消抖法的优缺点
说了这么多,我们来总结一下状态机消抖法的特点。我列个表格,一目了然。
| 优点 | 缺点 |
|---|---|
| 逻辑清晰,状态转移一目了然 | 代码量相对较大,初学者可能觉得复杂 |
| 按下和释放可以分别设置消抖时间 | 需要额外的计数器资源 |
| 抗干扰能力强,能有效滤除毛刺 | 状态机设计不当容易产生毛刺或死锁 |
| 易于扩展,比如增加长按、双击检测 | 对时序要求较高,需要仔细设计 |
我个人觉得,状态机消抖法是“一劳永逸”的方案。虽然第一次写代码时花的时间多一点,但一旦写好,后面基本不用改。而且它的可扩展性特别好。我在一个项目中,基于这个三态状态机,只加了几个状态和计数器,就实现了长按、短按、双击的检测。如果你有兴趣,后面可以自己试试。
好了,状态机消抖法就讲到这里。记住:状态机设计的核心是“状态清晰、转移明确、输出可控”。只要把握住这三点,你也能写出优雅的消抖代码。
一句话总结:三态消抖状态机,用三个状态(空闲、按下消抖、释放消抖)和连续采样机制,实现了按键抖动的可靠滤除。它比延时法和计数法更灵活、更可控,是嵌入式系统中按键处理的“高级玩法”。
可可精品资料库