三段式状态机:三段式写法结构、第一段(时序逻辑)、第二段(组合逻辑)、第三段(输出逻辑)

聊到状态机,很多新手上来就写一段式。我当年也是这么干的,觉得代码短、看着清爽。结果呢?项目一复杂,维护起来简直想哭。后来我彻底转向了三段式,这一用就是十几年。

三段式状态机,说白了就是把状态机的三个核心功能拆开写:状态跳转次态计算输出产生。每一段各司其职,互不干扰。你想想看,这就像公司里的三个部门——人事管考勤、财务管发钱、技术管干活,各管各的,出了问题也好追责。

三段式的基本结构

三段式状态机由三个 always 块组成:

  • 第一段:时序逻辑,负责状态寄存器的更新(当前态 → 次态)
  • 第二段:组合逻辑,负责计算下一个状态(当前态 + 输入 → 次态)
  • 第三段:输出逻辑,可以是时序也可以是组合,负责产生输出信号

嗯,这里要注意:第一段和第三段都可以用时序逻辑,但第二段必须是组合逻辑。为什么?因为次态计算需要实时响应输入变化,不能等时钟沿。

第一段:时序逻辑——状态寄存器

第一段最简单,就是几个触发器。我个人习惯写成这样:

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

这段代码没什么技术含量,但有个坑:复位信号一定要处理干净。我曾经在一个项目中,复位只清了部分状态位,结果上电后状态机跑飞了,查了两天才找到原因。所以复位时一定要给 current_state 赋一个确定的值,最好是 IDLE 或者初始状态。

注意:第一段只做状态寄存器的更新,不要在这里掺和任何逻辑判断。如果你看到有人在第一段里写 case 语句,那基本可以断定他没理解三段式的精髓。

第二段:组合逻辑——次态计算

第二段是状态机的核心,也是最容易出 bug 的地方。它根据当前状态和输入信号,计算出下一个状态。

// 第二段:次态计算(组合逻辑)
always @(*) begin
    next_state = current_state;  // 默认保持
    case (current_state)
        IDLE: begin
            if (start)
                next_state = READ;
            else
                next_state = IDLE;
        end
        READ: begin
            if (data_valid)
                next_state = DONE;
            else
                next_state = READ;
        end
        DONE: begin
            next_state = IDLE;
        end
        default: next_state = IDLE;
    endcase
end

这里有个小技巧:先给 next_state 赋默认值。我习惯在 case 语句之前写一句 next_state = current_state;,这样即使某个状态没覆盖到,状态机也不会跑到未知状态去。说白了,这就是个安全网。

个人经验:第二段一定要写完整。我曾经漏掉一个状态分支,综合后生成了锁存器,整个状态机直接废了。所以记得加 default 分支,哪怕只是回到 IDLE。

第三段:输出逻辑——信号产生

第三段负责产生输出信号。这里有两种写法:

  • 组合逻辑输出:响应快,但容易产生毛刺
  • 时序逻辑输出:干净稳定,但会延迟一个时钟周期

我个人更推荐时序逻辑输出,尤其是在高速设计中。你看:

// 第三段:输出逻辑(时序)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_out <= 8'd0;
        valid    <= 1'b0;
    end else begin
        case (current_state)
            IDLE: begin
                data_out <= 8'd0;
                valid    <= 1'b0;
            end
            READ: begin
                data_out <= data_in;
                valid    <= 1'b0;
            end
            DONE: begin
                data_out <= data_out;  // 保持
                valid    <= 1'b1;
            end
            default: begin
                data_out <= 8'd0;
                valid    <= 1'b0;
            end
        endcase
    end
end

你可能会问:为什么第三段要用 current_state 而不是 next_state?因为输出应该反映当前状态,而不是未来的状态。这个细节我当年也搞混过,后来被 mentor 狠狠批了一顿。

三段式的优势对比

对比项 一段式 二段式 三段式
代码可读性
维护难度
输出毛刺 易产生 易产生 可避免
时序收敛
适合场景 极简控制 中等复杂 复杂系统
核心要点:三段式状态机的精髓在于「分离关注点」。状态跳转、次态计算、输出产生三者解耦,任何一个环节出问题,都能快速定位。我在一个 50+ 状态的大型控制器项目中,就是用三段式把 bug 率降到了接近零。

避坑指南

最后分享几个我踩过的坑:

  • 锁存器问题:第二段组合逻辑一定要覆盖所有分支,否则综合出锁存器。我曾经因为这个让整个芯片多了一版流片,成本十几万。
  • 输出时序:如果第三段用组合逻辑,输出会随输入变化而抖动。建议在输出端加一级寄存器打一拍。
  • 状态编码:别用二进制编码,用独热码(one-hot)。虽然多用了触发器,但组合逻辑更简单,时序更容易收敛。
  • 默认状态:每个 case 都要有 default,哪怕你觉得所有分支都覆盖了。FPGA 上电初始状态不确定,default 就是你的救命稻草。

嗯,三段式状态机就聊到这儿。说白了,它就是一套工程化的方法论,让状态机变得可读、可维护、可调试。你刚开始可能觉得多写了几行代码麻烦,但等你遇到复杂项目时,就会感谢当初的自己选择了三段式。