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」分支,结果状态机跑飞了,仿真三天才找到原因。说白了,状态表里每个格子都要填满,哪怕下一状态是自身。

⚠️ 避坑指南: 我曾经在状态表里漏写了某个非法状态的转移路径,导致芯片在特定输入组合下死锁。从那以后,我强制自己在状态表最后加一行「others → IDLE」,哪怕综合工具会帮你处理,但写清楚能让你自己心里有底。

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
💡 小技巧: 格雷编码特别适合「顺序跳转」的状态机,比如计数器、FIFO指针。如果是乱序跳转(比如从状态A直接跳到状态D),格雷编码的优势就不明显了。

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 知识体系图

下面这张图帮你理清状态表与状态编码的核心逻辑:

状态表与状态编码知识体系 状态表设计 当前状态 → 下一状态 → 输出 二进制编码 格雷编码 独热编码 寄存器最少 毛刺风险高 相邻只变1位 适合顺序跳转 速度最快 面积最大 选择依据:状态数 | 时序要求 | 功耗约束 | 跳转方式 💡 经验法则:状态数 < 8 用独热,8~16 用格雷,> 16 用二进制

嗯,这张图把三种编码的核心特性和选择依据都串起来了。你设计的时候,对着这张图选编码方式,基本不会跑偏。

📌 我的个人建议: 别在编码方式上纠结太久。先选一种最顺手的(我一般默认用二进制),等综合报告出来,如果时序不满足或者面积超标,再换成其他编码。很多时候,工具优化得比你想的还好。

可可精品资料库