一、状态机基础概念
什么是状态机?
状态机,说白了就是一个「有记忆的逻辑系统」。
我经常跟团队里的新人说:你想想看,一个开关只有开和关两种状态,按一下切换一次——这就是最简单的状态机。只不过在实际的嵌入式系统里,状态可能多得多,逻辑也复杂得多。
状态机的全称叫「有限状态机」(Finite State Machine, FSM)。核心思想就一句话:系统在任意时刻只能处于有限个状态中的一个,并且根据输入事件,从一个状态跳转到另一个状态。
嗯,这里要注意:不是所有能跳转的东西都叫状态机。真正的状态机必须满足三个条件——状态是有限的、跳转是有条件的、行为是可预测的。
核心定义:有限状态机是一种数学模型,用于描述系统在有限个状态之间的行为逻辑。它由状态、事件和动作三要素构成。
状态机的三要素
我在项目中遇到过不少新手,一上来就写 if-else 嵌套,写到后面自己都看不懂。其实只要掌握了三要素,状态机的设计就清晰了。
1. 状态(State)
状态就是系统在某个时刻的「快照」。比如一个门禁系统,可以有「锁定」、「验证中」、「已开锁」三个状态。
状态有几个特点:
- 互斥性——同一时刻只能处于一个状态
- 稳定性——没有事件触发时,状态不会自己变
- 可枚举——所有状态都是预先定义好的
我的习惯:设计状态机时,我一般先用枚举把所有状态列出来。这样后面写代码时,编译器还能帮你检查有没有漏掉状态。
2. 事件(Event)
事件是触发状态跳转的「导火索」。可以是外部输入(比如按键按下),也可以是内部信号(比如定时器超时)。
举个例子:
- 按键按下 → 从「待机」跳转到「唤醒」
- 传感器数据异常 → 从「正常运行」跳转到「故障处理」
- 通信超时 → 从「等待应答」跳转到「重发」
你想想看,如果没有事件,状态机就是死的。事件才是让系统「活」起来的关键。
3. 动作(Action)
动作是状态跳转时执行的「副作用」。比如:
- 进入新状态时点亮LED
- 退出旧状态时关闭外设
- 跳转过程中记录日志
动作可以发生在三个时机:进入状态时、退出状态时、跳转过程中。我个人习惯把动作尽量放在「进入状态」时执行,这样逻辑更清晰。
我曾经踩过的坑:有一次做智能家居项目,我在状态跳转过程中做了太多动作——开灯、关空调、发通知、写日志全挤在一起。结果跳转延迟了200ms,用户体验极差。后来我把动作拆成「必须立即执行」和「可以延迟执行」两类,问题就解决了。
状态机的应用场景
状态机在嵌入式系统里几乎无处不在。我列几个典型的场景:
| 应用场景 | 典型状态 | 典型事件 |
|---|---|---|
| 按键消抖 | 空闲、按下确认、按下、释放确认 | 电平变化、定时器超时 |
| 通信协议 | 空闲、发送中、等待应答、重发 | 数据到达、超时、应答收到 |
| 电源管理 | 运行、休眠、深度休眠、唤醒 | 空闲超时、唤醒中断、电量低 |
| 菜单导航 | 主菜单、子菜单、设置、确认 | 按键上、按键下、确认键、返回键 |
| 设备控制 | 待机、启动、运行、故障、停止 | 启动指令、停止指令、故障信号 |
为什么这些场景适合用状态机?因为它们都有一个共同点:行为逻辑是「状态依赖」的。同一个按键按下,在不同状态下做的事情完全不同——这就是状态机的用武之地。
状态机的图形化表示
下面我用一个简单的按键消抖状态机来展示三要素之间的关系。这个图我画了很多次,每次给新人讲状态机都会用到。
这个图里,每个椭圆代表一个状态,箭头代表事件触发的跳转,而动作就写在跳转的「副作用」里。你看,三要素一目了然。
状态机的两种实现方式
实际编码时,状态机主要有两种写法:
方式一:switch-case 状态机
typedef enum {
STATE_IDLE,
STATE_PRESS_CONFIRM,
STATE_PRESSED,
STATE_RELEASE_CONFIRM
} state_t;
state_t current_state = STATE_IDLE;
void fsm_run(event_t event) {
switch (current_state) {
case STATE_IDLE:
if (event == EVENT_PIN_LOW) {
current_state = STATE_PRESS_CONFIRM;
// 动作:启动定时器
}
break;
case STATE_PRESS_CONFIRM:
if (event == EVENT_TIMEOUT) {
current_state = STATE_PRESSED;
// 动作:输出按键按下
} else if (event == EVENT_PIN_HIGH) {
current_state = STATE_IDLE;
// 动作:取消定时器
}
break;
// ... 其他状态
}
}
方式二:状态表驱动
typedef struct {
state_t current_state;
event_t event;
state_t next_state;
void (*action)(void);
} transition_t;
transition_t table[] = {
{STATE_IDLE, EVENT_PIN_LOW, STATE_PRESS_CONFIRM, start_timer},
{STATE_PRESS_CONFIRM, EVENT_TIMEOUT, STATE_PRESSED, key_pressed},
{STATE_PRESS_CONFIRM, EVENT_PIN_HIGH, STATE_IDLE, cancel_timer},
// ...
};
我的建议:状态少于10个时,用switch-case就够了,简单直观。状态多了,比如20个以上,我建议用状态表,维护起来方便很多。
小结
状态机不是什么高深的东西。说白了就是:系统在有限个状态之间,根据事件做跳转,跳转时执行动作。三要素——状态、事件、动作——就是它的全部。
我见过太多人把状态机搞复杂了。其实你只要记住:状态是「在哪里」,事件是「发生了什么」,动作是「要做什么」。想清楚这三件事,状态机就设计好了。
下一节我们会聊状态机的设计原则,包括怎么划分状态、怎么定义事件、怎么避免状态爆炸。这些都是我在实际项目中踩过的坑,到时候跟你细说。