4. 状态机设计基础:Moore型与Mealy型状态机、三段式状态机写法、状态编码
状态机这东西,说白了就是数字电路里的"大脑"。你想想看,串口通信要处理起始位、数据位、校验位、停止位,没有状态机来管着,数据早就乱套了。我刚开始做FPGA时,第一个实战项目就是UART收发器,那时候对状态机理解不深,写出来的代码自己都看不懂。后来踩了不少坑,才慢慢摸出门道。
4.1 Moore型与Mealy型状态机
这两种状态机的区别,其实就一句话:输出跟谁有关。
- Moore型:输出只取决于当前状态。说白了,状态变了,输出才变。
- Mealy型:输出取决于当前状态 和 输入。输入一变,输出可能立刻跟着变。
我在项目中遇到过这样一个场景:设计一个简单的数据包检测器。用Moore型做,状态多两个,但时序好分析;用Mealy型做,状态少,但组合逻辑容易出毛刺。嗯,这里要注意——Mealy型对输入信号的时序要求更严格。
核心区别速记:
- Moore型:状态决定输出,稳,但可能慢一拍
- Mealy型:输入+状态决定输出,快,但组合逻辑多
举个例子。假设我们要检测串口数据中是否出现了"101"这个序列。
Moore型状态机:需要4个状态(S0~S3),每个状态对应一个输出值。检测到"101"时,进入S3,输出1。
Mealy型状态机:只需要3个状态(S0~S2),在S2状态下如果输入为1,立刻输出1。少了一个状态,但输出是组合逻辑产生的。
我的建议:新手先从Moore型入手。逻辑清晰,不容易出错。等你把Moore型玩熟了,再尝试Mealy型去优化面积和延迟。
4.2 三段式状态机写法
写状态机,我个人最推荐三段式。为什么?因为可读性强,综合结果好,而且不容易出锁存器。
三段式是哪三段?
- 第一段:时序逻辑,描述状态跳转(当前态→次态)
- 第二段:组合逻辑,描述次态如何产生(根据输入和当前态)
- 第三段:时序逻辑或组合逻辑,描述输出
我曾经见过不少同事用一段式写状态机,一个always块里又管状态跳转又管输出,代码看着像意大利面。后来项目出了问题,定位bug花了三天。从那以后,我坚持用三段式。
来看一个UART接收器的状态机片段,Moore型,三段式写法:
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
case (current_state)
IDLE: begin
if (rx_start) // 检测到起始位
next_state = START;
else
next_state = IDLE;
end
START: begin
if (bit_done) // 起始位接收完成
next_state = DATA;
else
next_state = START;
end
DATA: begin
if (bit_cnt == 3'd7 && bit_done) // 8位数据收完
next_state = STOP;
else
next_state = DATA;
end
STOP: begin
if (bit_done) // 停止位接收完成
next_state = IDLE;
else
next_state = STOP;
end
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(Moore型,只与当前状态有关)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_data_valid <= 1'b0;
rx_data <= 8'd0;
end else begin
case (current_state)
IDLE: rx_data_valid <= 1'b0;
STOP: begin
rx_data_valid <= 1'b1; // 停止位到来时,数据有效
rx_data <= shift_reg;
end
default: rx_data_valid <= 1'b0;
endcase
end
end
避坑指南:第二段是组合逻辑,一定要用 always @(*) 或 always_comb。我曾经见过有人在这里用了 posedge clk,结果综合出来多了一级寄存器,时序全乱了。
4.3 状态编码:二进制、格雷码、独热码
状态编码这事儿,很多人觉得随便选就行。其实不然。编码方式直接影响资源消耗、速度、功耗。我根据项目经验,给你总结一下:
| 编码方式 | 资源消耗 | 速度 | 功耗 | 适用场景 |
|---|---|---|---|---|
| 二进制 | 最少(log₂N个触发器) | 中等 | 中等 | 状态数多,资源紧张时 |
| 格雷码 | 最少(log₂N个触发器) | 中等 | 低(相邻状态只变1位) | 跨时钟域、低功耗设计 |
| 独热码 | 最多(N个触发器) | 快(译码逻辑简单) | 高(翻转频繁) | 状态数少(≤16),追求速度 |
二进制编码:最省触发器。比如4个状态,用2位二进制就够了:00, 01, 10, 11。但译码逻辑复杂,组合逻辑路径长。
格雷码:相邻状态之间只有1位不同。我在做跨时钟域同步时特别喜欢用格雷码——因为每次只有1位变化,亚稳态概率大大降低。比如:00→01→11→10。
独热码:每个状态对应一个触发器,任何时候只有1位为1。比如4个状态:0001, 0010, 0100, 1000。译码逻辑就是简单的"检查某一位是否为1",速度极快。但状态数一多,触发器消耗就上去了。
我的经验:FPGA中,如果状态数不超过16个,我一般用独热码。因为FPGA的触发器资源相对丰富,而且独热码的译码逻辑简单,能跑更高的频率。如果是ASIC设计,资源紧张,那就用二进制或格雷码。
来看一个实际对比。假设有8个状态:
- 二进制:3个触发器,译码需要3输入查找表
- 格雷码:3个触发器,译码同样需要3输入查找表,但相邻状态切换功耗低
- 独热码:8个触发器,译码只需要检查对应位,不需要查找表
你想想看,在FPGA中,查找表资源往往比触发器更紧张。所以独热码虽然用了更多触发器,但节省了查找表,整体资源可能更优。
总结一下我的选择原则:
- 状态数 ≤ 16 → 独热码(速度优先)
- 状态数 16~32 → 二进制码(资源优先)
- 跨时钟域 → 格雷码(安全优先)
4.4 本章知识体系
下面这张图,是我自己梳理的状态机设计知识结构。你看一眼,心里就有谱了。
嗯,状态机这部分内容,说难不难,说简单也不简单。关键是多写、多练、多仿真。我刚开始学的时候,一个简单的UART状态机写了七八遍才彻底搞明白。别急,慢慢来。
一个小技巧:写状态机之前,先在纸上画出状态转移图。把每个状态、每个转移条件、每个输出都标清楚。画清楚了再写代码,事半功倍。
可可精品资料库