12. 事件驱动状态机:状态机基础、事件驱动状态转换、状态机实现模式
状态机这东西,说白了就是让程序“记住自己干到哪了”。
我刚开始做嵌入式那会儿,总觉得状态机是教科书里的概念,实际写代码用if-else堆就完事了。直到有一次做一个多按键的菜单系统,if-else嵌套了七八层,改一个按键逻辑要翻半天代码……嗯,从那以后,我老老实实把状态机捡起来了。
12.1 状态机基础:三个核心要素
一个状态机,不管多复杂,核心就三样东西:状态、事件、动作。
- 状态(State):系统当前所处的“模式”。比如一个按键,有“按下”、“释放”、“长按”三种状态。
- 事件(Event):触发状态变化的东西。比如“定时器超时”、“引脚电平变化”、“消息到达”。
- 动作(Action):状态转换时执行的操作。比如“点亮LED”、“发送数据”、“启动定时器”。
我个人习惯把状态机画成一张表,比看代码直观得多。下面这张图就是最经典的状态转换模型:
你看,状态机其实就是一个“有限状态 + 事件驱动”的循环。每个状态只关心它能处理的事件,其他事件要么忽略,要么报错。这种“有限”恰恰是它可靠的原因。
12.2 事件驱动状态转换:谁触发了谁?
事件驱动,说白了就是“有事才动,没事歇着”。这和轮询完全不同——轮询是“不管有事没事,我都去看看”。
我在项目中遇到过一个问题:一个传感器采集模块,用轮询方式每10ms检查一次数据是否就绪。结果CPU占用率居高不下,低优先级任务饿死了。后来改成事件驱动——数据就绪时触发一个中断,状态机收到事件才处理。CPU占用率直接降了60%。
事件驱动状态转换的典型流程:
- 事件产生:硬件中断、定时器超时、消息队列投递等。
- 事件分发:将事件投递到对应状态机的事件队列。
- 状态查询:状态机从当前状态的事件表中查找该事件对应的处理函数。
- 动作执行:执行处理函数,可能包含状态转换。
- 状态更新:如果动作中指定了新状态,则更新当前状态。
关键点:事件驱动状态机中,状态转换是原子操作。也就是说,一个事件处理过程中,不会插入另一个事件的处理。这避免了竞态条件。
12.3 状态机实现模式:三种主流写法
实际写代码时,状态机实现方式有好几种。我按自己的经验,从简单到复杂给你捋一遍。
12.3.1 switch-case 模式(最直观)
这是入门级写法,适合状态少于10个的简单场景。
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_ERROR
} state_t;
state_t current_state = STATE_IDLE;
void state_machine_run(event_t event) {
switch (current_state) {
case STATE_IDLE:
if (event == EVT_START) {
motor_start();
current_state = STATE_RUNNING;
}
break;
case STATE_RUNNING:
if (event == EVT_STOP) {
motor_stop();
current_state = STATE_IDLE;
} else if (event == EVT_FAULT) {
motor_emergency_stop();
current_state = STATE_ERROR;
}
break;
case STATE_ERROR:
if (event == EVT_RESET) {
motor_reset();
current_state = STATE_IDLE;
}
break;
default:
break;
}
}
这种写法好处是简单,一眼能看懂。坏处是——状态多了以后,一个switch-case能写几百行,维护起来想哭。我曾经维护过一个项目,一个switch-case函数有800多行,改一个状态要翻半天。
12.3.2 状态表模式(推荐)
把状态和事件的对应关系抽成一张表。这是我最常用的模式,可读性和可维护性都很好。
typedef struct {
state_t current_state;
event_t event;
state_t next_state;
void (*action)(void);
} state_transition_t;
// 状态转换表
static const state_transition_t trans_table[] = {
{STATE_IDLE, EVT_START, STATE_RUNNING, motor_start},
{STATE_RUNNING, EVT_STOP, STATE_IDLE, motor_stop},
{STATE_RUNNING, EVT_FAULT, STATE_ERROR, motor_emergency_stop},
{STATE_ERROR, EVT_RESET, STATE_IDLE, motor_reset},
// 终止标记
{STATE_MAX, EVT_MAX, STATE_MAX, NULL}
};
void state_machine_run(event_t event) {
const state_transition_t *p = trans_table;
while (p->action != NULL) {
if (p->current_state == current_state && p->event == event) {
p->action(); // 执行动作
current_state = p->next_state; // 状态转换
return;
}
p++;
}
// 未找到匹配项,忽略事件或报错
}
这种模式的好处:
- 状态转换逻辑集中在一张表里,改一个状态只需改表的一行。
- 新增状态或事件,不会影响已有逻辑。
- 甚至可以做成配置化,从外部文件加载。
我的习惯:状态转换表用const修饰,放在Flash里。嵌入式MCU的RAM宝贵,能省则省。另外,表末尾加一个终止标记,防止越界。
12.3.3 函数指针模式(最灵活)
每个状态对应一个处理函数,状态转换就是切换函数指针。这种模式适合状态行为差异很大的场景。
typedef void (*state_handler_t)(event_t event);
void state_idle_handler(event_t event);
void state_running_handler(event_t event);
void state_error_handler(event_t event);
static state_handler_t current_handler = state_idle_handler;
void state_machine_run(event_t event) {
current_handler(event);
}
void state_idle_handler(event_t event) {
switch (event) {
case EVT_START:
motor_start();
current_handler = state_running_handler;
break;
default:
break;
}
}
void state_running_handler(event_t event) {
switch (event) {
case EVT_STOP:
motor_stop();
current_handler = state_idle_handler;
break;
case EVT_FAULT:
motor_emergency_stop();
current_handler = state_error_handler;
break;
default:
break;
}
}
这种模式的好处是每个状态的处理逻辑完全独立,可以单独测试。坏处是状态转换分散在各个函数里,全局视图不如状态表清晰。
注意:函数指针模式中,一定要小心野指针。我曾经在项目中遇到一个bug,状态处理函数指针被意外覆盖,结果程序跑飞了。排查了两天才找到原因。从那以后,我每次切换函数指针前都会加一个断言检查。
12.4 状态机设计中的常见陷阱
做状态机这么多年,踩过的坑不少。挑几个典型的说说:
- 状态爆炸:状态太多,导致转换表庞大。解决办法是引入层次状态机(HSM),把公共行为提取到父状态。
- 事件丢失:事件处理过程中,新事件到达被覆盖。解决办法是用队列缓存事件,保证每个事件都被处理。
- 状态转换死循环:两个状态互相触发转换,CPU卡死。解决办法是加一个转换计数器,超过阈值就报错。
- 动作执行时间过长:在状态机里做延时操作,阻塞了事件处理。解决办法是把耗时操作放到任务里异步执行。
嗯,说到这个,我想起一个真实案例。有个同事做智能家居网关,状态机里直接调用了WiFi连接函数,结果连接超时阻塞了5秒,期间所有按键事件全部丢失。用户按了开关,灯要等5秒才反应……后来改成异步连接,状态机只负责发起连接和接收连接结果事件,问题就解决了。
12.5 状态机与事件队列的配合
事件驱动状态机,光有状态机还不够,还得有个事件队列。事件队列负责“缓冲”事件,状态机负责“消费”事件。两者配合,才能构建一个健壮的系统。
典型的配合模式:
// 事件队列
event_t event_queue[QUEUE_SIZE];
int head = 0, tail = 0;
// 投递事件(通常由中断或任务调用)
void post_event(event_t event) {
int next = (tail + 1) % QUEUE_SIZE;
if (next != head) {
event_queue[tail] = event;
tail = next;
}
// 队列满时,可丢弃或覆盖旧事件
}
// 主循环或任务中处理事件
void event_loop(void) {
while (1) {
if (head != tail) {
event_t event = event_queue[head];
head = (head + 1) % QUEUE_SIZE;
state_machine_run(event);
}
// 无事件时,可进入低功耗模式
}
}
这种设计的好处是:中断或任务只管投递事件,不用关心状态机当前在干什么。状态机只管处理事件,不用关心事件从哪里来。解耦得干干净净。
总结一下:事件驱动状态机 = 事件队列(缓冲) + 状态表(逻辑) + 动作函数(执行)。三个部分各司其职,组合起来就是一个可靠、可维护的嵌入式系统核心。
好了,状态机的基础和实现模式就聊到这儿。这东西看着简单,但用好了,能让你的代码结构清晰一个数量级。下次写复杂逻辑时,不妨先画个状态图,再动手写代码——你会发现,思路顺了,bug也少了。