8. 状态模式:状态机实现、状态表驱动、状态切换逻辑
状态模式,说白了就是让对象的行为随着内部状态的改变而改变。你想想看,嵌入式系统里到处都是状态——按键按下、松开,设备初始化、运行、休眠,通信协议的空闲、接收、发送……每个状态下的行为都不一样,而且状态之间还有复杂的转换关系。
我刚开始做嵌入式那会儿,处理状态切换就是一堆 if-else 嵌套。代码写到后面,自己都看不懂了。后来接触了状态模式,才算是找到了正解。
8.1 状态机的三种实现方式
状态机的实现,我个人习惯分成三种:嵌套 if-else、switch-case 和 状态表驱动。前两种大家应该都用过,咱们重点聊聊状态表驱动。
8.1.1 嵌套 if-else 与 switch-case
先看个简单的例子。一个交通灯系统,有红灯、绿灯、黄灯三种状态。用 switch-case 实现是这样的:
typedef enum {
STATE_RED,
STATE_GREEN,
STATE_YELLOW
} TrafficLightState;
void traffic_light_run(TrafficLightState state) {
switch (state) {
case STATE_RED:
turn_on_red();
// 延时后切换到绿灯
break;
case STATE_GREEN:
turn_on_green();
// 延时后切换到黄灯
break;
case STATE_YELLOW:
turn_on_yellow();
// 延时后切换到红灯
break;
default:
break;
}
}
这种写法简单直观,但问题也很明显。状态一多,case 分支就膨胀。而且每个状态里的行为逻辑和切换逻辑混在一起,改一个地方可能影响全局。
我曾经在一个项目里接手过别人写的代码,一个函数里嵌套了七八层 if-else,状态变量有十几个。改一个 bug 要花半天时间梳理逻辑。从那以后,我坚决不用这种写法处理复杂状态机。
8.1.2 状态表驱动
状态表驱动,说白了就是把状态和事件的关系抽出来,放到一张表里。这张表告诉你:当前在什么状态,来了什么事件,应该执行什么动作,然后切换到什么状态。
先定义状态和事件:
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_PAUSED,
STATE_STOPPED
} State;
typedef enum {
EVENT_START,
EVENT_PAUSE,
EVENT_RESUME,
EVENT_STOP
} Event;
然后定义状态表的结构:
typedef struct {
State current_state;
Event event;
void (*action)(void);
State next_state;
} StateTableEntry;
接着填充状态表:
StateTableEntry state_table[] = {
{STATE_IDLE, EVENT_START, action_start, STATE_RUNNING},
{STATE_RUNNING, EVENT_PAUSE, action_pause, STATE_PAUSED},
{STATE_RUNNING, EVENT_STOP, action_stop, STATE_STOPPED},
{STATE_PAUSED, EVENT_RESUME, action_resume, STATE_RUNNING},
{STATE_PAUSED, EVENT_STOP, action_stop, STATE_STOPPED},
// ... 其他条目
};
最后是状态机的执行引擎:
State current_state = STATE_IDLE;
void state_machine_run(Event event) {
for (int i = 0; i < sizeof(state_table)/sizeof(state_table[0]); i++) {
if (state_table[i].current_state == current_state &&
state_table[i].event == event) {
if (state_table[i].action) {
state_table[i].action();
}
current_state = state_table[i].next_state;
return;
}
}
// 未找到匹配项,可以记录错误或忽略
}
核心思想:状态表把「状态-事件-动作-下一状态」的映射关系集中管理。增加新状态或新事件,只需要在表里加一行,不需要修改核心引擎代码。
8.2 状态切换逻辑的细节
状态切换看着简单,但实际项目中坑不少。我总结几个关键点。
8.2.1 进入动作与退出动作
很多时候,进入一个状态和离开一个状态需要执行特定的操作。比如进入休眠状态前要保存上下文,离开休眠状态后要恢复外设配置。
我建议在状态表里增加两个字段:entry_action 和 exit_action。进入状态时调用 entry_action,离开状态时调用 exit_action。
typedef struct {
State current_state;
Event event;
void (*entry_action)(void);
void (*action)(void);
void (*exit_action)(void);
State next_state;
} StateTableEntry;
执行引擎也要相应调整:
void state_machine_run(Event event) {
for (int i = 0; i < sizeof(state_table)/sizeof(state_table[0]); i++) {
if (state_table[i].current_state == current_state &&
state_table[i].event == event) {
// 退出当前状态
if (state_table[i].exit_action) {
state_table[i].exit_action();
}
// 执行动作
if (state_table[i].action) {
state_table[i].action();
}
// 进入新状态
current_state = state_table[i].next_state;
if (state_table[i].entry_action) {
state_table[i].entry_action();
}
return;
}
}
}
小技巧:entry_action 和 exit_action 可以用来做状态切换时的资源管理。比如进入某个状态时申请内存,退出时释放。这样不容易遗漏。
8.2.2 状态表的优化
状态表如果很大,线性查找可能影响性能。我常用的优化方法有两种:
- 二维数组索引:用状态和事件作为二维数组的下标,直接定位到对应的表项。查找时间固定为 O(1)。
- 哈希表:如果状态和事件的数量都很大,可以用哈希表加速查找。
二维数组的方式在嵌入式里更常见:
#define MAX_STATE 4
#define MAX_EVENT 4
StateTableEntry state_matrix[MAX_STATE][MAX_EVENT];
void state_machine_init(void) {
// 初始化所有表项为无效
for (int s = 0; s < MAX_STATE; s++) {
for (int e = 0; e < MAX_EVENT; e++) {
state_matrix[s][e].action = NULL;
state_matrix[s][e].next_state = s; // 默认不切换
}
}
// 设置有效转换
state_matrix[STATE_IDLE][EVENT_START] = (StateTableEntry){
.action = action_start,
.next_state = STATE_RUNNING
};
// ... 其他设置
}
void state_machine_run(Event event) {
StateTableEntry *entry = &state_matrix[current_state][event];
if (entry->action) {
entry->action();
current_state = entry->next_state;
}
}
这种方式查找速度快,但内存占用固定。如果状态和事件数量不多,很实用。
8.3 状态模式在嵌入式中的典型应用
我在项目中用过状态模式的地方不少。举几个例子:
- 按键扫描:空闲、消抖、按下、长按、释放,每个状态处理不同的按键逻辑。
- 通信协议:比如 I2C 的起始、地址、数据、停止状态,每个状态处理不同的字节。
- 电源管理:运行、空闲、休眠、深度休眠,不同状态下的功耗策略不同。
我记得有一次做电池供电的设备,电源管理状态机有七八个状态。用状态表驱动实现后,新增一个「低电量休眠」状态,只需要在表里加两行代码,测试也很快通过。
注意:状态表驱动虽然好,但也不是万能的。如果状态之间的转换逻辑非常复杂,比如同一个事件在不同条件下走向不同状态,那状态表就不好表达了。这时候可以考虑用状态模式 + 条件判断的组合方式。
8.4 状态模式的核心知识体系
下面这张图总结了状态模式的核心逻辑,包括三种实现方式、状态表的结构以及状态切换的完整流程。
8.5 避坑指南
最后分享几个我踩过的坑:
- 状态表不要太大:如果状态和事件都超过 20 个,表会变得很庞大。这时候考虑分层状态机,把大状态拆成小状态。
- 注意空指针:动作函数指针可能为 NULL,调用前一定要判断。我见过因为忘记判断导致跑飞的案例。
- 状态切换的原子性:如果在中断里执行状态切换,要注意保护临界区。否则可能切换一半被打断,状态不一致。
- 默认处理:状态表里找不到匹配项时,要有默认处理逻辑。可以记录日志,或者保持当前状态不变。
嗯,状态模式就聊到这儿。记住一句话:状态表驱动不是银弹,但对付大部分嵌入式状态机场景,它足够优雅且可靠。
公众号:蓝海资料掘金营,微信deep3321