状态机基础:分类、编码与跳转逻辑

各位同学,今天咱们聊聊状态机的基础。说实话,状态机这东西,我做了十几年芯片设计,几乎每个项目都离不开它。从简单的接口控制到复杂的协议解析,状态机就像芯片的「大脑」——它决定了电路在什么时候该干什么事。

先问大家一个问题:你写状态机的时候,有没有遇到过「跑飞」的情况?就是状态莫名其妙跳到不该去的地方。嗯,我当年第一次流片回来就碰到过,查了三天才发现是状态编码没处理好。所以这一节,咱们把基础打扎实。

一、状态机的两种「性格」:Moore 与 Mealy

状态机按输出方式分两类:Moore 机和 Mealy 机。说白了,区别就在于输出跟谁有关。

1. Moore 型状态机

输出只取决于当前状态。换句话说,只要状态不变,输出就保持不变。我习惯用 Moore 机做控制逻辑,因为它的输出稳定,时序好分析。

特点:

  • 输出与输入无关(只跟当前状态有关)
  • 输出在状态跳转后一个时钟周期才变化
  • 时序分析简单,不容易出毛刺
// Moore 机示例:检测 "101" 序列
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

always @(*) begin
    case (state)
        IDLE:   next_state = (data_in) ? S1 : IDLE;
        S1:     next_state = (data_in) ? S1 : S2;
        S2:     next_state = (data_in) ? S3 : IDLE;
        S3:     next_state = (data_in) ? S1 : S2;
        default: next_state = IDLE;
    endcase
end

// 输出只跟当前状态有关
assign detect_out = (state == S3);

2. Mealy 型状态机

输出不仅取决于当前状态,还取决于输入。这意味着输出可以在状态跳转的同一拍就变化,响应更快。但代价是——容易产生毛刺。

注意:Mealy 机的组合逻辑输出路径上,输入信号的任何毛刺都会直接传到输出。我在做高速接口设计时吃过这个亏,后来在输出端加了寄存器打一拍才解决。

// Mealy 机示例:同样是检测 "101"
assign detect_out = (state == S2) && (data_in == 1'b1);

你看,同样是检测到 "101",Mealy 机在输入最后一个 '1' 的同一拍就拉高输出,而 Moore 机要等到下一拍。这就是响应速度的差异。

二、状态编码方式:三种「语言」的选择

状态编码,说白了就是给每个状态起个「二进制名字」。名字怎么起,直接影响面积、功耗和时序。我见过不少新手在这上面栽跟头。

编码方式 状态数 N 所需寄存器数 功耗 速度 适用场景
二进制编码 log₂(N) 中等 中等 状态数较多时
格雷码 log₂(N) 低 中等 相邻状态频繁跳转
独热码 N 高(寄存器多) 快 状态数少(<16)

1. 二进制编码

最直观的方式。比如 4 个状态用 2 位二进制数表示:00, 01, 10, 11。优点是寄存器用得少,但缺点是状态跳转时可能有多位同时变化,容易产生竞争。

我个人习惯在状态数超过 16 个时用二进制编码。不过要注意,一定要加 default 分支,否则综合工具会给你生成不必要的锁存器。

2. 格雷码

格雷码的特点是相邻状态之间只有 1 位不同。这有什么好处?你想想看,如果状态机在 IDLE 和 S1 之间来回跳转,二进制编码下两位都在变,功耗自然大。格雷码每次只变一位,动态功耗能降低不少。

我的经验:在做低功耗设计时,如果状态机有「循环遍历」的行为(比如遍历所有状态),格雷码是首选。我曾经在一个传感器接口项目中,把状态编码从二进制改成格雷码,动态功耗降了 15%。

3. 独热码

每个状态对应一个寄存器,只有当前状态对应的寄存器为 1,其余全为 0。比如 4 个状态:0001, 0010, 0100, 1000。

独热码的译码逻辑最简单——不需要译码器,直接拿寄存器输出当状态判断。所以速度最快。但代价是寄存器数量多,面积大。

避坑指南:我曾经在一个项目中用了 32 个状态的独热码,结果综合出来面积超标。后来改成二进制编码,面积直接砍了一半。所以独热码只适合状态数少(一般不超过 16 个)的场景。

三、状态跳转逻辑:怎么写才靠谱?

状态跳转逻辑,说白了就是「当前状态 + 输入 → 下一状态」的映射关系。这里我分享几个实战经验。

1. 三段式写法

我强烈推荐三段式状态机写法:

  • 第一段:时序逻辑,更新当前状态
  • 第二段:组合逻辑,计算下一状态
  • 第三段:输出逻辑(可以是时序也可以是组合)
// 三段式 Moore 机模板
// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

// 第二段:次态逻辑
always @(*) begin
    case (state)
        IDLE:   next_state = (cond1) ? S1 : IDLE;
        S1:     next_state = (cond2) ? S2 : S1;
        // ...
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        out <= 1'b0;
    else
        out <= (state == S2);
end

为什么推荐三段式?因为组合逻辑和时序逻辑分开,综合工具更容易优化,而且仿真时也容易 debug。

2. 避免「状态跑飞」

状态机最怕的就是跑到未定义状态。我见过最惨的一次,同事没写 default 分支,结果芯片在特定输入组合下状态跑飞,整个系统死机。

一定要做的事:

  • case 语句必须写 default 分支
  • default 分支回到 IDLE 或复位状态
  • 如果用了独热码,考虑加「非法状态检测」逻辑

3. 低功耗跳转技巧

嗯,这里有个小技巧。如果状态机大部分时间停留在某个状态(比如 IDLE),可以考虑用「门控时钟」或「操作数隔离」来降低功耗。说白了,就是让不活动的状态对应的逻辑不翻转。

// 操作数隔离示例:只在特定状态下使能计算
always @(*) begin
    if (state == CALC) begin
        result = a + b;  // 只有在 CALC 状态才做加法
    end else begin
        result = '0;     // 其他状态输出固定值,减少翻转
    end
end

四、知识体系总览

下面这张图,是我自己总结的状态机知识框架。你看一眼,心里就有数了。

状态机基础 状态机分类 Moore 型 Mealy 型 输出只与状态有关 输出与状态+输入有关 状态编码方式 二进制 格雷码 独热码 面积小 / 功耗低 / 速度快 三者不可兼得,需权衡 状态跳转逻辑 三段式写法 防跑飞设计 时序+组合+输出分离 default 分支不可少 核心原则:根据状态数、功耗要求、速度需求选择合适的编码与结构 没有最好的方案,只有最合适的方案 💡 低功耗要点:格雷码 + 操作数隔离 + 门控时钟

好了,状态机的基础就讲到这里。记住:分类决定了输出行为,编码决定了面积和功耗,跳转逻辑决定了可靠性。这三者选好了,你的状态机就成功了一大半。

可可精品资料库