一句话: 状态机能用枚举+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 --> [*]

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

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

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

注意两个关键点:

  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;

主循环变成这样:

展开查看完整代码(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 状态图,小白看着就劝退。

其实核心就三句话:

  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 里拎出来,变成一个清晰的状态变量。写起来多几行代码,调试起来省半天时间。


实测对比:if-else 嵌套方案: 加一个新状态要改 6 个地方 | 状态机方案: 加一个 case,其他代码不动

有用的话点个收藏,下次调试直接用。有问题欢迎评论区交流,看到了都会回。

下一篇170条实战总结——状态机、编码规范、编译链接——每个嵌入式新人都要过的三道坎

更多推荐