2. STM32状态机实现:三种主流方案
状态机的实现方式,说白了就是怎么把「当前状态 + 事件 → 下一状态 + 动作」这个逻辑落地到代码里。我在实际项目中见过不下十种写法,但真正经得起工程考验的,其实就三种:switch-case、函数指针表、状态表驱动。
今天咱们就把这三种方案掰开揉碎,看看它们各自适合什么场景,坑在哪,怎么选。
2.1 switch-case 实现:最直观,但别滥用
这是大多数人入门状态机的写法。一个 switch 套一个 case,每个 case 里处理当前状态下的所有事件。简单粗暴,一眼就能看懂。
// 状态枚举
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_PAUSED,
STATE_ERROR
} State_t;
// 事件枚举
typedef enum {
EVT_START,
EVT_STOP,
EVT_PAUSE,
EVT_RESUME,
EVT_ERROR
} Event_t;
// 状态机处理函数
State_t stateMachine(State_t currentState, Event_t event) {
State_t nextState = currentState;
switch (currentState) {
case STATE_IDLE:
if (event == EVT_START) {
nextState = STATE_RUNNING;
// 执行进入动作:启动定时器、点亮指示灯
}
break;
case STATE_RUNNING:
switch (event) {
case EVT_STOP:
nextState = STATE_IDLE;
break;
case EVT_PAUSE:
nextState = STATE_PAUSED;
break;
case EVT_ERROR:
nextState = STATE_ERROR;
break;
default:
break;
}
break;
case STATE_PAUSED:
if (event == EVT_RESUME) {
nextState = STATE_RUNNING;
} else if (event == EVT_STOP) {
nextState = STATE_IDLE;
}
break;
case STATE_ERROR:
if (event == EVT_STOP) {
nextState = STATE_IDLE;
}
break;
default:
break;
}
return nextState;
}
但这里有个坑——状态多了以后,代码会迅速膨胀。我曾经接手过一个项目,状态机有 20 多个状态,每个 case 里嵌套着 switch,整个函数写了 800 多行。改一个状态要翻半天,加一个新事件得把所有 case 过一遍。嗯,那感觉,谁改谁知道。
2.2 函数指针表实现:灵活,但别玩脱了
函数指针表的核心思想是:把每个状态的处理逻辑封装成一个独立的函数,然后用一个二维函数指针数组来管理状态转移。
// 状态处理函数类型
typedef State_t (*StateHandler_t)(Event_t);
// 状态处理函数声明
State_t idleHandler(Event_t event);
State_t runningHandler(Event_t event);
State_t pausedHandler(Event_t event);
State_t errorHandler(Event_t event);
// 状态处理函数表
StateHandler_t stateHandlers[] = {
idleHandler, // STATE_IDLE
runningHandler, // STATE_RUNNING
pausedHandler, // STATE_PAUSED
errorHandler // STATE_ERROR
};
// 状态机主循环
State_t currentState = STATE_IDLE;
while (1) {
Event_t event = getEvent(); // 获取事件
currentState = stateHandlers[currentState](event);
}
每个状态处理函数内部可以写自己的逻辑,互不干扰:
State_t idleHandler(Event_t event) {
switch (event) {
case EVT_START:
initMotor(); // 初始化电机
return STATE_RUNNING;
default:
return STATE_IDLE;
}
}
State_t runningHandler(Event_t event) {
switch (event) {
case EVT_STOP:
stopMotor();
return STATE_IDLE;
case EVT_PAUSE:
pauseMotor();
return STATE_PAUSED;
case EVT_ERROR:
emergencyStop();
return STATE_ERROR;
default:
return STATE_RUNNING;
}
}
但函数指针表也有它的短板。你想想看,如果状态和事件都很多,这个二维表会变得非常大。比如 20 个状态 × 15 个事件 = 300 个函数指针。而且,函数指针的调用开销比直接 switch-case 要大,在 STM32F103 这种主频不高的芯片上,高频调用的场景下会有性能问题。
2.3 状态表驱动实现:最工程化,但门槛最高
状态表驱动,说白了就是把状态转移规则做成一张表。这张表里每一行就是一个「当前状态 + 事件 → 下一状态 + 动作」的映射。状态机引擎只需要查表执行就行了。
// 状态表条目结构
typedef struct {
State_t currentState;
Event_t event;
State_t nextState;
void (*action)(void); // 转移时执行的动作
} StateTableEntry_t;
// 动作函数声明
void actionStartMotor(void);
void actionStopMotor(void);
void actionPauseMotor(void);
void actionResumeMotor(void);
void actionErrorHandler(void);
// 状态转移表
const StateTableEntry_t stateTable[] = {
{STATE_IDLE, EVT_START, STATE_RUNNING, actionStartMotor},
{STATE_RUNNING, EVT_STOP, STATE_IDLE, actionStopMotor},
{STATE_RUNNING, EVT_PAUSE, STATE_PAUSED, actionPauseMotor},
{STATE_PAUSED, EVT_RESUME, STATE_RUNNING, actionResumeMotor},
{STATE_PAUSED, EVT_STOP, STATE_IDLE, actionStopMotor},
{STATE_RUNNING, EVT_ERROR, STATE_ERROR, actionErrorHandler},
{STATE_IDLE, EVT_ERROR, STATE_ERROR, actionErrorHandler},
// 表结束标记
{STATE_MAX, EVT_MAX, STATE_MAX, NULL}
};
// 状态机引擎
State_t stateMachineEngine(State_t currentState, Event_t event) {
const StateTableEntry_t *entry = stateTable;
while (entry->action != NULL) {
if (entry->currentState == currentState && entry->event == event) {
if (entry->action != NULL) {
entry->action(); // 执行动作
}
return entry->nextState;
}
entry++;
}
// 未找到匹配项,保持当前状态
return currentState;
}
我在一个工业控制项目中用过这种方案。那个项目有 30 多个状态、20 多种事件,状态转移规则有 100 多条。如果用 switch-case,代码得写到天荒地老。用状态表驱动,整个状态机引擎只有 30 行代码,剩下的全是数据表。测试人员可以直接对着表写测试用例,开发效率翻了一倍。
2.4 三种方案对比
| 对比维度 | switch-case | 函数指针表 | 状态表驱动 |
|---|---|---|---|
| 代码可读性 | 状态少时很好 | 良好 | 需要理解表结构 |
| 可维护性 | 状态多时差 | 良好 | 优秀 |
| 执行效率 | 最高 | 中等 | 中等(线性查表) |
| 代码量 | 少 | 中等 | 引擎少,数据多 |
| 适合状态数 | ≤ 10 | ≤ 20 | ≥ 10 |
| 调试难度 | 低 | 中等 | 中等 |
| 典型场景 | 简单按键处理 | 中等复杂协议 | 工业控制、通信协议 |
2.5 三种方案的选择策略
说了这么多,到底怎么选?我个人的经验是:
- 原型验证阶段:用 switch-case,快。等需求稳定了再重构。
- 状态数 5-10 个,且不会大幅增长:switch-case 够用,别折腾。
- 状态数 10-20 个,需要团队协作:函数指针表,每个状态一个文件,分工明确。
- 状态数超过 20 个,或者需求频繁变更:状态表驱动,改表不改代码。
- 对性能要求极高(比如中断里调用):switch-case 或者优化后的状态表(用二分查找)。
嗯,三种方案都讲完了。你可能会问:「那我到底该学哪一种?」我的回答是:全都要学。因为在实际项目中,你很可能在一个系统里同时用到它们——比如主状态机用状态表驱动,子状态机用函数指针表,某个简单的按键处理用 switch-case。工具没有好坏,只有合不合适。