3. 状态机设计模式:单次触发、循环扫描、层次状态机(HSM)
状态机这东西,说白了就是让单片机知道「现在该干嘛」。我见过太多工程师把逻辑写成一锅粥——一个 switch-case 里塞了上百行,改一个功能崩三个地方。其实状态机就那么几种套路,选对了,功耗和可维护性都能上一个台阶。
今天咱们就聊聊三种最常用的模式:单次触发、循环扫描、还有层次状态机。每种我都踩过坑,咱们边看代码边聊。
3.1 单次触发状态机
这种模式最适合「按一下就执行一次」的场景。比如按键消抖、单次传感器采集、或者一次性配置外设。
它的核心逻辑是:事件来了,状态跳转,执行动作,然后停在终态。不会自己循环,除非外部再次触发。
典型应用场景:
- 按键短按/长按识别
- 单次 ADC 转换
- 初始化序列
- 错误恢复流程
我举个例子。以前做一款智能门锁,按键唤醒后需要执行「检测按下 → 消抖 → 确认有效 → 触发开锁」这个序列。用单次触发状态机,代码干净得像白纸。
typedef enum {
STATE_IDLE,
STATE_DEBOUNCE,
STATE_CONFIRM,
STATE_TRIGGER,
STATE_DONE
} KeyState_t;
KeyState_t state = STATE_IDLE;
void KeyFSM_SingleShot(Event_t event) {
switch (state) {
case STATE_IDLE:
if (event == KEY_PRESSED) {
startDebounceTimer(10); // 10ms 消抖
state = STATE_DEBOUNCE;
}
break;
case STATE_DEBOUNCE:
if (event == TIMEOUT) {
if (readKey() == PRESSED) {
state = STATE_CONFIRM;
} else {
state = STATE_IDLE; // 抖动,回到空闲
}
}
break;
case STATE_CONFIRM:
if (event == KEY_RELEASED) {
state = STATE_TRIGGER;
}
break;
case STATE_TRIGGER:
unlockDoor();
state = STATE_DONE;
break;
case STATE_DONE:
// 等待外部复位或下次触发
break;
}
}
我的习惯:单次触发状态机里,终态一定要明确。我曾经漏写 STATE_DONE 的处理,结果状态机跑飞了,门锁反复开合——嗯,那次被测试同事追着骂了一周。
3.2 循环扫描状态机
这是嵌入式里最常见的模式。主循环里不断扫描状态,每个状态执行一小块任务,然后跳转到下一个状态。适合周期性任务,比如 LED 呼吸灯、通信协议解析、或者多步传感器校准。
它的特点是:状态机自己会跑,不需要外部事件驱动。每个循环周期,状态机前进一次。
| 特性 | 单次触发 | 循环扫描 |
|---|---|---|
| 触发方式 | 外部事件 | 定时/主循环 |
| 执行次数 | 一次 | 无限循环 |
| 典型周期 | 毫秒级 | 微秒~毫秒级 |
| 功耗特点 | 空闲时可深度睡眠 | 需周期性唤醒 |
| 适用场景 | 按键、单次采集 | LED 效果、协议解析 |
来看一个呼吸灯的例子。LED 从暗到亮再到暗,循环往复。用循环扫描状态机,每个状态只做一件事:调整 PWM 占空比。
typedef enum {
RAMP_UP,
RAMP_DOWN,
HOLD_ON,
HOLD_OFF
} BreathState_t;
BreathState_t breathState = RAMP_UP;
uint16_t pwmDuty = 0;
void BreathFSM_Cyclic(void) {
switch (breathState) {
case RAMP_UP:
pwmDuty += 5;
if (pwmDuty >= 1000) {
pwmDuty = 1000;
breathState = HOLD_ON;
}
break;
case HOLD_ON:
holdCounter++;
if (holdCounter >= 50) {
holdCounter = 0;
breathState = RAMP_DOWN;
}
break;
case RAMP_DOWN:
pwmDuty -= 5;
if (pwmDuty <= 0) {
pwmDuty = 0;
breathState = HOLD_OFF;
}
break;
case HOLD_OFF:
holdCounter++;
if (holdCounter >= 30) {
holdCounter = 0;
breathState = RAMP_UP;
}
break;
}
setPWMDuty(pwmDuty);
}
避坑指南:循环扫描状态机最怕「死循环状态」。我曾经在 HOLD_ON 里忘了递增计数器,LED 直接卡在最高亮度——功耗从 5mA 飙到 50mA,电池半天就废了。记住:每个状态要么改变条件,要么有超时保护。
3.3 层次状态机(HSM)
这个就高级一点了。当状态多到一定程度,你会发现很多状态有「共同行为」。比如一个通信协议栈:空闲态、接收态、发送态、错误态——每个态都可能需要处理「超时」和「复位」这两个事件。
如果每个状态都写一遍超时处理,代码量翻倍不说,改一个超时逻辑得改 N 个地方。层次状态机的思路就是:把公共行为提取到父状态,子状态只关心自己的特殊逻辑。
HSM 的核心思想:
- 父状态处理通用事件(超时、复位、错误)
- 子状态处理具体事件(收到数据、发送完成)
- 事件从子状态向上冒泡,直到被处理
我画了一张图,帮你理解这个层级关系。
看明白了吗?父状态 CommState 处理「超时」和「复位」,四个子状态都不用再写一遍。代码实现上,我习惯用函数指针表来做事件冒泡。
typedef struct StateNode {
void (*entry)(void);
void (*exit)(void);
uint8_t (*handleEvent)(Event_t event);
struct StateNode *parent;
} StateNode_t;
uint8_t HSM_Dispatch(StateNode_t *current, Event_t event) {
StateNode_t *node = current;
while (node != NULL) {
if (node->handleEvent(event)) {
return 1; // 事件被处理
}
node = node->parent; // 冒泡到父状态
}
return 0; // 无人处理
}
我的经验:HSM 的父状态不要超过三层。我曾经在一个项目里堆了五层,结果调试时自己都搞不清事件冒泡到哪了。三层以内,逻辑清晰,代码可读性最好。
3.4 三种模式怎么选?
你想想看,选模式其实就三个问题:
- 这个任务需要重复执行吗? 不需要 → 单次触发;需要 → 循环扫描或 HSM
- 状态数量多吗? 少于 5 个 → 循环扫描就够了;多于 5 个 → 考虑 HSM
- 有没有公共行为? 有 → HSM;没有 → 循环扫描
| 判断条件 | 推荐模式 | 理由 |
|---|---|---|
| 一次性任务,状态少 | 单次触发 | 代码简单,功耗最低 |
| 周期性任务,状态少 | 循环扫描 | 实现直接,容易调试 |
| 状态多,有公共逻辑 | 层次状态机 | 复用代码,减少重复 |
| 状态多,无公共逻辑 | 循环扫描 + 表驱动 | 避免 HSM 的复杂度 |
最后提醒一句:别为了用 HSM 而用 HSM。我见过有人把两个状态的状态机硬写成 HSM,结果代码量翻了三倍,还多了一堆 bug。简单就是美,够用就好。