3、单级状态机设计:结构、状态转移图与编码

好,咱们今天聊聊单级状态机。这是状态机设计里最基础、也是最核心的一块。你想想看,不管多复杂的嵌套结构,拆到最底层,其实都是一个个单级状态机在干活。

我个人习惯把单级状态机比作一个「有记忆的开关」。它不像组合逻辑那样,输入一变输出立马变。它得记住自己当前在哪个状态,然后根据输入,决定下一步去哪。这个「记忆」的能力,就是靠寄存器实现的。

3.1 单级状态机的结构

单级状态机,说白了就三个部分:

  • 状态寄存器:存当前状态。我一般用 D 触发器实现。
  • 次态逻辑:组合逻辑,算下一个状态是啥。
  • 输出逻辑:组合逻辑,根据当前状态(或加上输入)产生输出。

这里有个关键点:次态逻辑的输入是当前状态和外部输入。输出逻辑则分两种——摩尔型只依赖当前状态,米利型还依赖输入。我在项目中遇到过有人把摩尔型和米利型搞混,结果仿真波形怎么看怎么不对。后来发现是输出逻辑里多引了一个输入信号。

核心公式(记牢了):

次态 = f(当前状态, 输入)

输出 = g(当前状态) // 摩尔型

输出 = g(当前状态, 输入) // 米利型

嗯,这里要注意:状态寄存器必须是时钟沿触发的。我曾经见过一个新手,用组合逻辑的反馈来「模拟」状态机,结果跑起来完全不可控。那玩意儿叫振荡器,不叫状态机。

3.2 状态转移图绘制

画状态转移图,是设计状态机最重要的一步。我个人习惯先画图,再写代码。图画清楚了,代码就是翻译工作。

状态转移图里,圆圈代表状态,箭头代表转移。箭头上的标签写「输入条件 / 输出」。举个例子:

IDLE WORK !start / 0 start / 0 done / 1 !done / 0

画图时有几个坑,我踩过:

  • 每个状态都要有转移条件。别漏了自环。我曾经漏掉一个自环,结果状态机跑到那个状态就「卡死」了——因为没有转移条件,综合工具会给你加个锁存器。
  • 转移条件要互斥且完备。同一状态下,任意两个转移条件不能同时为真,且所有条件加起来要覆盖所有情况。否则就是「未定义行为」。
  • 初始状态要标清楚。我习惯用复位信号把状态机拉到 IDLE 状态。别指望上电瞬间寄存器自动变成你想要的值。

小技巧:画图时用不同颜色区分不同状态组。比如空闲态用绿色,工作态用蓝色,错误态用红色。这样一眼就能看出状态机的「健康程度」。

3.3 状态编码

状态编码,就是把状态名(比如 IDLE、WORK)映射成二进制数。编码方式直接影响电路的面积、速度和功耗。常用的有三种:

编码方式 原理 状态数 N 所需位数 优点 缺点
二进制编码 按顺序分配二进制数 ceil(log2(N)) 寄存器最少 译码逻辑复杂,速度慢
格雷码 相邻状态只有1位不同 ceil(log2(N)) 减少毛刺,适合跨时钟域 状态数不是2的幂时难处理
独热码 每个状态对应1位为1 N 译码逻辑简单,速度快 寄存器多,功耗大

我个人的选择原则是这样的:

  • 状态少于 8 个:用独热码。速度快,代码好写。FPGA 里触发器多的是,不差这几个。
  • 状态 8~32 个:用二进制编码。寄存器省一半以上。
  • 状态超过 32 个:考虑分层设计。别在一个状态机里塞太多状态,否则你自己都维护不了。
  • 跨时钟域传输状态:用格雷码。格雷码相邻状态只有1位变化,能有效减少亚稳态传播。我在一个项目中用二进制编码跨时钟域,结果状态值从 3'b011 跳到 3'b100 时,中间出现了 3'b111 的毛刺,导致接收端误判。后来换成格雷码就没事了。

避坑指南:我曾经在一个项目里用了独热码,但状态数有 16 个。综合后报告显示,状态寄存器占了整个芯片面积的 30%。老板看了直接让我重做。所以独热码虽好,但别滥用。

3.4 代码示例:二进制编码的交通灯

咱们用 Verilog 写一个简单的交通灯状态机,感受一下:

module traffic_light (
    input  clk,
    input  rst_n,
    input  car_sensor,   // 有车通过
    output reg [1:0] light  // 00:红, 01:黄, 10:绿
);

    // 状态编码:二进制
    localparam IDLE   = 2'b00;
    localparam GREEN  = 2'b01;
    localparam YELLOW = 2'b10;
    localparam RED    = 2'b11;

    reg [1:0] state, next_state;

    // 状态寄存器
    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 = car_sensor ? GREEN : IDLE;
            GREEN:  next_state = YELLOW;  // 固定时间后变黄
            YELLOW: next_state = RED;
            RED:    next_state = car_sensor ? GREEN : RED;
            default: next_state = IDLE;
        endcase
    end

    // 输出逻辑
    always @(*) begin
        case (state)
            IDLE:   light = 2'b00;  // 红
            GREEN:  light = 2'b10;  // 绿
            YELLOW: light = 2'b01;  // 黄
            RED:    light = 2'b00;  // 红
            default: light = 2'b00;
        endcase
    end

endmodule

这段代码里,我用了三段式写法。为什么?因为组合逻辑和时序逻辑分开,仿真时容易定位问题。而且综合工具对三段式的优化效果最好。

记住:状态机设计的三段式是「黄金法则」——状态寄存器一段,次态逻辑一段,输出逻辑一段。别偷懒写成两段式,除非你很清楚自己在做什么。

好了,单级状态机的内容就这些。结构、画图、编码,这三样是基本功。你把这章吃透了,后面学多级嵌套才不会晕。

最后说一句:状态编码的选择没有的对错。我见过有人用独热码做 100 个状态的状态机,也见过有人用二进制编码做 4 个状态。关键是你得清楚你的设计目标——是要速度、要面积、还是要低功耗?想清楚了再选。

可可精品资料库