状态机设计:一段/两段/三段式、状态编码与完备性
更新时间:2026-09-05。本文是「集成电路大类 · 芯片设计」**进阶层(intermediate)**第二篇新正文(#04)。上一篇 可综合 RTL 编码规范 立了规矩,这一篇把规矩用在控制逻辑最常见的结构上。入门层 微架构入门 说过「控制靠状态机、数据靠通路」,本篇解决:状态机到底怎么写才好维护、时序好、不会跑飞。
本文要回答的问题
- Moore 和 Mealy 状态机差在哪?输出会不会有毛刺?
- 一段/两段/三段式分别是什么结构,为什么业界推荐三段式?
- binary / gray / one-hot 编码怎么选?
- 状态机「跑飞」「死锁」是怎么发生的,怎么从设计上杜绝?
一、先分清两种模型:Moore vs Mealy
- Moore 型:输出只取决于当前状态(进入某状态就输出某值)。输出与时钟沿同步、干净无毛刺,但反应慢一拍。
- Mealy 型:输出取决于当前状态 + 当前输入。少一个状态就能反应、省周期,但输出是组合逻辑,输入一变输出立刻变,可能有毛刺,不能直接当时钟/使能给外部。
工程经验:控制输出尽量 Moore 化(输出寄存一拍),Mealy 只用于内部组合判断。这正好引出三种写法。
二、三种写法:从能跑到好维护
以「带计数的突发传输」简化 FSM 为例:IDLE → BURST(发 len 笔)→ DONE。
一段式:状态转移、输出全写在一个时序 always 里。代码短,但所有判断挤在一起,综合出的次态逻辑和输出逻辑混在触发器输入端,改一处怕动全身,且输出虽然寄存了但可读性差。教学常见,工程不推荐。
两段式:一个时序块只做「现态 ≤ 次态」的寄存器;一个组合块算次态和输出。结构清晰,但输出在组合块里 = Mealy/组合输出,下游直接用有毛刺风险。
三段式(工业推荐):拆成三块,各司其职:
// ① 状态寄存器:时序逻辑,现态 <= 次态
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= IDLE;
else state <= next_state;
// ② 次态转移:纯组合,只算 next_state
always_comb begin
next_state = state; // 默认保持,防锁存器
unique case (state)
IDLE: if (start) next_state = BURST;
BURST: if (cnt == len-1) next_state = DONE;
DONE: next_state = IDLE;
default: next_state = IDLE; // 完备性
endcase
end
// ③ 输出逻辑:时序,输出寄存 -> Moore 型,无毛刺
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin req <= 1'b0; end
else begin
req <= (next_state == BURST); // 用 next_state 可提前一拍输出
end
| 写法 | 块数 | 输出类型 | 时序/毛刺 | 可维护性 | 适用 |
|---|---|---|---|---|---|
| 一段式 | 1 | 寄存 | 好 | 差,全挤一起 | 极简 FSM、教学 |
| 两段式 | 2 | 组合 | 有毛刺风险 | 中 | 输出仅内部使用 |
| 三段式 | 3 | 寄存(Moore) | 最好 | 好,改转移不动输出 | 工程默认 |
三、状态编码:省寄存器 vs 省组合逻辑
| 编码 | 触发器数 | 译码逻辑 | 翻转特点 | 典型场景 |
|---|---|---|---|---|
| binary 二进制 | ⌈log₂N⌉ 最少 | 复杂(多位比较) | 多 bit 同时翻 | ASIC 省面积、状态多 |
| gray 格雷码 | ⌈log₂N⌉ | 复杂 | 每次只翻 1 bit | 跨时钟域指针、低功耗 |
| one-hot 独热 | N 个(状态数) | 极简(1 bit 判断) | 每次翻 2 bit | FPGA 首选(触发器富裕) |
gray 码跨时钟域的用途在 FIFO 深度设计 和进阶 #11 异步 FIFO 篇细讲。实际工程常直接写 enum 让综合器 auto 编码(DC 的 set_fsm_encoding、Vivado 的 fsm_encoding 属性),但安全相关、跨时钟的状态寄存器要手动锁定编码。用 SystemVerilog enum 定义状态名而非裸 parameter,波形和日志里直接显示状态名,debug 效率天差地别。
四、完备性:不让 FSM 有「没写的路」
状态机失效有三种典型死法,全部靠设计规范杜绝:
- 跑飞(进入未定义状态):N 个状态用 binary 编码实际有 2^⌈log₂N⌉ 个编码,多出来的编码一旦因扰动进入就无人处理。对策:组合块加
default: next_state = IDLE(SystemVerilog 用unique case让工具检查完备性); - 死锁(卡在某状态出不来):转移条件设计成了「等一个永远不来的信号」。对策:每个等待状态加 timeout 退出路径;
- 复位不一致:复位后不在合法状态、输出没复位。对策:复位态显式 IDLE,输出寄存器全部纳入复位。
验证侧对应动作:状态机覆盖率(每个状态、每条转移弧都要覆盖到,见进阶层 覆盖率驱动验证 #19),加上 SVA 断言「禁止进入非法状态」(#20)。
五、FSM 与数据通路分家
控制器(FSM)发命令、数据通路(MUX/ALU/寄存器组/FIFO)执行并回状态标志(zero/empty/full/cnt_done)——这就是入门层 微架构 讲的 controller/datapath 分工。FSM 里不要塞算术运算,只做「看到什么标志、跳到什么状态、拉什么使能」;数据通路模块自己算好标志位喂回来。这样 FSM 图永远画得清楚,时序也容易收敛(关键路径在数据通路,可单独流水线,见进阶 #15 时序收敛)。
相关与延伸
上一篇:可综合 RTL 编码规范。下一篇:FIFO 深度计算与设计——FSM 常和 FIFO/握手配套出现。FSM 控制的典型外设实例见进阶 #29 常见外设 RTL;验证状态机完备性见 覆盖率驱动验证 与 SVA 断言;入门铺垫见 微架构入门、三个最常见基础模块。
总结
状态机用三段式写(状态寄存器 / 次态组合 / 输出寄存器各一块),输出寄存化走 Moore 无毛刺,default 分支 + 复位态 + timeout 保证不跑飞不死锁,编码交给工具但跨时钟用 gray、FPGA 用 one-hot。