1. 状态机基础概念
1.1 什么是有限状态机
FSM,说白了就是「一个系统在有限个状态之间跳来跳去,每次跳都看输入条件,跳完还能决定输出什么」。你想想看,电梯、自动售货机、交通灯控制器,底层逻辑全是这玩意儿。
我刚开始学FPGA时,总觉得状态机是个高大上的概念,后来拆了几个项目才发现——它其实就是三样东西:
- 状态集合——系统所有可能的状态,比如电梯的「上行、下行、停止」
- 输入集合——触发状态变化的条件,比如按键、传感器信号
- 转移规则——当前状态 + 输入 → 下一个状态 + 输出
拿序列检测器举个例。你要检测连续输入「101」,就需要三个状态:S0(等第一个1)、S1(已经收到1)、S2(已经收到10)。每到一位数据,状态就根据当前位决定是前进、后退还是保持。这跟数字逻辑里教科书式的教材一模一样,但真正写到RTL里,坑多得很——后面我会细说。
1.2 Moore与Mealy状态机
这两个家伙的区别,是面试必问、项目必考的硬骨头。简单粗暴地说:
- Moore型:输出只跟当前状态有关,跟输入没关系。输入变了?输出不变,得等下一个时钟沿状态更新了再说。
- Mealy型:输出既跟当前状态有关,又跟当前输入有关。输入一变,输出马上可能变——不用等时钟沿。
我在项目中遇到过这样一个场景:用Moore做高速串行数据检测,输出总是晚一个周期,导致握手协议对不上。后来换成Mealy,输出跟随输入即时变化,问题就解决了。
嗯,这里要注意——不是Mealy就一定好。Mealy输出对输入噪声更敏感,组合逻辑多了还容易出毛刺。选哪种,得看你系统对响应速度和抗干扰的要求。
核心差异速记:Moore输出「慢一拍但干净」,Mealy输出「快一拍但可能带刺」。
下面这张图,我用一个检测连续「11」的序列检测器,把两种输出方式画在一起对比:
怎么选? 我的经验是:输出路径如果要求组合逻辑尽量少、抗干扰好,优先选Moore;如果对延迟敏感、要求输入变化后输出马上响应,选Mealy。曾经有一个UART的接收模块,我用Moore做导致采样窗口对不准,换成Mealy就稳了。
下面这张表,把两个家伙的差异列得清清楚楚,面试时照着背就行:
| 对比维度 | Moore | Mealy |
|---|---|---|
| 输出依赖 | 只依赖当前状态 | 依赖当前状态 + 当前输入 |
| 输出时序 | 时钟沿后更新,晚一个周期 | 输入变化后立即更新,无需等时钟 |
| 状态数量 | 通常更多(输出信息编码在状态里) | 通常更少(输出直接由组合逻辑产生) |
| 响应速度 | 慢一拍,但时序更干净 | 快一拍,但可能有组合毛刺 |
| 抗干扰能力 | 更强(输出经过寄存器同步) | 较弱(输出对输入噪声敏感) |
| 典型场景 | 控制类、安全关键系统 | 高速数据路径、协议解析 |
1.3 状态机的三种描述方式
你画电路、写代码、做调试,逃不出这三种表达:
- 状态转移图——上面SVG画的那种,直观展示状态和转移条件,方案设计阶段必用。
- 状态转移表——把图翻译成表格,每一行是一条转移规则,综合工具最喜欢这个。
- RTL代码——最终落实到Verilog或VHDL,通常三段式写法最稳。
我个人习惯先画图,再列表,最后写代码。图能帮你发现漏掉的状态和死循环,表能帮你检查转移条件是否完备。
给你一个最简单的Moore型序列检测器(检测"11")的Verilog骨架:
// 三段式Moore状态机 — 检测连续"11"
module moore_11 (
input clk, rst_n,
input din,
output reg dout
);
localparam IDLE = 2'b00,
S1 = 2'b01,
S2 = 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
next_state = IDLE; // 默认
case (state)
IDLE: if (din) next_state = S1; else next_state = IDLE;
S1: if (din) next_state = S2; else next_state = IDLE;
S2: if (din) next_state = S2; else next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) dout <= 1'b0;
else if (state == S2) dout <= 1'b1;
else dout <= 1'b0;
end
endmodule
重要提醒:三段式的第二段必须用组合逻辑(always @(*)),第三段用时序逻辑(posedge clk)。我曾经图省事把第二段也写成了时序逻辑,结果状态转移慢了整整一个周期,调试了三天才找到问题。
1.4 工程师避坑指南
干了这么多年,我在状态机上栽过的跟头不少。挑三个最常见的说说:
- 死锁状态——状态机卡在某个状态出不来了。原因通常是转移条件没覆盖完全,或者复位后进入了未定义状态。
✅ 对策:case语句一定要加default,把所有未使用的状态编码都指向安全状态(比如IDLE)。 - 冗余状态——明明3个状态能搞定的事,画了6个状态,代码又长又容易出错。
✅ 对策:先画状态转移图,合并等价状态。比如两个状态在相同输入下转到相同次态、产生相同输出,它们就能合并。 - 编码方式选错——小状态机用二进制编码,大状态机用独热码,格雷码适合跨时钟域。
✅ 对策:16个状态以下用二进制或格雷码,16个以上用独热码(减少组合逻辑深度)。
嗯,说白了,状态机就是「画图→列表→写代码→检查覆盖」这四个步骤。大多数故障都出在第二步和第四步没做到位。后面几个章节我会专门展开讲每一步的排查方法。