嵌入式状态机怎么写?用“洗衣机“讲清楚
一句话: 状态机能用枚举+switch-case把散乱的if-else改造成清晰的状态流转。本文用洗衣机做比喻,5分钟看懂,附可直接复用的代码模板。
系列文章:《嵌入式编程基础》 — 状态机、编码规范、编译链接——每个嵌入式新人都要过的三道坎
✅ 保姆级教程 📄 170条实战总结 📄 3分钟看懂Keil编译输出 📄 一条浮点指令差几十倍 📄 为什么两个都要 📄 复制粘贴超过2次就该停 📄 两个变量搞定开/关/空闲 📄 先画条件矩阵再取名 📄 dead-code-chain-deletion 📄 macro-derive-coefficients
单片机程序写着写着就容易乱——标志位满天飞,if-else 嵌套五六层,过两周自己都看不懂。
状态机就是来解决这个问题的。
@[TOC]
适合谁读:适合嵌入式开发者、单片机初学者及遇到类似问题的工程师
一张图看懂状态机流转
stateDiagram-v2
[*] --> INIT: 上电
INIT --> IDLE: 初始化完成
IDLE --> RUNNING: 收到启动命令
RUNNING --> ERROR: 检测到故障
ERROR --> IDLE: 故障清除
RUNNING --> SHUTDOWN: 收到停机命令
SHUTDOWN --> [*]
状态机是什么?想想洗衣机
你家的全自动洗衣机,按"启动"之后是这样的:
进水 → 洗涤 → 排水 → 脱水 → 漂洗 → 排水 → 脱水 → 结束
注意两个关键点:
- 洗衣机一次只在一个状态——进水的时候绝不可能同时脱水
- 状态切换有条件——水满了才能开始洗,水排干了才能开始脱水
这就是状态机。单片机程序也是一样的道理。
没有状态机的时候,代码长什么样
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;
主循环变成这样:
展开查看完整代码(42行)
展开查看完整代码(42行)
展开查看完整代码(42行)
展开查看完整代码(42行)
展开查看完整代码(42行)
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 了 | 不知道从哪跳进来的 | 状态切换只有一处,直接定位 |
| 三个月后维护 | 想死 | 还能看懂 |
状态切换集中管理
实际项目里,状态切换最好统一在一个地方做:
展开查看完整代码(26行)
展开查看完整代码(26行)
展开查看完整代码(26行)
展开查看完整代码(26行)
展开查看完整代码(26行)
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 状态图,小白看着就劝退。
其实核心就三句话:
- 用枚举列出所有状态——命名要能看懂,别用 0 1 2 3
- switch-case 处理每个状态——default 必须写,兜底
- 状态切换统一入口——一个函数搞定,方便调试
画不画图是次要的,先把代码写清楚。
什么时候该用状态机
| 场景 | 要不要用 |
|---|---|
| 单个 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 里拎出来,变成一个清晰的状态变量。写起来多几行代码,调试起来省半天时间。
实测对比:if-else 嵌套方案: 加一个新状态要改 6 个地方 | 状态机方案: 加一个 case,其他代码不动
有用的话点个收藏,下次调试直接用。有问题欢迎评论区交流,看到了都会回。
下一篇:170条实战总结——状态机、编码规范、编译链接——每个嵌入式新人都要过的三道坎
更多推荐

所有评论(0)