乐于分享
好东西不私藏

如何在嵌入式软件中优雅地设计状态机?

如何在嵌入式软件中优雅地设计状态机?

在嵌入式开发中,"状态机"几乎无处不在:

  • 按键处理
  • 通信协议
  • 电机控制
  • UI菜单
  • OTA升级
  • …………

很多工程师都“会用状态机”,但真正能把状态机写得优雅、可维护、可扩展的人并不多。

现实中的大量代码往往是这样的:

if(state == IDLE){if(key)    {        state = START;    }}elseif(state == START){if(timeout)    {        state = RUN;    }}elseif(state == RUN){    ...}

项目初期可能还能忍。

一年半载后:

  • 状态越来越多;
  • 转移关系越来越复杂;
  • flag 满天飞;
  • 新人根本不敢改。

最后演变成:"这个状态千万别动,动了会死机。"

这篇文章,我们就聊聊:如何在嵌入式软件中优雅地设计状态机?

一、状态机的本质是什么?

状态机(FSM,Finite State Machine)本质上是:一种基于事件驱动,在有限状态间进行有序跳转的逻辑控制模型

核心组成只有三个:

元素
含义
State
当前状态
Event
外部事件
Transition
状态转移

例如:

[IDLE]		// 状态   | key press	// 事件   v[RUNNING]	// 状态   | timeout	// 事件   v[STOP]		// 状态

状态机最大的价值:将"行为"与"状态"绑定。

而不是:散落各处的 flag + if 判断。

二、为什么很多状态机越写越烂?

因为很多代码只是:

"用了 enum"

却没有真正进行状态建模。

典型问题:

1. 状态与行为耦合混乱

例如:

if(state == RUN){    Motor_Start();if(error)    {        Alarm();        state = ERROR;    }}

问题:

  • 行为代码散落
  • 状态转移不集中
  • 无法看出整体流程

2. 转移关系不可见

项目大了以后:

RUN -> STOPRUN -> ERRORRUN -> SLEEPRUN -> UPDATE

转移逻辑散落在几十个文件。

你根本不知道:

"哪些状态能跳到哪里"

这就是维护灾难。

3. 状态与事件耦合不清晰

大量代码:

if(rx_ok)if(tx_ok)if(timeout)if(retry)if(key)

问题:

  • 事件来源不统一;
  • 没有事件抽象;
  • 状态机退化为 flag 判断器。

三、优雅状态机的核心思想

真正优秀的状态机设计,核心原则只有一句:

"状态机只处理事件"

不是轮询 flag。

不是直接操作硬件。

不是写业务流程。

而是:

事件驱动状态转移

即:

Event -> FSM -> Action -> New State

四、推荐架构:事件驱动状态机

1. 定义状态

typedefenum{    STATE_IDLE,    STATE_START,    STATE_RUN,    STATE_ERROR,}state_t;

2. 定义事件

typedefenum{    EVENT_KEY,    EVENT_TIMEOUT,    EVENT_ERROR,    EVENT_STOP,}event_t;

注意:

"事件"是状态机输入。

这一步非常关键。

很多项目根本没有 事件抽象。

3. 状态机上下文

// 例如通信处理状态机typedefstruct{state_t state;   // 当前状态uint32_t retry_cnt;  // 重试次数uint32_t timeout;  // 超时计时}fsm_t;

这里保存:

  • 当前状态;
  • 上下文数据;
  • 运行参数。

而不是使用全局变量。

4. 核心状态机函数

voidFSM_Dispatch(fsm_t *fsm, event_t event){switch(fsm->state)    {case STATE_IDLE:if(event == EVENT_KEY)            {                Motor_Start();                fsm->state = STATE_START;            }break;case STATE_START:if(event == EVENT_TIMEOUT)            {                fsm->state = STATE_RUN;            }break;case STATE_RUN:if(event == EVENT_ERROR)            {                Motor_Stop();                fsm->state = STATE_ERROR;            }break;default:break;    }}

这时候:

  • 状态转移集中;
  • 逻辑清晰;
  • 事件统一;
  • 容易调试。

