2. 状态机基础理论:有限状态机(FSM)概念、Moore型与Mealy型状态机区别、状态转移图绘制规范、状态编码方式
各位同学好,我是老李。今天咱们聊聊状态机的基础理论。说实话,状态机这东西,我做了十几年FPGA,几乎每个项目都离不开它。你想想看,从简单的按键消抖,到复杂的通信协议解析,背后都是状态机在干活。
嗯,咱们先搞清楚一个概念——什么是有限状态机?
2.1 有限状态机(FSM)概念
有限状态机,英文叫Finite State Machine,简称FSM。说白了,就是一个系统在有限个状态之间来回切换的模型。它有三个核心要素:状态、输入、输出。
我习惯用一个比喻来解释:想象你是一个自动售货机。你当前的状态是"等待投币",输入是"投了5块钱",然后你转移到"已投币"状态,输出是"亮起可乐按钮"。就这么简单。
在数字电路里,FSM通常由两部分组成:
- 组合逻辑:负责计算下一个状态和输出
- 时序逻辑:负责保存当前状态(也就是寄存器)
我在项目中遇到过不少新手,一上来就写状态机,结果状态定义得乱七八糟,代码根本没法维护。所以,先打好理论基础很重要。
核心要点:FSM的本质是"当前状态 + 输入 → 下一个状态 + 输出"。记住这个公式,后面所有内容都是它的变体。
2.2 Moore型与Mealy型状态机区别
这两种状态机,是面试常考题,也是实际项目中必须搞清楚的。我当年刚入行时,就因为搞混了它们,导致一个串口协议解析模块出了bug,查了两天才找到原因。
Moore型状态机:输出只取决于当前状态。换句话说,只要状态不变,输出就不变。
Mealy型状态机:输出取决于当前状态和输入。输入一变,输出可能立刻跟着变。
咱们用表格对比一下:
| 特性 | Moore型 | Mealy型 |
|---|---|---|
| 输出依赖 | 仅当前状态 | 当前状态 + 输入 |
| 输出时序 | 滞后一个时钟周期 | 输入变化立即响应 |
| 状态数量 | 通常较多 | 通常较少 |
| 抗干扰能力 | 较强 | 较弱(输入毛刺可能直通输出) |
| 典型应用 | 控制逻辑、安全关键系统 | 高速数据通路、协议解析 |
我个人习惯是:能用Moore就用Moore。为什么?因为Moore型输出稳定,不容易受输入毛刺干扰。调试起来也省心。但如果你追求极致的响应速度,比如做DDR控制器,那Mealy型更合适。
我的经验:在同一个项目中,我经常把Moore和Mealy混着用。顶层控制用Moore,底层数据通路用Mealy。各取所长,效果很好。
2.3 状态转移图绘制规范
画状态转移图,是每个FPGA工程师的基本功。我见过太多人画得乱七八糟,圆圈不圆、箭头乱飞、标注不清。嗯,这里我给大家定个规范。
基本元素:
- 圆圈:表示状态,里面写状态名称
- 箭头:表示转移方向,从当前状态指向下一个状态
- 标注:写在箭头旁边,格式为"输入条件 / 输出动作"
- 起始状态:用双圆圈或箭头指向表示
举个例子,一个简单的"两状态"系统:
输入=1 / 输出=0
┌─────────────────────┐
▼ │
┌──────┐ 输入=0 ┌──────┐
│ IDLE │───────────►│ BUSY │
└──────┘ └──────┘
│ │
└─────────────────────┘
输入=1 / 输出=1
注意看:Moore型的话,输出写在圆圈里面;Mealy型的话,输出写在箭头旁边。这个区别很关键。
避坑指南:我曾经犯过一个低级错误——状态转移图里漏画了某个转移条件。结果综合出来的电路,在特定输入组合下会进入非法状态。从那以后,我画完图一定会做"状态覆盖检查",确保每个状态的所有输入组合都有对应的转移。
下面我用SVG画一个更完整的状态转移图,展示一个简单的"三状态"系统:
这张图里,S0是空闲状态,收到start信号后进入S1工作状态,工作完成(done=1)进入S2完成状态,最后复位回到S0。每个箭头都标注了转移条件和输出值。
2.4 状态编码方式
状态编码,说白了就是给每个状态分配一个二进制编号。编码方式直接影响电路的面积、速度和功耗。我见过有人随便编,结果综合出来的电路又大又慢。
常用的编码方式有三种:
| 编码方式 | 原理 | 状态数N所需位数 | 优点 | 缺点 |
|---|---|---|---|---|
| 二进制编码 | 按自然数顺序编码 | log₂(N) | 寄存器最少 | 译码逻辑复杂 |
| 格雷码 | 相邻状态只有1位不同 | log₂(N) | 减少毛刺、低功耗 | 译码逻辑较复杂 |
| 独热码 | 每个状态对应1位为1 | N | 译码简单、速度快 | 寄存器多 |
二进制编码:最省寄存器,但组合逻辑复杂。适合状态数很多(比如几十个)的场景。我一般在状态数超过16个时优先考虑它。
格雷码:相邻状态切换时只有1位变化,能有效减少毛刺。我在做跨时钟域的状态机时特别喜欢用格雷码,因为它天然适合异步FIFO的指针设计。
独热码:每个状态一个触发器,译码逻辑就是简单的"线与"。速度最快,但寄存器用量大。FPGA里触发器资源丰富,所以我个人习惯在状态数少于8个时无脑用独热码。
我的选择原则:
- 状态数 ≤ 8:独热码,速度优先
- 状态数 8~16:格雷码,平衡速度和功耗
- 状态数 > 16:二进制码,面积优先
举个例子,4个状态用三种编码方式:
状态名 二进制 格雷码 独热码
─────────────────────────────
S0 00 00 0001
S1 01 01 0010
S2 10 11 0100
S3 11 10 1000
你看,独热码每个状态只有1位是1,译码时只需要检查对应位是否为1就行。而二进制码需要比较两位的组合,逻辑自然复杂一些。
一个小技巧:在Vitis HLS中,你可以用ap_uint<N>来定义状态寄存器,然后用枚举类型定义状态。这样代码可读性高,综合工具也能自动优化编码方式。
避坑指南:我曾经在一个项目里用了独热码,但忘了给非法状态做处理。结果上电后状态机进入了"全0"的非法状态,整个系统卡死了。从那以后,我写状态机一定会加一个default分支,把非法状态引导到复位状态。这个习惯救了我好几次。
好了,状态机的基础理论就讲到这里。记住:概念要清、画图要规范、编码要选对。下一节咱们会把这些理论用到实际代码中,写一个真正的状态机模块。