三段式状态机:三段式写法结构、第一段(时序逻辑)、第二段(组合逻辑)、第三段(输出逻辑)
聊到状态机,很多新手上来就写一段式。我当年也是这么干的,觉得代码短、看着清爽。结果呢?项目一复杂,维护起来简直想哭。后来我彻底转向了三段式,这一用就是十几年。
三段式状态机,说白了就是把状态机的三个核心功能拆开写:状态跳转、次态计算、输出产生。每一段各司其职,互不干扰。你想想看,这就像公司里的三个部门——人事管考勤、财务管发钱、技术管干活,各管各的,出了问题也好追责。
三段式的基本结构
三段式状态机由三个 always 块组成:
- 第一段:时序逻辑,负责状态寄存器的更新(当前态 → 次态)
- 第二段:组合逻辑,负责计算下一个状态(当前态 + 输入 → 次态)
- 第三段:输出逻辑,可以是时序也可以是组合,负责产生输出信号
嗯,这里要注意:第一段和第三段都可以用时序逻辑,但第二段必须是组合逻辑。为什么?因为次态计算需要实时响应输入变化,不能等时钟沿。
第一段:时序逻辑——状态寄存器
第一段最简单,就是几个触发器。我个人习惯写成这样:
// 第一段:状态寄存器更新
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
这段代码没什么技术含量,但有个坑:复位信号一定要处理干净。我曾经在一个项目中,复位只清了部分状态位,结果上电后状态机跑飞了,查了两天才找到原因。所以复位时一定要给 current_state 赋一个确定的值,最好是 IDLE 或者初始状态。
第二段:组合逻辑——次态计算
第二段是状态机的核心,也是最容易出 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;,这样即使某个状态没覆盖到,状态机也不会跑到未知状态去。说白了,这就是个安全网。
第三段:输出逻辑——信号产生
第三段负责产生输出信号。这里有两种写法:
- 组合逻辑输出:响应快,但容易产生毛刺
- 时序逻辑输出:干净稳定,但会延迟一个时钟周期
我个人更推荐时序逻辑输出,尤其是在高速设计中。你看:
// 第三段:输出逻辑(时序)
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 狠狠批了一顿。
三段式的优势对比
| 对比项 | 一段式 | 二段式 | 三段式 |
|---|---|---|---|
| 代码可读性 | 差 | 中 | 好 |
| 维护难度 | 高 | 中 | 低 |
| 输出毛刺 | 易产生 | 易产生 | 可避免 |
| 时序收敛 | 难 | 中 | 易 |
| 适合场景 | 极简控制 | 中等复杂 | 复杂系统 |
避坑指南
最后分享几个我踩过的坑:
- 锁存器问题:第二段组合逻辑一定要覆盖所有分支,否则综合出锁存器。我曾经因为这个让整个芯片多了一版流片,成本十几万。
- 输出时序:如果第三段用组合逻辑,输出会随输入变化而抖动。建议在输出端加一级寄存器打一拍。
- 状态编码:别用二进制编码,用独热码(one-hot)。虽然多用了触发器,但组合逻辑更简单,时序更容易收敛。
- 默认状态:每个 case 都要有 default,哪怕你觉得所有分支都覆盖了。FPGA 上电初始状态不确定,default 就是你的救命稻草。
嗯,三段式状态机就聊到这儿。说白了,它就是一套工程化的方法论,让状态机变得可读、可维护、可调试。你刚开始可能觉得多写了几行代码麻烦,但等你遇到复杂项目时,就会感谢当初的自己选择了三段式。