状态机基础:分类、编码与描述方法
各位同学好,我是你们的芯片设计讲师。今天咱们聊聊状态机的基础知识。说实话,状态机这东西,我做了十几年设计,几乎每个项目都离不开它。你想想看,从简单的按键消抖到复杂的DDR控制器,底层逻辑全是状态机在撑着。
这一节我打算从三个维度来讲:状态机的分类、状态编码方式、以及怎么用代码描述状态机。嗯,都是基本功,但恰恰是这些基本功,决定了你设计的芯片能不能跑得稳、功耗低。
一、状态机的分类:Moore与Mealy
先说说分类。状态机分两种:Moore型和Mealy型。区别在哪?说白了就是输出跟谁有关。
Moore型状态机:输出只取决于当前状态。换句话说,只要状态不变,输出就不变。我在做低功耗设计时特别喜欢用Moore机,为什么?因为它的输出稳定,不会因为输入毛刺而抖动,这对降低动态功耗有好处。
Mealy型状态机:输出不仅取决于当前状态,还取决于当前输入。它的响应更快——输入一变,输出立刻跟着变。但代价是什么?容易产生毛刺。我曾经在一个高速接口项目中用了Mealy机,结果输出信号上全是毛刺,后来不得不在输出端加寄存器打一拍,才把问题解决。
核心区别总结:
- Moore机:输出 = f(当前状态) —— 稳定,但响应慢一拍
- Mealy机:输出 = f(当前状态, 输入) —— 响应快,但容易有毛刺
实际项目中怎么选?我个人习惯是:控制通路用Moore,数据通路用Mealy。控制通路需要稳定,数据通路需要快速响应。当然,这不是铁律,具体还得看你的应用场景。
二、状态编码方式:二进制、格雷码、独热码
接下来是编码方式。这部分跟功耗直接挂钩,大家打起精神来。
二进制编码:最直观的方式。比如4个状态,就用2位二进制数:00、01、10、11。优点是状态数少时用的寄存器少。但缺点也很明显——状态跳转时可能有多位同时翻转。比如从01跳到10,两位都变了,这会产生很大的动态功耗。
格雷码编码:相邻状态之间只有1位不同。比如00→01→11→10。我在做低功耗设计时,如果状态跳转是有序的(比如计数器、顺序执行),我首选格雷码。为什么?因为每次跳转只翻转1个寄存器,动态功耗能降低30%以上。我记得有个项目,用格雷码替换二进制编码后,整个状态机的功耗降了将近一半。
独热码编码:每个状态对应一个寄存器,只有1位为1。比如4个状态:0001、0010、0100、1000。优点是译码逻辑简单,速度快。但寄存器用量大——N个状态需要N个寄存器。不过,独热码在FPGA设计中很受欢迎,因为FPGA的寄存器资源丰富,而且独热码的译码逻辑可以用LUT高效实现。
| 编码方式 | 寄存器数 | 组合逻辑 | 功耗特点 | 适用场景 |
|---|---|---|---|---|
| 二进制 | log₂(N) | 复杂 | 多位翻转,功耗高 | 状态数少,对功耗不敏感 |
| 格雷码 | log₂(N) | 中等 | 单bit翻转,功耗低 | 顺序跳转,低功耗设计 |
| 独热码 | N | 简单 | 寄存器多,但无毛刺 | FPGA,高速设计 |
我的建议:如果你在做低功耗ASIC,状态数少于8个,用二进制;状态数多且跳转有序,用格雷码;如果是FPGA项目,直接用独热码,省心又省力。
三、状态机描述方法
最后说说怎么写代码。我见过太多新手把状态机写得乱七八糟,一个always块里又管状态跳转又管输出,最后仿真一跑全是bug。
我个人强烈推荐三段式写法:
- 第一段:时序逻辑,描述状态跳转
- 第二段:组合逻辑,描述下一状态
- 第三段:组合逻辑或时序逻辑,描述输出
这样写的好处是结构清晰,综合工具也能优化得更好。来看看代码:
// 三段式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 = start ? WORK : IDLE;
WORK: next_state = done ? IDLE : WORK;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
out <= 1'b0;
else if (state == WORK)
out <= 1'b1;
else
out <= 1'b0;
end
避坑指南:我曾经犯过一个错误——在第二段组合逻辑里用了非阻塞赋值(<=),结果仿真时状态跳转总是不对。记住:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。这是铁律,别搞混了。
另外,写状态机时别忘了加default分支。为什么?因为综合工具会认为你没覆盖到的状态是don't care,然后给你生成一堆锁存器。嗯,锁存器这东西,在数字设计里能不用就别用,功耗高还容易出问题。
知识体系总览
下面这张图把本节内容串起来了,大家可以对照着看:
这张图把三个知识点串在了一起。你从中心往外看,左边是分类,中间是编码,右边是描述方法。底部那句话是我多年经验的总结——低功耗设计,优先考虑格雷码加Moore型加三段式描述。当然,这不是万能公式,但至少能帮你少走很多弯路。
好了,状态机的基础知识就讲到这里。记住:分类决定了你的输出行为,编码决定了你的功耗水平,描述方法决定了你的代码质量。这三样东西,缺一不可。
课后思考:如果你要设计一个低功耗的UART接收器,状态机应该选Moore还是Mealy?编码用格雷码还是独热码?为什么?
可可精品资料库