嵌入式状态机怎么写?用“洗衣机“讲清楚(附代码模板)

嵌入式状态机怎么写?用"洗衣机"讲清楚(附代码模板)

一句话: 状态机能用枚举+switch-case把散乱的if-else改造成清晰的状态流转。本文用洗衣机做比喻,5分钟看懂,附可直接复用的代码模板。

单片机程序写着写着就容易乱——标志位满天飞,if-else 嵌套五六层,过两周自己都看不懂。

状态机就是来解决这个问题的。

状态机是什么?想想洗衣机

你家的全自动洗衣机,按"启动"之后是这样的:

进水 → 洗涤 → 排水 → 脱水 → 漂洗 → 排水 → 脱水 → 结束

注意两个关键点:

  1. 洗衣机一次只在一个状态——进水的时候绝不可能同时脱水
  2. 状态切换有条件——水满了才能开始洗,水排干了才能开始脱水

这就是状态机。单片机程序也是一样的道理。

没有状态机的时候,代码长什么样

void main_loop(void) { if (step == 0) { // 进水 if (water_level > FULL) { step = 1; } } else if (step == 1) { // 洗涤 if (timer > 600) { step = 2; } } else if (step == 2) { // 排水 if (water_level == 0) { step = 3; } } else if (step == 3) { // 脱水 if (timer > 300) { step = 4; } } // ... 再写 20 个 else if }

这代码能跑吗?能。好维护吗?你试试三个月后改一步逻辑——牵一发动全身。

用状态机重写

先定义状态:

typedef enum { STATE_INIT, // 初始化 STATE_IDLE, // 空闲 STATE_RUNNING, // 运行中 STATE_ERROR, // 故障 STATE_SHUTDOWN // 关机 } E_SYS_STATE;

主循环变成这样:

E_SYS_STATE g_eState = STATE_INIT; void main_loop(void) { switch (g_eState) { case STATE_INIT: init_peripherals(); g_eState = STATE_IDLE; break; case STATE_IDLE: if (start_cmd_received) { g_eState = STATE_RUNNING; } break; case STATE_RUNNING: run_task(); if (fault_detected) { g_eState = STATE_ERROR; } if (stop_cmd_received) { g_eState = STATE_SHUTDOWN; } break; case STATE_ERROR: handle_error(); if (error_cleared) { g_eState = STATE_IDLE; } break; case STATE_SHUTDOWN: power_down(); break; default: g_eState = STATE_ERROR; // 兜底:跑到不该来的地方,进故障 break; } }

对比一下:

没有状态机有状态机
当前在干什么step变量猜g_eState一目了然
加一个新状态改一堆 if-else加一个 case 就行
出 bug 了不知道从哪跳进来的状态切换只有一处,直接定位
三个月后维护想死还能看懂

状态切换集中管理

实际项目里,状态切换最好统一在一个地方做:

void set_state(E_SYS_STATE eNewState) { if (eNewState == g_eState) { return; // 没变化就不折腾 } // 离开旧状态前做清理 switch (g_eState) { case STATE_RUNNING: stop_all_outputs(); // 安全操作:离开运行态先关输出 break; default: break; } g_eState = eNewState; // ★ 只有这一处赋值 // 进入新状态时做初始化 switch (eNewState) { case STATE_RUNNING: reset_timers(); break; default: break; } }

好处:整个程序里状态切换只有set_state()一个入口。出 bug 的时候在这加个printf,你马上知道是谁、什么时候、切到了什么状态。

状态机 ≠ 一定要画图

很多教程一上来就甩 UML 状态图,小白看着就劝退。

其实核心就三句话:

  1. 用枚举列出所有状态——命名要能看懂,别用 0 1 2 3
  2. switch-case 处理每个状态——default 必须写,兜底
  3. 状态切换统一入口——一个函数搞定,方便调试

画不画图是次要的,先把代码写清楚。

什么时候该用状态机

场景要不要用
单个 LED 闪烁没必要,计时器+标志位够了
设备上电→自检→待机→运行→关机必须用
通信协议解析(等数据→校验→处理→应答)必须用
传感器读值+显示随便,状态不超过 3 个就算了

判断标准很简单:如果你的流程超过 3 步,或者经常在"等某个条件"和"做某件事"之间切换,用状态机。

一个容易犯的错:在状态里死等

// 错误写法:在 case 里 while 死等 case STATE_WAIT_READY: while (!device_ready()); // ← 卡死了!其他事全干不了 g_eState = STATE_NEXT; break; // 正确写法:条件不满足就走,下次循环再检查 case STATE_WAIT_READY: if (device_ready()) { g_eState = STATE_NEXT; } // 不满足就保持当前状态,主循环继续跑 break;

状态机是非阻塞的。每个状态进来,检查条件,能干就干然后切走,不能干就退出等下次循环。整个系统靠主循环的高速轮转来驱动,而不是在某个状态里死等。

总结

要点一句话
核心思想一次只在一个状态,切换有条件
实现方式枚举 + switch-case + 统一切换函数
最大禁忌在状态里 while 死等
什么时候用流程超过 3 步就用

状态机不是什么高深的玩意。说白了就是把"程序跑到哪了"这件事从一堆散乱的 if-else 里拎出来,变成一个清晰的状态变量。写起来多几行代码,调试起来省半天时间。