常见状态机编码:二进制、格雷、One-hot

各位工程师朋友,今天我们来聊聊状态机编码。说实话,我在刚入行时,觉得编码方式随便选一个就行。直到有一次,产品量产后的返修率突然飙升……后来排查发现,就是编码选择不当导致的毛刺问题。

状态机编码,说白了就是怎么给每个状态分配一个唯一的“代号”。不同的编码方式,直接影响电路的功耗、速度和抗干扰能力。

二进制编码(Binary Encoding)

二进制编码是最直观的方式——用自然二进制数依次表示状态。比如 4 个状态,就用 00、01、10、11。

我习惯叫它“紧凑型选手”。为什么这么说?因为需要的触发器数量是 log₂(N),是所有编码方式中最少的。对于状态数很多的设计,它能省下不少面积。

特性 说明
触发器数量 最少(log₂N)
组合逻辑复杂度 较高(需要译码器)
功耗 中等(多位同时翻转)
抗干扰能力 较弱(多bit变化时易出错)

不过,二进制编码有个潜在陷阱:相邻状态间可能有多位同时翻转。比如从 01 到 10,两个 bit 同时变化。在同步设计中问题不大,但如果组合逻辑路径不平衡,很容易产生毛刺。

⚠ 曾经踩过的坑: 我在一个通信协议模块里,用了二进制编码做 8 状态的状态机。仿真时一切正常,但上板实测偶尔会出现状态跳转错误。后来用示波器抓时钟边沿发现,多 bit 同时变化时,组合逻辑输出会出现短暂的“中间态”,导致下一级触发器采到错误数据。

格雷编码(Gray Encoding)

格雷编码的核心思想就一个:相邻状态之间,只有 1 个 bit 变化。这简直是专门为了解决二进制编码的多bit翻转问题而生的。

举个例子,4 个状态的格雷编码序列是:00、01、11、10。你看,每次只变 1 位。

💡 个人经验: 如果状态机是按顺序循环执行的(比如流水线控制、计数器型控制器),我建议优先用格雷编码。它的抗毛刺能力比二进制强太多了。

格雷编码的代价是什么?嗯,触发器数量跟二进制一样(log₂N),但状态转移表需要专门设计。构造格雷码有个简单方法:镜像反射法。其实就是把二进制 MSB 设 0,对称复制然后 MSB 设 1。

// N=4的状态转换,用格雷编码
// 状态顺序:S0(00) -> S1(01) -> S2(11) -> S3(10) -> S0
// 每个相邻跳转都只变1位
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= GRAY_S0;  // 00
    else
        case (state)
            GRAY_S0: state <= GRAY_S1;  // 00 -> 01
            GRAY_S1: state <= GRAY_S2;  // 01 -> 11
            GRAY_S2: state <= GRAY_S3;  // 11 -> 10
            GRAY_S3: state <= GRAY_S0;  // 10 -> 00
            default: state <= GRAY_S0;
        endcase
end

你想想看,是不是特别适合那种“转一圈回来”的场景?我有个项目做 SPI 控制,8 个状态轮流跑,用了格雷编码后,再也没有毛刺误触发了。

One-hot 编码(独热码)

One-hot 编码是另一个极端。N 个状态,用 N 个触发器,每个状态只有一个触发器为 1,其余全为 0。

说白了,就是“一个状态一个人,谁说话谁举手”。这种编码的好处是:译码逻辑极其简单——不需要译码器,直接用触发器的 Q 端就知道当前状态。

特性 说明
触发器数量 最多(N)
组合逻辑复杂度 最低(无译码器)
速度 最快(组合逻辑级数少)
功耗 高(但很稳定)
🔧 我的选择原则: 如果状态数在 8 个以内,并且对速度要求极高(比如 CPU 流水线控制),我无脑选 One-hot。代价就是面积大点,但 FPGA 里触发器本来就是“便宜货”,用得起。

不过 One-hot 有个致命伤:非法的组合态。如果噪声导致两个触发器同时为 1,状态机就会进入“非法状态”。所以 One-hot 编码必须配合完备的 default 语句,检测到非法组合时要么自恢复,要么进安全态。

// 4状态的One-hot编码
parameter S0 = 4'b0001;
parameter S1 = 4'b0010;
parameter S2 = 4'b0100;
parameter S3 = 4'b1000;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= S0;
    else
        case (1'b1)  // 独热译码
            state[0]: state <= S1;
            state[1]: state <= S2;
            state[2]: state <= S3;
            state[3]: state <= S0;
            default:  state <= S0;  // 捕获非法状态!
        endcase
end
⚠ 一定要加 default! 我曾经看到过一个同事写的 One-hot 状态机,没有 default 分支。结果有一次上电时序异常,两个状态同时激活,状态机直接“死机”了。加了 default 后,就算遇到非法组合态,也能自动回到初始状态。

如何选择?一张图讲清楚

我觉得,选择编码方式不用死记硬背。你只要想清楚三个维度:面积、速度、可靠性。下面这张图帮你看清它们的关系——

状态机编码选择三维对比 二进制编码 格雷编码 One-hot 编码 面积(触发器数量) 少 多 速度(组合逻辑级数) 慢 快 抗毛刺能力(单bit翻转) 弱 强 选择建议:状态数多→ 二进制;顺序循环 → 格雷;状态数少且要求高速 → One-hot 我个人习惯:8 个状态以下用 One-hot,以上用格雷,二进制留给“面积就是命”的特殊场景

看明白了吗?二进制在最左边(面积最小)、One-hot 在最右边(面积最大但最快)。格雷编码,嗯,可以说是“中间派”——面积不大,抗干扰能力又好。

综合对比表

维度 二进制 格雷 One-hot
触发器数 log₂N log₂N N
组合逻辑深度 深 中 浅(1~2级)
相邻状态翻转 可能多bit 严格1bit 2bit(一清零一置位)
非法态检测 需译码 需译码 可直接检测多1/全0
典型场景 状态数16+的设计 状态循环/双边沿应用 FPGA、高速控制

实用判断流程

我在项目里怎么选?其实就三个问题:

  1. 状态数超过 16 了吗?

    是 → 二进制(面积压力太大,没办法)
    否 → 看问题 2

  2. 状态是按固定顺序循环的吗?

    是 → 格雷(抗干扰、省面积)
    否 → 看问题 3

  3. 主频很高(>200MHz)?

    是 → One-hot(拼速度,组合逻辑越短越好)
    否 → 格雷或二进制(看个人偏好)

✨ 我的习惯: 其实大部分场合,格雷编码是最省心的选择。面积不大、毛刺少、写代码也方便。你要是拿不准,先选格雷一定不会错得太离谱。

好了,编码方式就聊到这里。记住一点:没有最好的编码,只有最合适的编码。 选之前想清楚你的设计更缺面积、速度,还是可靠性——答案就出来了。