状态机设计:空闲态、等待稳定态、确认态的三态转移图与Verilog实现

按键消抖这件事,说白了就是跟物理世界的「抖动」做斗争。

机械按键按下时,金属触点会弹跳几次,产生一串毛刺信号。如果你直接拿这个信号去触发逻辑,那结果就是——按一次键,系统可能响应三五次。我在早期做项目时就吃过这个亏,一个简单的按键控制LED,愣是让调试搞得焦头烂额。

后来我学乖了。用状态机来做消抖,稳定、可靠、可复用。今天咱们就聊聊这个三态消抖状态机。

为什么是三个状态?

你想想看,一个按键按下到稳定,经历了什么过程?

  • 按下瞬间:信号开始抖动,电平不稳定
  • 抖动衰减:弹跳幅度逐渐减小,但仍有毛刺
  • 稳定阶段:电平完全稳定,可以采信

这三个阶段,正好对应三个状态:空闲态、等待稳定态、确认态。

嗯,这里要注意:有些教材会写四个状态甚至五个状态,但我个人习惯用三个。为什么?因为够用。多一个状态就多一份复杂度,少一个状态又可能漏掉毛刺。三态是经过实践检验的黄金分割点。

三态转移图

先看状态转移的逻辑关系。我画了一张图,帮你直观理解:

按键消抖三态状态转移图 空闲态 IDLE 等待稳定态 WAIT_STABLE 确认态 CONFIRMED key_in == 0 cnt == MAX key_in 抖动返回高电平 key_in == 1(按键释放) cnt < MAX key_in == 1 key_in == 0

这张图你看懂了吗?我解释一下核心逻辑:

  • 空闲态:等待按键按下。检测到低电平(按键按下),立即进入等待稳定态
  • 等待稳定态:启动计数器,开始计时。如果计时过程中按键抖动返回高电平,说明是毛刺,退回空闲态。如果计数器计满,说明抖动已经过去,进入确认态
  • 确认态:输出按键有效信号。一直保持,直到检测到按键释放(高电平),回到空闲态

核心思想:用「时间换稳定」。不急着相信按键的变化,给它一个窗口期,让抖动自己消停。

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的输出。相信我,跑一遍仿真比看十遍文章都管用。

可可精品资料库