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电平
- 启动/停止定时器
- 发送消息到队列
- 更新内部变量
- 调用外部函数
动作有三种常见时机:
- 进入动作(Entry Action)——进入某个状态时执行一次。比如进入「发送」状态时,把数据塞进TX缓冲区。
- 退出动作(Exit Action)——离开某个状态时执行一次。比如退出「接收」状态时,关闭接收中断。
- 转移动作(Transition Action)——在状态转移过程中执行。比如从「空闲」转到「忙碌」时,点亮指示灯。
2.4 转移(Transition)——状态之间的「桥梁」
转移定义了:当前状态 + 事件 → 下一个状态 + 动作。它是状态机的核心逻辑。
一个完整的转移包含五个要素:
- 源状态——从哪来
- 目标状态——到哪去
- 触发事件——什么条件触发
- 守卫条件(可选)——额外判断,比如事件到了但数据还没准备好,就不转移
- 动作——转移过程中执行什么
举个例子:
// 伪代码:LED状态机转移表
// 当前状态: LED_OFF
// 事件: EVT_BUTTON_PRESS
// 守卫: button_debounce == OK
// 动作: HAL_GPIO_WritePin(LED_PIN, SET);
// 下一状态: LED_ON
你想想看,如果没有守卫条件,按键一抖动就触发转移,那LED就会乱闪。这就是为什么实际项目中,守卫条件必不可少。
2.5 初始状态——一切从这里开始
状态机必须有一个明确的起点。系统上电或复位后,状态机应该进入哪个状态?这就是初始状态。
我一般把初始状态设计成「空闲」或「待机」。在这个状态下,所有外设处于安全配置,不执行任何实质性操作。等收到第一个有效事件后,再转移到工作状态。
STATE_INIT。
2.6 一张图看懂FSM核心概念
下面这张图展示了一个简单的按键控制LED状态机。它包含了我们刚才讲的所有要素。
这张图里,LED_OFF 和 LED_ON 是两个状态。初始状态是 LED_OFF(用绿色圆圈S标记)。每次按键事件触发一次转移,同时执行对应的动作。守卫条件确保按键消抖完成才响应。
2.7 状态机 vs 传统if-else:差距在哪?
| 对比维度 | 传统if-else | 有限状态机 |
|---|---|---|
| 可读性 | 嵌套深,逻辑散乱 | 状态+事件清晰,一目了然 |
| 可维护性 | 加一个分支要改多处 | 加一个状态/事件,只改转移表 |
| 可测试性 | 难以覆盖所有路径 | 状态覆盖+转移覆盖,测试用例明确 |
| 资源开销 | 无额外开销 | 少量RAM存状态变量,可忽略 |
| 适合场景 | 简单顺序逻辑(2-3个分支) | 复杂多状态交互(4个状态以上) |
我个人经验是:超过3个分支的复杂逻辑,直接上状态机。别犹豫。if-else 嵌套到第4层的时候,你自己都看不懂上个月写的代码。
2.8 小结:记住这五个词就够了
状态机五要素速记:
- 状态——系统在哪
- 事件——发生了什么
- 动作——要做什么
- 转移——下一步去哪
- 初始状态——从哪开始