第三章:嵌入式状态机设计模式:查表法、嵌套switch-case法、状态模式
大家好,我是你们的老朋友。今天我们来聊聊状态机的三种经典实现方式。说实话,我在嵌入式这行摸爬滚打十几年,这三种方法都反复用过。每种方法都有它的脾气,选对了事半功倍,选错了...嗯,后面调试的时候你会想哭的。
3.1 嵌套switch-case法:最直观的写法
先说说最基础的方法。你想想看,刚学编程的时候,谁没用过switch-case?把状态和事件都塞进去,简单粗暴。
// 嵌套switch-case示例
void process_event(State_t current_state, Event_t event) {
switch(current_state) {
case STATE_IDLE:
switch(event) {
case EVENT_START:
// 进入测量状态
current_state = STATE_MEASURING;
break;
case EVENT_STOP:
// 保持空闲
break;
default:
break;
}
break;
case STATE_MEASURING:
switch(event) {
case EVENT_TIMEOUT:
// 测量完成
current_state = STATE_COMPLETE;
break;
case EVENT_ERROR:
current_state = STATE_ERROR;
break;
default:
break;
}
break;
// ... 更多状态
}
}
我个人习惯在小项目里用这种方法。比如一个简单的按键消抖,状态不超过5个,事件不超过3个。为什么?因为代码量少,一眼就能看明白。
优点:
- 代码直观,新手也能看懂
- 不需要额外数据结构
- 调试方便,单步执行就能跟踪
缺点:
- 状态多了以后,代码膨胀得厉害
- 状态和事件耦合太紧,改一个地方可能影响一片
- 可维护性差,20个状态以上基本没法看
避坑指南:我曾经在一个项目里用嵌套switch-case写了30多个状态,结果客户要加一个新功能,我改了三天,改完又测了两天,最后还是出了bug。从那以后,超过10个状态的项目,我坚决不用这种方法。
3.2 查表法:用空间换时间
接下来聊聊查表法。说白了,就是把状态转移关系存到一张表里。运行时查表决定下一步怎么走。
// 查表法示例
typedef struct {
State_t current_state;
Event_t event;
State_t next_state;
Action_t action;
} Transition_t;
// 状态转移表
const Transition_t state_table[] = {
{STATE_IDLE, EVENT_START, STATE_MEASURING, action_start_measure},
{STATE_MEASURING, EVENT_TIMEOUT, STATE_COMPLETE, action_save_result},
{STATE_MEASURING, EVENT_ERROR, STATE_ERROR, action_handle_error},
{STATE_COMPLETE, EVENT_RESET, STATE_IDLE, action_reset_device},
// ... 更多转移
};
State_t process_event(State_t current, Event_t event) {
for(int i = 0; i < TABLE_SIZE; i++) {
if(state_table[i].current_state == current &&
state_table[i].event == event) {
// 执行动作
state_table[i].action();
// 返回下一个状态
return state_table[i].next_state;
}
}
return current; // 没有匹配的转移
}
这个方法我特别喜欢。为什么?因为状态和事件的关系一目了然。你想想看,要改一个转移,只需要改表里的一行数据,不用翻来覆去找代码。
我的经验:在体温计项目里,我用了查表法。体温计有8个状态:待机、测量中、完成、错误、低电量、校准、关机、休眠。事件有6个。用查表法,整个状态机只有48行表数据,加上处理函数,总共不到200行代码。后来客户要加一个「温度过高报警」状态,我只花了10分钟就加好了。
| 对比项 | 嵌套switch-case | 查表法 |
|---|---|---|
| 代码量(10个状态) | 约300行 | 约100行 |
| 修改难度 | 高 | 低 |
| 运行速度 | 快(直接跳转) | 中等(查表循环) |
| 内存占用 | 低 | 较高(需要存储表) |
| 适合场景 | 小规模(<10状态) | 中规模(10-50状态) |
3.3 状态模式:面向对象的优雅方案
最后说说状态模式。这个方法在C++里用得比较多,但C语言也能实现。核心思想是:每个状态都是一个独立的对象,有自己的处理函数。
// 状态模式示例(C语言实现)
typedef struct StateMachine StateMachine_t;
// 状态接口
typedef struct {
void (*enter)(StateMachine_t *sm);
void (*exit)(StateMachine_t *sm);
State_t (*handle_event)(StateMachine_t *sm, Event_t event);
} State_t;
// 具体状态
void idle_enter(StateMachine_t *sm) {
// 进入空闲状态
turn_off_display();
}
State_t idle_handle_event(StateMachine_t *sm, Event_t event) {
switch(event) {
case EVENT_START:
return &measuring_state; // 切换到测量状态
default:
return &idle_state; // 保持当前状态
}
}
// 状态机主体
typedef struct {
State_t *current_state;
// 其他数据...
} StateMachine_t;
void state_machine_process(StateMachine_t *sm, Event_t event) {
State_t *next = sm->current_state->handle_event(sm, event);
if(next != sm->current_state) {
sm->current_state->exit(sm);
sm->current_state = next;
sm->current_state->enter(sm);
}
}
这个方法的好处是:每个状态都是独立的,改一个状态不会影响其他状态。我在一个复杂的医疗设备项目里用过,那个设备有50多个状态,用状态模式管理得井井有条。
什么时候用状态模式?
- 状态数量多(20个以上)
- 状态之间差异大,每个状态的行为完全不同
- 需要频繁增删状态
- 团队协作开发,每个人负责不同的状态
注意:状态模式在资源受限的MCU上要慎用。每个状态都需要一个结构体,函数指针也会占用额外的RAM。我曾经在一个只有2KB RAM的芯片上试过,结果内存不够用,最后只能换回查表法。
3.4 如何选择?我的建议
说了这么多,到底该用哪种?我给大家一个简单的判断标准:
- 状态少于5个:用嵌套switch-case,简单直接
- 状态5-20个:用查表法,好维护
- 状态超过20个:用状态模式,或者查表法+状态模式混合
其实没有绝对的好坏。我见过有人用查表法写了1000多个状态,也见过有人用switch-case管理50个状态。关键是要适合你的项目。
最后说一句:不管你选哪种方法,一定要画状态图。我每次做状态机,第一件事就是画图。画完图再写代码,思路清晰,bug也少。这个习惯我保持了十几年,从来没出过大的状态机问题。
好了,这一章就到这里。下一章我们聊聊状态机的测试方法,这可是个大学问。到时候见!