2. 状态机基础理论:有限状态机(FSM)核心概念

说实话,很多工程师一听到「状态机」三个字,就觉得是高大上的理论。其实没那么玄乎。我做了十几年嵌入式,状态机是我用得最多的设计模式之一。它说白了就是一套规则:当前在什么状态,来了什么事件,该做什么动作,下一步跳到哪。

今天咱们就把这五个核心概念掰开揉碎讲清楚。状态、事件、动作、转移,再加上一个「初始状态」。搞懂这些,你就能看懂八成以上的状态机代码。

2.1 状态(State)——系统在某个时刻的「快照」

状态是什么?就是系统在某个时间点上的稳定情形。比如一个LED灯,要么亮着,要么灭着。一个串口收发模块,要么在等待帧头,要么在接收数据,要么在校验。

我在项目中遇到过最典型的例子:一个电机驱动模块,有「停止」「加速」「匀速」「减速」「急停」五个状态。每个状态下,PWM输出、电流采样频率、故障处理逻辑都不一样。如果不做状态机,代码里全是 if-else 嵌套,改一个逻辑要翻半天。

状态的三要素:
  • 唯一性——同一时刻只能处于一个状态
  • 稳定性——没有事件触发时,状态保持不变
  • 可观测性——通过某些变量或输出可以判断当前状态

嗯,这里要注意:不要把「状态」和「变量」搞混。状态是离散的、有限的,而变量可以是连续的。比如温度值不是状态,但「温度过高」「温度正常」「温度过低」这三个区间才是状态。

2.2 事件(Event)——触发变化的「导火索」

事件就是让状态机动起来的东西。没有事件,状态机就死在那。事件可以是外部中断、定时器超时、消息队列里的数据、某个变量达到阈值……

我个人习惯把事件分为三类:

事件类型 来源 例子
外部事件 硬件引脚、外设中断 GPIO下降沿、UART接收中断
内部事件 软件定时器、任务间通信 10ms超时、消息队列有数据
系统事件 OS调度、错误处理 任务切换、看门狗复位

曾经有个项目,同事把所有事件都定义成枚举,结果枚举值从0排到200多。调试的时候根本分不清哪个事件对应哪个场景。我后来建议他按模块分组,比如 EVT_UART_RX_DONE、EVT_TIMER_100MS,一眼就能看出来源。

2.3 动作(Action)——状态机「干活」的部分

动作就是状态机在响应事件时执行的具体操作。它可以是:

  • 设置GPIO电平
  • 启动/停止定时器
  • 发送消息到队列
  • 更新内部变量
  • 调用外部函数

动作有三种常见时机:

  1. 进入动作(Entry Action)——进入某个状态时执行一次。比如进入「发送」状态时,把数据塞进TX缓冲区。
  2. 退出动作(Exit Action)——离开某个状态时执行一次。比如退出「接收」状态时,关闭接收中断。
  3. 转移动作(Transition Action)——在状态转移过程中执行。比如从「空闲」转到「忙碌」时,点亮指示灯。
我的建议:能用 Entry/Exit Action 解决的问题,就别写在转移动作里。这样状态机的逻辑更清晰,每个状态只管自己进出时该做的事。

2.4 转移(Transition)——状态之间的「桥梁」

转移定义了:当前状态 + 事件 → 下一个状态 + 动作。它是状态机的核心逻辑。

一个完整的转移包含五个要素:

  • 源状态——从哪来
  • 目标状态——到哪去
  • 触发事件——什么条件触发
  • 守卫条件(可选)——额外判断,比如事件到了但数据还没准备好,就不转移
  • 动作——转移过程中执行什么

举个例子:

// 伪代码:LED状态机转移表
// 当前状态: LED_OFF
// 事件:     EVT_BUTTON_PRESS
// 守卫:     button_debounce == OK
// 动作:     HAL_GPIO_WritePin(LED_PIN, SET);
// 下一状态: LED_ON

你想想看,如果没有守卫条件,按键一抖动就触发转移,那LED就会乱闪。这就是为什么实际项目中,守卫条件必不可少。

2.5 初始状态——一切从这里开始

状态机必须有一个明确的起点。系统上电或复位后,状态机应该进入哪个状态?这就是初始状态。

我一般把初始状态设计成「空闲」或「待机」。在这个状态下,所有外设处于安全配置,不执行任何实质性操作。等收到第一个有效事件后,再转移到工作状态。

避坑指南:千万不要把初始状态设计成「未知」或「无」。我曾经在一个项目中看到状态机初始化为0,结果0正好对应一个工作状态,上电后电机直接转了半圈才停下来……从那以后,我强制要求每个状态机必须有一个显式的初始状态枚举值,比如 STATE_INIT。

2.6 一张图看懂FSM核心概念

下面这张图展示了一个简单的按键控制LED状态机。它包含了我们刚才讲的所有要素。

按键控制LED有限状态机 LED_OFF 灯灭状态 LED_ON 灯亮状态 S EVT_BUTTON_PRESS 动作: 点亮LED EVT_BUTTON_PRESS 动作: 熄灭LED 守卫条件:按键消抖完成 (button_debounce == OK) 只有消抖通过后,按键事件才有效

这张图里,LED_OFF 和 LED_ON 是两个状态。初始状态是 LED_OFF(用绿色圆圈S标记)。每次按键事件触发一次转移,同时执行对应的动作。守卫条件确保按键消抖完成才响应。

2.7 状态机 vs 传统if-else:差距在哪?

对比维度 传统if-else 有限状态机
可读性 嵌套深,逻辑散乱 状态+事件清晰,一目了然
可维护性 加一个分支要改多处 加一个状态/事件,只改转移表
可测试性 难以覆盖所有路径 状态覆盖+转移覆盖,测试用例明确
资源开销 无额外开销 少量RAM存状态变量,可忽略
适合场景 简单顺序逻辑(2-3个分支) 复杂多状态交互(4个状态以上)

我个人经验是:超过3个分支的复杂逻辑,直接上状态机。别犹豫。if-else 嵌套到第4层的时候,你自己都看不懂上个月写的代码。

2.8 小结:记住这五个词就够了

状态机五要素速记:

  • 状态——系统在哪
  • 事件——发生了什么
  • 动作——要做什么
  • 转移——下一步去哪
  • 初始状态——从哪开始