3、单级状态机设计:结构、状态转移图与编码
好,咱们今天聊聊单级状态机。这是状态机设计里最基础、也是最核心的一块。你想想看,不管多复杂的嵌套结构,拆到最底层,其实都是一个个单级状态机在干活。
我个人习惯把单级状态机比作一个「有记忆的开关」。它不像组合逻辑那样,输入一变输出立马变。它得记住自己当前在哪个状态,然后根据输入,决定下一步去哪。这个「记忆」的能力,就是靠寄存器实现的。
3.1 单级状态机的结构
单级状态机,说白了就三个部分:
- 状态寄存器:存当前状态。我一般用 D 触发器实现。
- 次态逻辑:组合逻辑,算下一个状态是啥。
- 输出逻辑:组合逻辑,根据当前状态(或加上输入)产生输出。
这里有个关键点:次态逻辑的输入是当前状态和外部输入。输出逻辑则分两种——摩尔型只依赖当前状态,米利型还依赖输入。我在项目中遇到过有人把摩尔型和米利型搞混,结果仿真波形怎么看怎么不对。后来发现是输出逻辑里多引了一个输入信号。
核心公式(记牢了):
次态 = f(当前状态, 输入)
输出 = g(当前状态) // 摩尔型
输出 = g(当前状态, 输入) // 米利型
嗯,这里要注意:状态寄存器必须是时钟沿触发的。我曾经见过一个新手,用组合逻辑的反馈来「模拟」状态机,结果跑起来完全不可控。那玩意儿叫振荡器,不叫状态机。
3.2 状态转移图绘制
画状态转移图,是设计状态机最重要的一步。我个人习惯先画图,再写代码。图画清楚了,代码就是翻译工作。
状态转移图里,圆圈代表状态,箭头代表转移。箭头上的标签写「输入条件 / 输出」。举个例子:
画图时有几个坑,我踩过:
- 每个状态都要有转移条件。别漏了自环。我曾经漏掉一个自环,结果状态机跑到那个状态就「卡死」了——因为没有转移条件,综合工具会给你加个锁存器。
- 转移条件要互斥且完备。同一状态下,任意两个转移条件不能同时为真,且所有条件加起来要覆盖所有情况。否则就是「未定义行为」。
- 初始状态要标清楚。我习惯用复位信号把状态机拉到 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 个状态。关键是你得清楚你的设计目标——是要速度、要面积、还是要低功耗?想清楚了再选。
可可精品资料库