1. 状态机基础:概念、三要素与嵌入式应用
大家好,欢迎来到这门实战课。今天咱们从最基础的东西聊起——状态机。
说实话,我刚开始做嵌入式那会儿,觉得状态机就是个花架子。写几个 if-else 不也能跑?后来在一个多按键的遥控器项目里,被各种标志位折腾得欲哭无泪,才老老实实回头补课。嗯,从那以后,状态机就成了我代码里的标配。
1.1 什么是状态机?
状态机,全称有限状态机(Finite State Machine, FSM)。说白了,就是一个系统在任意时刻只能处于“有限个状态”中的一个。当某个事件发生时,它会根据当前状态和事件,决定要不要跳转到另一个状态,以及执行什么动作。
你想想看,这像不像我们日常生活中的红绿灯?红灯、绿灯、黄灯,三个状态。时间到了(事件),就切换。简单、清晰、不会乱。
核心思想:把复杂的逻辑拆解成“状态 + 事件 + 动作”的组合,让程序变得可预测、可调试。
1.2 状态机三要素
状态机有三个核心要素,我习惯叫它“铁三角”。缺一个,这机器就转不起来。
| 要素 | 定义 | 嵌入式中的例子 |
|---|---|---|
| 状态(State) | 系统在某一时刻的稳定情形 | 空闲、接收中、处理中、发送中 |
| 事件(Event) | 触发状态切换的外部或内部信号 | 按键按下、定时器溢出、数据到达 |
| 动作(Action) | 进入/退出状态时执行的操作 | 点亮LED、启动定时器、发送数据 |
我的小习惯:在设计状态机时,我会先把所有可能的状态列出来,画成圆圈。然后再想哪些事件能让状态之间产生连线。最后才写代码。这样不容易漏状态。
1.3 状态机在嵌入式中的应用场景
嵌入式系统里,状态机几乎无处不在。我挑几个最常见的场景说说。
1.3.1 按键消抖与长按检测
这是状态机的经典入门案例。一个按键,有按下、弹起、抖动、长按等多个状态。用状态机来处理,比用延时消抖优雅得多。
// 按键状态机示例(伪代码)
enum KeyState {
KEY_IDLE, // 空闲
KEY_DEBOUNCE, // 消抖中
KEY_PRESSED, // 已按下
KEY_LONG_PRESS // 长按
};
void key_fsm(KeyEvent event) {
switch (current_state) {
case KEY_IDLE:
if (event == KEY_DOWN) {
current_state = KEY_DEBOUNCE;
start_timer(10ms); // 启动10ms消抖定时器
}
break;
case KEY_DEBOUNCE:
if (event == TIMEOUT) {
if (key_still_down()) {
current_state = KEY_PRESSED;
action_key_press();
} else {
current_state = KEY_IDLE; // 抖动,回到空闲
}
}
break;
// ... 后续状态类似
}
}
我曾经踩过的坑:在消抖状态里,只判断了定时器超时,没考虑按键在超时前就松开了。结果按键明明只按了一下,却触发了两次。后来我在每个状态入口都加了一个“事件有效性检查”,才彻底解决。
1.3.2 通信协议解析
UART、SPI、I2C 这些通信协议,接收数据时天然适合用状态机。比如一个简单的数据帧:帧头(0xAA) + 长度 + 数据 + 校验。用状态机来解析,代码结构非常清晰。
// 串口接收状态机
enum RxState {
WAIT_HEADER, // 等待帧头
WAIT_LENGTH, // 等待长度字节
WAIT_DATA, // 等待数据
WAIT_CHECKSUM // 等待校验
};
void uart_rx_fsm(uint8_t byte) {
switch (rx_state) {
case WAIT_HEADER:
if (byte == 0xAA) {
rx_state = WAIT_LENGTH;
rx_crc = 0;
}
break;
case WAIT_LENGTH:
rx_len = byte;
rx_index = 0;
rx_state = (rx_len > 0) ? WAIT_DATA : WAIT_CHECKSUM;
break;
// ...
}
}
1.3.3 外设驱动与电源管理
STM32 的很多外设,比如 DMA、ADC、定时器,内部本身就是状态机。我们写驱动时,其实是在跟这些硬件状态机打交道。
电源管理更是状态机的重灾区。待机、睡眠、深度睡眠、运行……每个状态切换都要考虑外设状态、时钟配置、唤醒源。我见过一个同事,在电源切换时忘了关 DMA,结果芯片在睡眠状态下还被 DMA 唤醒,功耗死活降不下来。
1.4 状态机的两种实现方式
嵌入式里,状态机实现主要有两种流派。我两种都用过,各有优劣。
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| switch-case | 简单直观,代码量少 | 状态多时代码膨胀,不易维护 | 状态少于10个的简单场景 |
| 状态表(查表法) | 结构清晰,易于扩展 | 需要额外内存存储表 | 状态多、事件复杂的系统 |
我的建议:刚开始学状态机,先用 switch-case 把逻辑跑通。等状态超过七八个,再考虑重构为查表法。别一上来就搞复杂架构,容易把自己绕进去。
1.5 状态机知识体系总览
下面这张图,是我自己整理的状态机知识框架。你可以把它当作本章的“地图”,后面每一节都会对应到其中的一个模块。
1.6 本章小结
状态机不是什么高深的理论,它就是一种编程思想。把复杂逻辑拆成“状态 + 事件 + 动作”,代码会变得非常干净。
- 状态:系统在某个时刻的稳定情形
- 事件:触发状态切换的信号
- 动作:进入/退出状态时执行的操作
嵌入式里,按键、通信、外设驱动、电源管理……到处都是状态机的身影。掌握了它,你的代码会从“一团乱麻”变成“一张清晰的地图”。
记住一句话:好的状态机设计,是让 bug 无处藏身。因为每个状态、每个事件、每个动作都是明确定义的,出问题一眼就能定位。