五、更优雅的做法:状态表驱动

switch-case 状态机在大型项目中会膨胀。更进一步的做法是——表驱动状态机

核心思想是将“状态-事件-下一状态-动作”的映射关系抽离成一张常驻 ROM 的表格,状态机引擎只需按表索骥。这种设计将逻辑流程解耦,修改或增加转换关系只需修改表格数据,无需改动引擎代码,极大降低错误引入的可能。

我们定义四元组结构体:

typedefstruct {    State_t     curState;  // 当前状态    Event_t     event;   // 事件    State_t     nextState;  // 下一状态void        (*action)(void);// 动作} FsmTransition_t;

其中 State_t 与 Event_t 为枚举类型,action 是执行转换时需要调用的动作函数指针。

下面以一个手持设备的电源管理为例,展示表驱动状态机的完整应用。

状态包括:关机(OFF)、待机(STANDBY)、运行(ACTIVE)、充电中(CHARGING)、电池低电(LOW_BATT)。事件由按键、充电器插入、电压监测模块产生。

1. 定义状态与事件

typedefenum {    STATE_OFF,    STATE_STANDBY,    STATE_ACTIVE,    STATE_CHARGING,    STATE_LOW_BATT,    STATE_NULL       // 哨兵值,不得用作有效状态} State_t;typedefenum {    EV_KEY_PRESS,    EV_CHARGER_PLUG,    EV_CHARGER_UNPLUG,    EV_BATT_LOW,    EV_BATT_OK,    EV_TIMEOUT,    EV_NULL} Event_t;

2. 动作函数原型

staticvoidTurnOnDisplay(void);staticvoidTurnOffDisplay(void);staticvoidEnterLowPower(void);staticvoidNormalOperation(void);staticvoidTurnOnChargeLED(void);// 实现略,针对具体硬件操作

3. 转换表

staticconst FsmTransition_t pmFsmTable[] = {// 关机状态    {STATE_OFF,      EV_KEY_PRESS,      STATE_STANDBY,  TurnOnDisplay},    {STATE_OFF,      EV_CHARGER_PLUG,   STATE_CHARGING, TurnOnChargeLED},// 待机状态    {STATE_STANDBY,  EV_KEY_PRESS,      STATE_ACTIVE,   NormalOperation},    {STATE_STANDBY,  EV_TIMEOUT,        STATE_OFF,      TurnOffDisplay},    {STATE_STANDBY,  EV_CHARGER_PLUG,   STATE_CHARGING, TurnOnChargeLED},    {STATE_STANDBY,  EV_BATT_LOW,       STATE_LOW_BATT, EnterLowPower},// 运行状态    {STATE_ACTIVE,   EV_TIMEOUT,        STATE_STANDBY,  TurnOffDisplay},    {STATE_ACTIVE,   EV_BATT_LOW,       STATE_LOW_BATT, EnterLowPower},    {STATE_ACTIVE,   EV_CHARGER_PLUG,   STATE_CHARGING, NULL},// 充电状态    {STATE_CHARGING, EV_CHARGER_UNPLUG, STATE_STANDBY,  NormalOperation},    {STATE_CHARGING, EV_BATT_OK,        STATE_ACTIVE,   NULL},  // 低电状态    {STATE_LOW_BATT, EV_CHARGER_PLUG,   STATE_CHARGING, TurnOnChargeLED},    {STATE_LOW_BATT, EV_BATT_OK,        STATE_STANDBY,  NormalOperation},// 结束标记    {STATE_NULL,     EV_NULL,           STATE_NULL,     NULL}};

阅读这张表即可把握系统全貌——没有任何一行 if-else 嵌套。

4. 集成到主程序

// 状态机实例结构体(支持多实例)typedefstruct {    State_t currentState;// ...} Fsm_t;// 初始化状态机voidFsm_Init(Fsm_t *fsm, State_t initState){    fsm->currentState = initState;// 可在此处执行初始状态的 EntryAction}// 事件分发处理voidFsm_Dispatch(Fsm_t *fsm, Event_t event){    State_t curState = fsm->currentState;const FsmTransition_t *pTrans = fsmTable;for (; pTrans->curState != STATE_NULL; pTrans++) {// 匹配当前状态和事件if ((pTrans->curState == curState) && (pTrans->event == event)) {//  执行转移动作if (pTrans->action) {                pTrans->action();            }// 状态转移            fsm->currentState = pTrans->nextState;return// 一次只处理一个转移,完成后立即返回        }    }// 未找到匹配转换,可在此增加默认处理或日志记录}intmain(void){    Fsm_t myFsm; // 创建状态机实例    SysInit();    Fsm_Init(&myFsm, STATE_OFF); // 初始化状态机while(1) {        Event_t event = GetEvent();  // 非阻塞获取事件if (event != EV_NULL) {            Fsm_Dispatch(&myFsm, event); // 传入实例指针和事件        }        IdleProcess();  // 其它低优先级任务    }}

GetEvent() 是一个轮询函数,检查按键 GPIO、充电检测引脚或电量 ADC 阈值标志,并返回对应事件码。获取事件后必须清除底层事件标志,否则同一事件会被反复分发。

六、为什么表驱动状态机如此优秀?

1. 状态转移关系可视化

你一眼就能看懂:

当前状态 -> 事件 -> 新状态

这在协议栈中极其重要。

例如:

  • TCP
  • BLE
  • Modbus
  • MQTT

全是状态驱动。

2. 易于扩展

新增状态:

增加一行表项即可

而不是改几十个 if-else。

3. 更适合自动生成

很多工业协议:

  • UML
  • AUTOSAR

最终都会生成:

状态转移表

因为这是最标准的数据表达。

4. 更容易做调试

在 Fsm_Dispatch 入口添加日志输出(建议通过条件编译开关控制):

printf("[%d] + [%d] -> [%d]\n",       cur_state,       event,       next_state);

立刻能看到:

状态转移轨迹

调试体验提升巨大。

七、状态机的进阶概念:Entry / Exit / 守卫条件

很多人状态机只会:

状态转移

但真正成熟的状态机还包含:

行为
含义
Entry
进入状态时执行(类比C++类的构造函数)
Exit
离开状态时执行(类比C++类的析构函数)
Guard
守卫条件,为true时才允许转移

例如:

IDLE --key--> RUN

实际过程:

if(Guard()==TRUE){    Exit(IDLE)    Action(key)    Entry(RUN)}

这非常重要。

进阶后的状态转移表结构:

// 扩展转移表结构typedefstruct {    State_t curState;    Event_t event;bool (*guard)(void);        // 守卫条件函数指针,返回true才允许转移void (*action)(void);       // 转移时执行的动作    State_t nextState;void (*exitAction)(void);   // 退出当前状态动作void (*entryAction)(void);  // 进入下一状态动作} FsmTransition_t;

示例

// 转移表定义FsmTransition_t fsmTable[] = {// 当前状态,     事件,         守卫条件,           动作,       下一状态,      退出动作, 进入动作// ………………    {STATE_NULL,  EV_NULL,     NULL,               NULL,      STATE_NULL,    NULL,    NULL// 结束标志};// 事件分发处理voidFsm_Dispatch(Fsm_t *fsm, Event_t event){    State_t curState = fsm->currentState;const FsmTransition_t *pTrans = fsmTable;for (; pTrans->curState != STATE_NULL; pTrans++) {// 匹配当前状态和事件if ((pTrans->curState == curState) && (pTrans->event == event)) {// 检查守卫条件(如果有的话)if (pTrans->guard != NULL && !pTrans->guard()) {continue// 守卫条件不满足,继续查找下一条可能匹配的规则            }// 1. 执行退出动作if (pTrans->exitAction) {                pTrans->exitAction();            }// 2. 执行转移动作if (pTrans->action) {                pTrans->action();            }// 3. 状态转移            fsm->currentState = pTrans->nextState;// 4. 执行进入动作if (pTrans->entryAction) {                pTrans->entryAction();            }return// 一次只处理一个转移,完成后立即返回        }    }// 未找到匹配转换,可在此增加默认处理或日志记录}

好处:

  • 行为与状态绑定;
  • 避免遗漏初始化;
  • 生命周期更清晰。

八、不要滥用状态机

这是非常重要的一点。

很多工程师:

任何逻辑都想抽象成状态机

最后:

  • 状态爆炸
  • 转移混乱
  • 可读性下降

一个经验原则

适合状态机的场景:

1. 存在明确阶段

例如:

INITCONNECTAUTHRUNERROR

2. 同一事件在不同阶段行为不同

例如:

按键:IDLE -> 开机RUN  -> 停机

3. 流程具有"生命周期"

例如:

  • OTA
  • 配网
  • 协议连接
  • UI菜单

不适合状态机的场景

例如:

简单数学计算纯数据处理一次性函数

不要为了"高级"而状态机化。

九、嵌入式状态机的最佳实践

1. 状态机不要直接操作硬件

错误:

FSM里直接 GPIO_WritePin()

正确:

FSM -> Action -> Driver

状态机应该:

"描述行为"

而不是:

"操作寄存器"

2. 状态机不要阻塞

错误:

while(!uart_ok);

这会破坏事件驱动。

正确做法:

发送请求等待 EVENT_UART_OK

3. 一个状态机只负责一个领域

不要:

通信 + UI + 电机 + OTA

全塞一个 FSM。

否则一定崩。

4. 使用消息队列输入事件

在 RTOS 中推荐:

ISR -> Queue -> FSM

而不是:

ISR直接修改状态

这样:

  • 解耦
  • 线程安全
  • 易调试

5. 中断安全

若事件在中断里产生,避免在 ISR 中直接调用 Fsm_Dispatch,否则可能与主循环形成重入,导致状态不一致。

推荐做法:中断生成事件,由主循环统一消费。

ISR:  Set_Event(EVENT_XXX);Main: if (flag) Fsm_Dispatch(EVENT_XXX);

若必须中断内处理,需使用临界区保护(关中断)保证原子性。

6. 状态变量封装

currentState 应设为模块级静态变量,禁止外部直接修改。所有状态变更必须通过 Fsm_Dispatch,保证状态转换的唯一入口。对外仅暴露只读接口:

State_t GetState(void)return fsm.currentState; }

7. 动作函数扩展

示例中动作函数无参数。若动作需要依据事件或附带数据执行,可定义带参数的函数指针:

typedefvoid(*action_func_t)(Event_t event, void *param);

此时只需修改结构体和所有动作函数原型,引擎调用改为 t->action(event, param) 即可,表格其余部分不受影响。

8. 未匹配转换的处理

未匹配的转换不应静默忽略。至少应:

  • 记录日志(便于调试)
  • 或调用默认错误处理

避免系统停滞于非法状态。

9. 内存与性能

表格存储在 ROM(Flash)中,引擎线性搜索。状态数量通常不超过几十个,性能完全满足多数嵌入式场景。

如需极致速度,可按当前状态建立"事件→转换"索引表(哈希思想),用空间换时间。类似思想可以参考我之前写过的文章RTOS中的“闪电调度”如何实现O(1)时间复杂度确定最高优先级任务?

十、一个优秀状态机的标准

真正好的状态机应该:

特性
是否具备
事件驱动
状态集中管理
转移关系清晰
无阻塞
易扩展
易日志跟踪
可视化
可测试

总结

很多人以为状态机只是:

switch-case

实际上:

状态机是一种"控制复杂度"的思想。

它的真正价值是:

  • 降低耦合;
  • 显式描述流程;
  • 约束系统行为;
  • 提高可维护性;
  • 提高可测试性。

以表格驱动的轻量级状态机,在嵌入式环境中做到了可读、可维护、可测试的平衡。它迫使开发者显式枚举所有合法转换,将散乱的业务逻辑纳入严谨的框架。结合适当的宏封装,代码甚至可以作为文档使用。

优秀嵌入式软件的本质:

不是"代码能跑"。

而是:

"复杂系统依然可控"。

当需求频繁变更,代码频繁迭代时,一个优雅的状态机架构会让你庆幸最初的设计选择。