4、二段式状态机:二段式写法结构、与三段式的对比、适用场景分析

聊完了状态机的基础概念,咱们今天来啃一块硬骨头——二段式状态机。

说实话,我刚入行那会儿,对二段式和三段式的选择特别纠结。总觉得三段式更高级,二段式就是偷懒。后来踩了几个坑才明白,没有最好的写法,只有最合适的写法。今天我就把二段式的底裤扒干净,让你彻底搞懂它。

4.1 二段式状态机的写法结构

二段式,顾名思义,就是把状态机分成两个过程块(always块)来写。

第一段:时序逻辑,描述状态跳转

这一段负责把当前状态更新为下一个状态。说白了就是每个时钟沿来的时候,把 next_state 赋值给 current_state

// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

第二段:组合逻辑,描述状态转移条件和输出

这一段就热闹了。它既要算下一个状态是啥,又要根据当前状态和输入信号,决定输出什么。两个活儿都压在这一段里。

// 第二段:状态转移 + 输出(组合逻辑)
always @(*) begin
    // 默认赋值,防止锁存器
    next_state = current_state;
    out1 = 1'b0;
    out2 = 1'b0;
    
    case (current_state)
        IDLE: begin
            if (start)
                next_state = WORK;
            else
                next_state = IDLE;
            out1 = 1'b0;
        end
        
        WORK: begin
            if (done)
                next_state = IDLE;
            else
                next_state = WORK;
            out1 = 1'b1;
            out2 = 1'b1;
        end
        
        default: begin
            next_state = IDLE;
        end
    endcase
end

关键点:第二段是组合逻辑,所以一定要给所有信号赋默认值。不然综合出来就是一堆锁存器,那可就热闹了。我曾经在项目里吃过这个亏,仿真怎么都对,上板就乱跳,查了两天才发现是组合逻辑里漏了默认赋值。

4.2 二段式 vs 三段式:到底差在哪?

咱们直接上对比表,一目了然。

对比项 二段式 三段式
代码量 少,两个always块 多,三个always块
输出类型 组合逻辑输出(可能有毛刺) 寄存器输出(干净无毛刺)
时序性能 组合路径长,频率受限 寄存器隔离,频率更高
可读性 紧凑,但逻辑混在一起 清晰,各司其职
调试难度 中等,输出毛刺难抓 低,波形干净好分析
资源占用 略少(少一级寄存器) 略多(多一级寄存器)

你看,二段式最大的问题就是输出是组合逻辑。这意味着什么?意味着你的输出信号可能会因为输入的变化而瞬间抖动。如果这个输出直接连到时钟使能或者复位信号上,那后果不堪设想。

我记得有一次做SPI从机,用二段式写的状态机。仿真跑得飞起,结果接上示波器一看,片选信号上全是毛刺。后来加了一级输出寄存器才搞定。嗯,从那以后,只要输出信号要出芯片,我必用三段式。

4.3 二段式的适用场景分析

那二段式是不是就一无是处了?当然不是。它有自己的用武之地。

场景一:内部状态指示,不对外输出

如果你的状态机只是内部用,比如控制一个计数器什么时候开始计数、什么时候清零,那二段式完全够用。因为内部信号即使有毛刺,只要在时钟沿采样的时候是稳定的就行。

场景二:资源极度受限

有些小芯片,比如CPLD或者低端FPGA,逻辑资源就那么几百个LE。这时候能省一个寄存器是一个。二段式少一级输出寄存器,在资源紧张的时候就是救命稻草。

场景三:高速流水线中的简单控制

在高速流水线里,状态机本身很简单,就两三个状态。这时候用二段式,代码简洁,综合出来的逻辑也小,时序反而好收敛。我做过一个DDR3的读数据通道,状态机就三个状态,用二段式跑的稳稳的。

我的个人建议:新手入门先用三段式,把状态机的结构搞熟了。等你有两三年经验了,再根据实际情况灵活选择二段式。别一上来就追求代码量少,先把功能做稳定再说。

4.4 避坑指南

如果你决定用二段式,这几个坑一定要避开:

  • 组合逻辑里一定要给所有输出赋默认值。我习惯在case前面先写一遍默认赋值,这样哪怕漏了某个分支,也不会生成锁存器。
  • 输出信号如果跨时钟域,必须加同步器。二段式的输出是组合逻辑,跨时钟域的时候毛刺会被采到,造成亚稳态。
  • 仿真时多看看输出波形。二段式的输出在仿真里可能看起来没问题,但实际综合后因为门延迟,毛刺就出来了。最好用后仿验证一下。

警告:千万不要在二段式的组合逻辑里写 @(posedge clk) 或者 wait 语句。这是不可综合的!我曾经见过有人把二段式的第二段写成时序逻辑,结果综合出来两个状态寄存器打架,整个状态机直接废了。

4.5 小结

二段式状态机,说白了就是用代码的紧凑换资源的节省。它适合内部简单控制、资源紧张、或者对输出毛刺不敏感的场景。但如果你要驱动外部器件,或者输出信号要跨模块传递,老老实实用三段式吧。

你想想看,一个状态机写错了,轻则功能异常,重则芯片流片回来直接废掉。所以我的原则是:能用三段式就用三段式,除非有充分的理由用二段式

下一节咱们聊聊状态机的编码方式——二进制、格雷码、独热码到底怎么选。这个选对了,能省不少事。