状态机设计:空闲态、等待稳定态、确认态的三态转移图与Verilog实现
按键消抖这件事,说白了就是跟物理世界的「抖动」做斗争。
机械按键按下时,金属触点会弹跳几次,产生一串毛刺信号。如果你直接拿这个信号去触发逻辑,那结果就是——按一次键,系统可能响应三五次。我在早期做项目时就吃过这个亏,一个简单的按键控制LED,愣是让调试搞得焦头烂额。
后来我学乖了。用状态机来做消抖,稳定、可靠、可复用。今天咱们就聊聊这个三态消抖状态机。
为什么是三个状态?
你想想看,一个按键按下到稳定,经历了什么过程?
- 按下瞬间:信号开始抖动,电平不稳定
- 抖动衰减:弹跳幅度逐渐减小,但仍有毛刺
- 稳定阶段:电平完全稳定,可以采信
这三个阶段,正好对应三个状态:空闲态、等待稳定态、确认态。
嗯,这里要注意:有些教材会写四个状态甚至五个状态,但我个人习惯用三个。为什么?因为够用。多一个状态就多一份复杂度,少一个状态又可能漏掉毛刺。三态是经过实践检验的黄金分割点。
三态转移图
先看状态转移的逻辑关系。我画了一张图,帮你直观理解:
这张图你看懂了吗?我解释一下核心逻辑:
- 空闲态:等待按键按下。检测到低电平(按键按下),立即进入等待稳定态
- 等待稳定态:启动计数器,开始计时。如果计时过程中按键抖动返回高电平,说明是毛刺,退回空闲态。如果计数器计满,说明抖动已经过去,进入确认态
- 确认态:输出按键有效信号。一直保持,直到检测到按键释放(高电平),回到空闲态
核心思想:用「时间换稳定」。不急着相信按键的变化,给它一个窗口期,让抖动自己消停。
Verilog实现
理论说完了,上代码。这是我个人比较喜欢的一种写法,三段式状态机,清晰明了。
module debounce_fsm (
input wire clk, // 系统时钟
input wire rst_n, // 异步复位,低有效
input wire key_in, // 按键输入(假设按下为低电平)
output reg key_out // 消抖后输出(按下为低电平)
);
// 参数定义
parameter IDLE = 2'b00;
parameter WAIT_STABLE = 2'b01;
parameter CONFIRMED = 2'b10;
// 计数器最大值,假设时钟20ms,20MHz时钟
// 20ms / (1/20MHz) = 400,000
parameter MAX_CNT = 20'd400_000;
// 内部信号
reg [1:0] state, next_state;
reg [19:0] cnt;
reg cnt_rst;
reg cnt_en;
// 第一段:状态寄存器
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 = WAIT_STABLE;
else
next_state = IDLE;
end
WAIT_STABLE: begin
if (key_in == 1'b1) // 抖动返回,是毛刺
next_state = IDLE;
else if (cnt == MAX_CNT - 1) // 计时结束,确认按下
next_state = CONFIRMED;
else
next_state = WAIT_STABLE;
end
CONFIRMED: begin
if (key_in == 1'b1) // 按键释放
next_state = IDLE;
else
next_state = CONFIRMED;
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 == CONFIRMED)
key_out <= 1'b0; // 确认按下,输出低电平
else
key_out <= 1'b1; // 其他状态,输出高电平
end
// 计数器控制
assign cnt_rst = (state != WAIT_STABLE);
assign cnt_en = (state == WAIT_STABLE);
// 计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 20'd0;
else if (cnt_rst)
cnt <= 20'd0;
else if (cnt_en && cnt < MAX_CNT - 1)
cnt <= cnt + 1'b1;
end
endmodule
个人经验:MAX_CNT的值怎么选?我一般取10ms~20ms。太短了消不干净,太长了影响响应速度。20MHz时钟下,20ms对应400,000个周期。如果你用更高的时钟频率,记得按比例调整。
避坑指南
代码写完了,但有几个坑我得提醒你。这些都是我曾经踩过的:
我曾经犯过的错:
- 计数器比较用 == MAX_CNT 而不是 MAX_CNT-1:计数器从0开始,到MAX_CNT-1才是满值。用MAX_CNT比较会多等一个周期,虽然影响不大,但强迫症受不了
- 次态逻辑里漏了 default:状态机综合出来会多一堆锁存器。我早期一个项目因此多耗了30%的LUT,被领导点名批评
- 输出逻辑放在组合逻辑里:key_out用组合逻辑输出,容易产生毛刺。建议像我这样,用时序逻辑打一拍
状态机对比
三态状态机不是唯一的方案。我整理了一个对比表,帮你理解不同方案的取舍:
| 方案 | 状态数 | 资源消耗 | 抗抖动能力 | 响应速度 |
|---|---|---|---|---|
| 简单延时消抖 | 无状态机 | 低 | 一般 | 快 |
| 三态状态机 | 3 | 中 | 强 | 中 |
| 四态状态机 | 4 | 中高 | 更强 | 慢 |
| 自适应消抖 | 5+ | 高 | 最强 | 自适应 |
你看,三态状态机在资源、性能和可靠性之间取得了很好的平衡。这也是为什么它成为工业界最常用的按键消抖方案。
小结
三态状态机消抖,说白了就是「不信、等待、确认」三个步骤。不信按键的第一次变化,给它一个窗口期,确认稳定后再响应。这个思路不仅适用于按键,也适用于任何存在抖动的数字信号输入。
代码我已经贴出来了,建议你直接跑仿真看看波形。把key_in模拟成带毛刺的信号,观察状态机的跳转和key_out的输出。相信我,跑一遍仿真比看十遍文章都管用。
可可精品资料库