1. 状态机基础:概念、三要素与嵌入式应用

各位同学,咱们今天正式开课。先聊点基础的——状态机。

说实话,我刚开始做嵌入式那会儿,代码写得那叫一个“随心所欲”。一个按键处理,if-else 能嵌套七八层,自己过两天再看都懵。后来带我的老工程师扔给我一句话:“小子,你这就是没有状态机的概念。” 从那以后,我才算真正入了门。

1.1 什么是状态机?

状态机,全称有限状态机(Finite State Machine, FSM)。说白了,就是一个系统在任意时刻只能处于“有限个”确定状态中的一个。它根据外部事件,从一个状态跳转到另一个状态。

你想想看,我们平时用的遥控器、智能手环、甚至电梯,本质上都是状态机。电梯不会同时“上升”和“下降”,它要么在上升状态,要么在下降状态,要么停在某层——这就是状态。

核心思想: 系统在“有限个”状态之间,根据“事件”驱动进行“转移”。这是嵌入式软件结构化的基石。

1.2 状态机三要素

状态机有三个核心要素,我习惯把它们叫做“三板斧”:状态、事件、转移。缺一个,这机器就转不起来。

要素 定义 嵌入式中的例子
状态 (State) 系统在某一时刻的稳定“模样”。状态是有限的、可枚举的。 LED 灯的 ON / OFF;UART 接收的 IDLE / RECV / DONE
事件 (Event) 触发状态转移的“导火索”。可以是外部输入、定时器超时、内部信号。 按键按下、串口收到一个字节、DMA 传输完成中断
转移 (Transition) 从一个状态到另一个状态的“路径”。通常包含:当前状态 → 事件 → 动作 → 下一状态。 ON 状态 + 按键事件 → 关灯动作 → 进入 OFF 状态

嗯,这里要注意:转移里经常还带一个“动作”(Action)。动作是转移发生时执行的操作,比如点亮LED、启动定时器、发送数据包。动作不是必须的,但大部分场景下都有。

我的经验: 设计状态机时,先把“状态”列全,再想“事件”,最后画“转移”。顺序别搞反了,否则容易漏状态。

1.3 状态机在嵌入式中的应用场景

状态机在嵌入式里,那真是无处不在。我做过一个车载仪表盘的项目,整个显示逻辑就是一个巨大的状态机。从“上电自检”到“正常显示”,再到“报警模式”,全靠状态机撑起来的。

常见的应用场景,我帮你列几个:

  • 按键/触摸处理: 去抖、短按、长按、双击。每个按键都是一个独立的状态机。
  • 通信协议解析: UART、I2C、SPI 的帧接收。比如 Modbus 协议,从 IDLE 到 RECV_ADDR 到 RECV_DATA 到 CHECKSUM。
  • 电源管理: 运行态、休眠态、深度睡眠态。事件就是各种唤醒源。
  • 外设驱动: 比如 SD 卡初始化,从 CMD0 到 CMD8 到 ACMD41,每一步都是一个状态。
  • 任务调度: 简单的前后台系统,主循环就是一个大状态机,根据标志位跳转。

说白了,只要你的系统有“顺序逻辑”或者“模式切换”,就适合用状态机。它让代码变得可读、可维护、可测试。

避坑指南: 我曾经在一个项目中,把状态机里的状态数搞到了 50 多个,结果维护起来痛不欲生。状态机不是万能的,状态太多时,建议考虑分层状态机或状态模式。后面章节我会详细讲。

1.4 状态机核心逻辑图

下面这张图,是我自己画的一个通用状态机结构。它展示了三要素之间的关系。你把它理解透了,后面写代码就顺了。

状态机三要素关系图 状态机三要素关系图 状态 A (例如: IDLE) 状态 B (例如: RECV) 事件 + 动作 (按键按下 → 开灯) 自转移: 事件不匹配 状态 C (例如: ERROR) 超时事件 复位事件 状态节点 转移路径 事件/动作说明 自转移/条件转移

这张图里,状态 A 和状态 B 是正常流转。状态 C 是异常处理。自转移表示事件不匹配时,状态机留在原地。你看,整个逻辑一目了然。

1.5 一个简单的例子:LED 灯控制

光说不练假把式。咱们写个最简单的 LED 状态机。这个例子我几乎在每个项目里都会用到,用来验证状态机框架好不好使。

// 状态枚举
typedef enum {
    LED_OFF,
    LED_ON,
    LED_BLINK
} led_state_t;

// 事件枚举
typedef enum {
    EVT_BUTTON_SINGLE_CLICK,
    EVT_BUTTON_DOUBLE_CLICK,
    EVT_TIMEOUT
} led_event_t;

// 状态机处理函数
led_state_t led_fsm(led_state_t current_state, led_event_t event) {
    led_state_t next_state = current_state;  // 默认不转移
    
    switch (current_state) {
        case LED_OFF:
            if (event == EVT_BUTTON_SINGLE_CLICK) {
                led_on();           // 动作:开灯
                next_state = LED_ON;
            } else if (event == EVT_BUTTON_DOUBLE_CLICK) {
                led_blink_start();  // 动作:开始闪烁
                next_state = LED_BLINK;
            }
            break;
            
        case LED_ON:
            if (event == EVT_BUTTON_SINGLE_CLICK) {
                led_off();          // 动作:关灯
                next_state = LED_OFF;
            } else if (event == EVT_BUTTON_DOUBLE_CLICK) {
                led_blink_start();  // 动作:开始闪烁
                next_state = LED_BLINK;
            }
            break;
            
        case LED_BLINK:
            if (event == EVT_BUTTON_SINGLE_CLICK) {
                led_blink_stop();   // 动作:停止闪烁
                led_off();          // 动作:关灯
                next_state = LED_OFF;
            } else if (event == EVT_TIMEOUT) {
                led_blink_stop();   // 动作:停止闪烁
                led_on();           // 动作:常亮
                next_state = LED_ON;
            }
            break;
            
        default:
            // 异常处理,回到安全状态
            led_off();
            next_state = LED_OFF;
            break;
    }
    
    return next_state;
}

这段代码,你仔细看。它把状态、事件、转移、动作都分得清清楚楚。主循环里只需要不断调用 led_fsm() 就行。这就是状态机的魅力——逻辑清晰,好维护。

小技巧: 我习惯把状态转移表单独抽出来,用查表法代替 switch-case。这样状态多了以后,代码不会膨胀。后面章节我会专门讲查表法实现。