三段式状态机:第一段(时序逻辑)、第二段(组合逻辑)、第三段(输出逻辑)
说实话,刚入行那会儿,我也觉得状态机嘛,一个 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 来判断。这样输出会比状态变化提前一个周期,适合需要提前准备好数据的场景。当然,具体用哪个要看你的协议要求。
三段式对比:一张图看懂
为了让你更直观地理解三段式的结构,我画了一张流程图。你看,三个模块各司其职,数据流清晰得很。
三段式 vs 两段式:我选哪个?
你可能要问:两段式(状态更新 + 次态/输出合并)不行吗?当然可以。但我个人更偏爱三段式,原因有三:
- 可读性更好:输出逻辑单独拎出来,review 代码时一眼就能看到输出条件。
- 便于修改:如果后期要加一个输出信号,只需要改第三段,不会影响到状态跳转逻辑。
- 时序优化方便:输出可以单独选择组合还是时序,灵活性更高。
我的建议:如果你刚开始学状态机,直接上三段式。养成好习惯,后面做复杂项目才不会手忙脚乱。
避坑指南
最后,分享几个我踩过的坑,你写代码时多留个心眼:
- 组合逻辑里别用非阻塞赋值:我曾经在第二段用了
<=,结果仿真时 next_state 总是慢一拍,查了半天才发现是赋值方式错了。 - 状态编码用独热码还是格雷码?小状态机(<8 个状态)我习惯用独热码,综合出来面积小;大状态机用格雷码,可以减少跳变时的毛刺。
- 别忘了 default 分支:不管是 case 语句还是 if-else,一定要覆盖所有情况。否则综合工具会给你生成意想不到的 latch。
好了,三段式状态机就讲到这里。你写代码时多练几遍,慢慢就能体会到它的好处了。
可可精品资料库