一、状态机基础概念
什么是状态机?
状态机,说白了就是一种「有记忆的逻辑」。
我刚开始做嵌入式那会儿,觉得状态机这名字挺唬人的。后来用多了才发现,它其实就是描述一个系统在不同阶段的行为方式。你想想看,我们平时写程序,大部分时候都是顺序执行——从上往下跑。但现实世界不是这样的,系统在不同时刻处于不同状态,行为也不一样。
举个例子,一个自动门。门要么开着,要么关着,要么正在开,要么正在关。每种状态下,它对「有人来了」这个事件的反应完全不同。门关着的时候,有人来了就开门;门开着的时候,有人来了就保持不动。这就是状态机的思维。
核心定义:状态机是一种数学模型,它描述了一个对象在生命周期内,如何响应外部事件,从一个状态迁移到另一个状态。
状态机的三要素
状态机有三个核心要素,我管它叫「铁三角」——状态、事件、动作。缺一个都不行。
1. 状态(State)
状态就是系统在某个时刻的「定格画面」。它代表了系统当前所处的模式或阶段。
- 特点:状态是稳定的,系统会一直待在某个状态里,直到有事件触发它离开。
- 数量:有限个。这也是「有限状态机」名字的由来。
- 例子:按键的「按下状态」「释放状态」;通信协议的「空闲状态」「接收状态」「解析状态」。
我的经验:设计状态机时,我习惯先列所有可能的状态,然后问自己一句:「这些状态之间真的互斥吗?」如果两个状态可以同时存在,那说明你还没把状态拆干净。
2. 事件(Event)
事件是推动状态机运转的「燃料」。没有事件,状态机就是一潭死水。
- 来源:外部输入(按键按下、数据到达)、内部定时(超时、计数满)、系统条件(缓冲区空、队列满)。
- 特点:事件是瞬时的,发生完就没了。它不像状态那样能持续存在。
- 例子:串口收到一个字节、定时器溢出、用户按下按钮。
嗯,这里要注意:事件和状态不要搞混。我曾经见过有人把「正在接收数据」既当状态又当事件,结果状态图画得乱七八糟。记住,状态是「待着的地方」,事件是「让你离开的原因」。
3. 动作(Action)
动作是状态机对事件的「回应」。它是在状态迁移过程中执行的具体操作。
- 类型:
- 进入动作:进入某个状态时执行(比如打开LED)
- 退出动作:离开某个状态时执行(比如关闭LED)
- 迁移动作:迁移过程中执行(比如发送数据)
- 例子:从「空闲状态」收到「开始帧」事件,执行「清空接收缓冲区」动作,然后进入「接收状态」。
避坑指南:我曾经在项目里把动作写在了事件处理函数里,结果状态还没变,动作先执行了。后来调试了整整一天才发现——动作一定要和状态迁移绑定在一起,而不是和事件绑定。事件只是触发条件,动作是迁移的一部分。
状态机的应用场景
状态机不是理论玩具,它在嵌入式系统里无处不在。我挑三个最常见的场景说说。
1. 协议解析
协议解析是状态机的「主场」。不管是Modbus、CAN、还是自定义的通信协议,本质上都是在解析一串字节流,而字节流在不同阶段含义不同。
// 一个简单的帧接收状态机
typedef enum {
FRAME_IDLE,
FRAME_HEADER,
FRAME_LENGTH,
FRAME_DATA,
FRAME_CRC,
FRAME_DONE
} FrameState_t;
FrameState_t state = FRAME_IDLE;
void ProcessByte(uint8_t byte) {
switch (state) {
case FRAME_IDLE:
if (byte == 0xAA) { // 帧头
state = FRAME_HEADER;
// 进入动作:重置校验和
checksum = 0;
}
break;
case FRAME_HEADER:
if (byte == 0x55) { // 第二个帧头
state = FRAME_LENGTH;
} else {
state = FRAME_IDLE; // 帧头错误,回到空闲
}
break;
case FRAME_LENGTH:
dataLen = byte;
dataIdx = 0;
state = FRAME_DATA;
break;
// ... 后续状态处理
}
}
你看,每个状态只关心自己该干什么。收到帧头之前,其他字节一概不理。这就是状态机的威力——把复杂的协议解析拆成一个个清晰的小步骤。
2. 按键消抖
按键消抖是每个嵌入式工程师都会遇到的「老朋友」。机械按键按下和释放时,会产生抖动——电平在几毫秒内反复跳变。如果用普通逻辑去读,一次按键可能被误判成十几次。
状态机解决这个问题非常优雅:
typedef enum {
KEY_UP, // 按键释放状态
KEY_DEBOUNCE, // 消抖等待状态
KEY_DOWN // 按键按下状态
} KeyState_t;
KeyState_t keyState = KEY_UP;
void KeyScan(void) {
uint8_t level = ReadKeyPin();
switch (keyState) {
case KEY_UP:
if (level == 0) { // 检测到低电平(按下)
keyState = KEY_DEBOUNCE;
debounceTimer = 10; // 启动10ms消抖定时
}
break;
case KEY_DEBOUNCE:
if (--debounceTimer == 0) {
if (level == 0) {
keyState = KEY_DOWN;
// 确认按键按下,触发事件
KeyPressEvent();
} else {
keyState = KEY_UP; // 抖动干扰,回到释放状态
}
}
break;
case KEY_DOWN:
if (level == 1) { // 检测到高电平(释放)
keyState = KEY_UP;
KeyReleaseEvent();
}
break;
}
}
我的习惯:按键消抖用状态机,比用延时函数优雅太多了。延时会阻塞CPU,而状态机可以和其他任务共存。我曾经在一个项目里同时处理了8个按键,每个按键都有自己的状态机实例,互不干扰。
3. 游戏AI
游戏AI也是状态机的经典应用。一个敌人的行为可以拆成几个状态:巡逻、追击、攻击、逃跑。每个状态下,它对玩家位置的反应完全不同。
| 当前状态 | 事件(玩家进入视野) | 事件(玩家离开视野) | 事件(血量低于20%) |
|---|---|---|---|
| 巡逻 | → 追击状态 | 保持巡逻 | → 逃跑状态 |
| 追击 | 保持追击 | → 巡逻状态 | → 逃跑状态 |
| 攻击 | 保持攻击 | → 巡逻状态 | → 逃跑状态 |
| 逃跑 | 保持逃跑 | 保持逃跑 | 保持逃跑 |
你看,用状态机描述AI行为,逻辑清晰,扩展也方便。想加一个新状态?直接加一行就行,不会影响其他状态。
本章知识体系
下面这张图,是我画的状态机知识体系。它把三要素和应用场景串在了一起,方便你理解整体脉络。
状态机不是什么高深莫测的东西。它就是一套「遇到什么事,就做什么事」的规则。你只要把状态列清楚,事件定义好,动作写明白,剩下的就是水到渠成的事。
我个人觉得,状态机是嵌入式工程师必须掌握的基本功。它不挑平台,不挑语言,C语言能写,Python也能写。学会了,你写代码的思路会清晰很多。
一句话总结:状态机就是把「什么时候该干什么事」这件事,用结构化的方式表达出来。状态是「在哪」,事件是「发生了什么」,动作是「做什么」。
可可精品资料库