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 (IDLE) S1 (WORK) S2 (DONE) start=1 / out=0 done=1 / out=1 reset=1 / out=0 start=0 复位

这张图里,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分支,把非法状态引导到复位状态。这个习惯救了我好几次。

好了,状态机的基础理论就讲到这里。记住:概念要清、画图要规范、编码要选对。下一节咱们会把这些理论用到实际代码中,写一个真正的状态机模块。

可可精品资料库