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

说实话,刚入行那会儿,我也觉得状态机嘛,一个 always 块全搞定,多省事。直到有一次我调试一个复杂的通信协议,单段式状态机里时序和组合逻辑搅在一起,波形一看全乱套了。从那以后,我就老老实实改用三段式了。

三段式状态机,说白了就是把状态机的三个核心功能拆开写:状态跳转、次态计算、输出产生。各管各的,互不干扰。你想想看,这样写出来的代码,不管是自己调试还是交给同事 review,都清爽得多。

核心思想:三段式 = 时序逻辑管状态更新 + 组合逻辑管下一状态 + 组合/时序逻辑管输出。每一段只做一件事。

第一段:时序逻辑——状态更新

这一段最简单,也最固定。我习惯把它放在最前面,就像给整个状态机打地基。

// 第一段:时序逻辑,更新当前状态
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

嗯,这里要注意:复位信号一定要用异步复位。我在项目中遇到过因为复位同步没做好,导致上电后状态机跑到一个非法状态,整个系统卡死。后来查了两天才发现是复位逻辑的问题。

我的习惯:第一段只用非阻塞赋值(<=),只写 current_state 和 next_state 的更新。别在这里掺和任何其他逻辑。

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

这一段是状态机的「大脑」。它根据当前状态和输入信号,算出下一个状态是什么。

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

这里有个坑,我曾经踩过好几次:组合逻辑里一定要给默认赋值。你看我第一行就写了 next_state = current_state,这样即使 case 没覆盖全,也不会产生锁存器。

警告:组合逻辑中如果漏掉某个分支,综合工具会给你生成一个 latch。这玩意儿在数字电路里就是个定时炸弹,时序分析根本过不了。我见过有人因为这个导致芯片功能异常,流片回来才发现,那叫一个欲哭无泪。

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

输出逻辑有两种写法:组合输出和时序输出。我个人更推荐时序输出,因为可以避免毛刺。

// 第三段(方式一):组合逻辑输出
always @(*) begin
    case (current_state)
        IDLE:   data_valid = 1'b0;
        READ:   data_valid = 1'b0;
        DONE:   data_valid = 1'b1;
        default: data_valid = 1'b0;
    endcase
end

// 第三段(方式二):时序逻辑输出(推荐)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_valid <= 1'b0;
    else
        case (next_state)  // 注意这里用 next_state
            DONE:   data_valid <= 1'b1;
            default: data_valid <= 1'b0;
        endcase
end

为什么推荐时序输出?因为组合逻辑的输出会随着输入变化而抖动。你想想看,如果 data_valid 直接连到后级模块的使能信号,一个毛刺就可能让数据采错。用时序输出的话,输出只在时钟沿变化,干净利落。

小技巧:时序输出时,我习惯用 next_state 而不是 current_state 来判断。这样输出会比状态变化提前一个周期,适合需要提前准备好数据的场景。当然,具体用哪个要看你的协议要求。

三段式对比:一张图看懂

为了让你更直观地理解三段式的结构,我画了一张流程图。你看,三个模块各司其职,数据流清晰得很。

第一段:时序逻辑 状态更新 current_state <= next_state 时钟沿触发 第二段:组合逻辑 次态计算 case (current_state) 输入信号决定跳转 第三段:输出逻辑 产生输出 data_valid = ... 组合/时序输出 next_state 反馈 输入信号(start, done...) 输出信号(data_valid...) 三段式状态机结构图 各段独立,数据流清晰,便于调试和维护

三段式 vs 两段式:我选哪个?

你可能要问:两段式(状态更新 + 次态/输出合并)不行吗?当然可以。但我个人更偏爱三段式,原因有三:

  • 可读性更好:输出逻辑单独拎出来,review 代码时一眼就能看到输出条件。
  • 便于修改:如果后期要加一个输出信号,只需要改第三段,不会影响到状态跳转逻辑。
  • 时序优化方便:输出可以单独选择组合还是时序,灵活性更高。
我的建议:如果你刚开始学状态机,直接上三段式。养成好习惯,后面做复杂项目才不会手忙脚乱。

避坑指南

最后,分享几个我踩过的坑,你写代码时多留个心眼:

  • 组合逻辑里别用非阻塞赋值:我曾经在第二段用了 <=,结果仿真时 next_state 总是慢一拍,查了半天才发现是赋值方式错了。
  • 状态编码用独热码还是格雷码?小状态机(<8 个状态)我习惯用独热码,综合出来面积小;大状态机用格雷码,可以减少跳变时的毛刺。
  • 别忘了 default 分支:不管是 case 语句还是 if-else,一定要覆盖所有情况。否则综合工具会给你生成意想不到的 latch。

好了,三段式状态机就讲到这里。你写代码时多练几遍,慢慢就能体会到它的好处了。

可可精品资料库