第一章:状态机基础
各位同学好,我是你们这门课的老朋友。今天咱们聊聊状态机——这个在嵌入式世界里无处不在的东西。
说实话,我刚开始做嵌入式那会儿,对状态机也没什么概念。觉得不就是几个 if-else 嘛,有什么好学的?直到有一次,我负责一个智能门锁项目,客户要求支持指纹、密码、刷卡、手机蓝牙四种开锁方式,还要考虑低电量、防撬报警、多次错误锁定……好家伙,if-else 嵌套了七八层,代码改一次崩一次。后来一位老前辈跟我说:「小伙子,学学状态机吧。」
嗯,从那以后,我就再也没离开过状态机。
什么是状态机
状态机,全称有限状态机(Finite State Machine, FSM)。说白了,就是一个系统在任意时刻只能处于「有限个状态」中的一个。系统根据外部事件,从一个状态切换到另一个状态。
你想想看,电梯就是一个典型的状态机:
- 它要么在「静止关门」状态
- 要么在「运行中」状态
- 要么在「开门」状态
不可能同时既开门又运行,对吧?这就是状态机的核心思想——同一时刻只有一个状态。
核心定义:有限状态机是一个数学模型,它由有限个状态以及在这些状态之间的转移构成。系统在任何时刻只能处于其中一个状态。
状态机的三要素
状态机有三个核心要素:状态、事件、动作。我习惯把它们叫做「状态机三剑客」,缺一个都不行。
1. 状态(State)
状态就是系统在某个时刻的「模样」。比如一个按键:
- 空闲态:没人按它
- 按下态:手指正在按着
- 释放态:手指刚松开
我在项目中遇到过一个问题:有人把「按下态」和「释放态」合并成一个「按下后释放」状态。结果呢?消抖逻辑全乱了。记住,每个状态必须是一个明确的、互斥的「快照」。
2. 事件(Event)
事件是触发状态变化的「导火索」。没有事件,状态机就是一潭死水。
常见的事件类型:
- 外部事件:按键按下、传感器触发、串口收到数据
- 内部事件:定时器超时、计数达到阈值、状态内部条件满足
- 时间事件:经过 100ms、到达某个时刻
举个例子,我做过一个智能灯控系统。灯在「关闭」状态时,收到「人体红外检测到人」这个事件,就切换到「开启」状态。事件来了,状态才变。
我的小技巧:事件命名要「动词+名词」,比如 EVT_BUTTON_PRESSED、EVT_TIMER_EXPIRED。别用 EVT_STATE1 这种,三个月后你自己都看不懂。
3. 动作(Action)
动作是状态切换时「做的事」。动作分三种:
- 进入动作:进入某个状态时执行(比如开灯)
- 退出动作:离开某个状态时执行(比如关灯)
- 转移动作:状态切换过程中执行(比如记录日志)
我曾经犯过一个错:把动作写在了事件处理函数里,而不是状态转移里。结果同一个事件在不同状态下触发了不同的动作,代码逻辑乱成一锅粥。后来我学乖了——动作一定要绑定在状态转移上,而不是事件上。
状态机的应用场景
状态机到底能用在哪儿?我随便列几个,都是实战中遇到的:
| 应用领域 | 典型场景 | 为什么用状态机 |
|---|---|---|
| 嵌入式控制 | 按键消抖、LED 灯效切换 | 逻辑清晰,避免 if-else 地狱 |
| 通信协议 | UART 帧解析、I2C 时序控制 | 每个字节对应一个状态,好调试 |
| 用户界面 | 菜单导航、多级界面切换 | 状态与界面一一对应,好维护 |
| 自动化设备 | 洗衣机流程、电梯控制 | 流程清晰,容易扩展新功能 |
| 游戏开发 | 角色行为、AI 决策 | 每个行为一个状态,好管理 |
你想想看,这些场景有什么共同点?都是「根据当前情况,决定下一步做什么」。状态机就是干这个的。
避坑指南:我曾经接手过一个项目,状态机里定义了 50 多个状态。一问才知道,他们把「等待 1ms」「等待 2ms」「等待 3ms」都当成了独立状态。其实用「等待中 + 计时器」一个状态就够了。记住:状态是「质」的变化,不是「量」的变化。
一个简单的例子:按键消抖
光说不练假把式。咱们看一个最经典的状态机例子——按键消抖。
按键按下时,机械触点会抖动,产生多个高低电平变化。如果用 if-else 直接判断,一次按下可能被误判成十几次。状态机怎么解决?
// 按键状态机 - 三个状态
typedef enum {
KEY_IDLE, // 空闲态:等待按键按下
KEY_DEBOUNCE, // 消抖态:等待抖动结束
KEY_PRESSED // 按下态:确认按键按下
} KeyState_t;
// 事件定义
#define EVT_KEY_DOWN 0x01 // 检测到低电平
#define EVT_KEY_UP 0x02 // 检测到高电平
#define EVT_TIMEOUT 0x03 // 消抖计时到
KeyState_t currentState = KEY_IDLE;
void KeyFSM_Handler(uint8_t event) {
switch (currentState) {
case KEY_IDLE:
if (event == EVT_KEY_DOWN) {
StartDebounceTimer(10); // 启动10ms消抖定时器
currentState = KEY_DEBOUNCE;
}
break;
case KEY_DEBOUNCE:
if (event == EVT_TIMEOUT) {
// 10ms后再次检测电平
if (ReadKeyPin() == LOW) {
currentState = KEY_PRESSED;
NotifyKeyPressed(); // 通知按键按下
} else {
currentState = KEY_IDLE; // 抖动,回到空闲
}
} else if (event == EVT_KEY_UP) {
// 消抖期间按键释放,说明是抖动
currentState = KEY_IDLE;
}
break;
case KEY_PRESSED:
if (event == EVT_KEY_UP) {
currentState = KEY_IDLE;
NotifyKeyReleased(); // 通知按键释放
}
break;
}
}
看到了吗?三个状态,三个事件,逻辑清清楚楚。每个状态只处理自己关心的事件,不会乱。这就是状态机的魅力——把复杂逻辑拆成小块,每块只管自己的事。
我的习惯:画状态转移图。画图的时候,你就能发现「哎呀,这个状态怎么没出口?」或者「这两个状态是不是可以合并?」。纸上谈兵总比代码里改来改去强。
小结
这一章咱们聊了状态机的基础:
- 什么是状态机——同一时刻只有一个状态,根据事件切换
- 三要素——状态、事件、动作,缺一不可
- 应用场景——从按键消抖到电梯控制,无处不在
下一章,咱们会深入状态机的两种实现方式——switch-case 和查表法。我会告诉你哪种更适合你的项目。嗯,到时候见。