4、状态表与状态编码:状态表设计、二进制编码、格雷编码、独热编码
状态机设计里,状态表和状态编码这步,说白了就是给状态机「画骨架」和「起名字」。骨架画歪了,后面时序全乱套;名字起得不好,面积和功耗就跟着遭殃。我这些年踩过的坑,有一半都跟这俩环节有关。
4.1 状态表设计——先把逻辑理清楚
状态表是状态机的「宪法」。它规定了:当前在哪个状态,来了什么输入信号,下一步该跳到哪,以及输出什么。
我个人习惯,设计状态表之前,先画状态转移图。画图的时候别急着编码,用自然语言把每个状态的功能写清楚。比如「空闲状态等待启动信号」、「接收数据状态计数到8就跳转」——这样后面填表才不会漏。
状态表一般长这样:
| 当前状态 | 输入条件 | 下一状态 | 输出 |
|---|---|---|---|
| IDLE | start = 1 | RECV | ready = 0 |
| IDLE | start = 0 | IDLE | ready = 1 |
| RECV | cnt == 8 | DONE | data_valid = 1 |
| DONE | — | IDLE | done = 1 |
嗯,这里要注意:每个状态的所有输入分支都要覆盖到。我曾经漏掉一个「else」分支,结果状态机跑飞了,仿真三天才找到原因。说白了,状态表里每个格子都要填满,哪怕下一状态是自身。
4.2 二进制编码——最直观,但小心毛刺
二进制编码就是用自然二进制数给状态编号:IDLE=00, RECV=01, DONE=10。你想想看,这多符合直觉?写代码的时候直接 case (state) 就完事了。
但问题来了:相邻状态跳转时,可能有多位同时变化。比如从 01 跳到 10,两位都要翻转。如果寄存器输出有延迟差异,中间就可能出现 00 或 11 的毛刺。这在组合逻辑输出里特别要命。
我建议:如果状态数少于8个,且对功耗不敏感,二进制编码是最省事的。综合工具优化起来也顺手。
// 二进制编码示例
localparam IDLE = 2'b00;
localparam RECV = 2'b01;
localparam DONE = 2'b10;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
case (state)
IDLE: if (start) state <= RECV;
RECV: if (cnt == 8) state <= DONE;
DONE: state <= IDLE;
default: state <= IDLE;
endcase
end
4.3 格雷编码——相邻状态只变一位
格雷编码的核心思想:相邻状态之间,只有1位不同。比如 00 → 01 → 11 → 10。这样跳转时不会出现多位同时翻转,毛刺风险大大降低。
我在项目中遇到过:一个高速串行接口的状态机,用二进制编码时,组合逻辑输出总是有毛刺,导致下游采样出错。换成格雷编码后,问题直接消失。你想想看,就改了几个参数,省了多少调试时间。
格雷编码的缺点也很明显:状态数必须是2的幂次,否则编码不连续。而且写代码时不够直观,你得对着格雷码表查。
| 状态 | 二进制 | 格雷码 |
|---|---|---|
| IDLE | 00 | 00 |
| RECV | 01 | 01 |
| DONE | 10 | 11 |
4.4 独热编码——面积换速度
独热编码,说白了就是每个状态独占一个bit。比如4个状态就用4位:IDLE=0001, RECV=0010, DONE=0100, ERROR=1000。任何时候只有1位为1。
这样做的好处是:译码逻辑超级简单。判断当前状态就是检查某一位是否为1,不需要比较器。所以独热编码的状态机跑得飞快,时序特别好收敛。
但代价也很明显:状态数越多,寄存器越多。16个状态就要16个触发器,而二进制编码只需要4个。我见过有人为了省事,30个状态全用独热编码,结果面积爆炸。
我个人习惯:状态数少于8个,且对时序要求高,用独热编码。超过16个状态,还是老老实实用二进制或格雷码。
// 独热编码示例
localparam IDLE = 4'b0001;
localparam RECV = 4'b0010;
localparam DONE = 4'b0100;
localparam ERROR = 4'b1000;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
case (1'b1) // 注意这种写法
state[0]: if (start) state <= RECV;
state[1]: if (cnt == 8) state <= DONE;
state[2]: state <= IDLE;
state[3]: state <= IDLE;
default: state <= IDLE;
endcase
end
- 二进制编码:最省寄存器,适合状态数多、对功耗敏感的场景。注意毛刺问题。
- 格雷编码:相邻跳转无毛刺,适合顺序状态机。状态数需为2的幂次。
- 独热编码:速度最快,译码简单,适合状态数少(<8)且时序紧张的设计。
4.5 三种编码对比
| 特性 | 二进制 | 格雷码 | 独热码 |
|---|---|---|---|
| 寄存器数量 | 最少(log₂N) | 最少(log₂N) | 最多(N) |
| 组合逻辑复杂度 | 中等 | 中等 | 最低 |
| 时序性能 | 一般 | 较好 | 最好 |
| 毛刺风险 | 高 | 低 | 低 |
| 适用场景 | 状态数多、面积敏感 | 顺序跳转、跨时钟域 | 状态数少、高速设计 |
4.6 知识体系图
下面这张图帮你理清状态表与状态编码的核心逻辑:
嗯,这张图把三种编码的核心特性和选择依据都串起来了。你设计的时候,对着这张图选编码方式,基本不会跑偏。