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

大家好,欢迎来到这门实战课。今天咱们从最基础的东西聊起——状态机。

说实话,我刚开始做嵌入式那会儿,觉得状态机就是个花架子。写几个 if-else 不也能跑?后来在一个多按键的遥控器项目里,被各种标志位折腾得欲哭无泪,才老老实实回头补课。嗯,从那以后,状态机就成了我代码里的标配。

1.1 什么是状态机?

状态机,全称有限状态机(Finite State Machine, FSM)。说白了,就是一个系统在任意时刻只能处于“有限个状态”中的一个。当某个事件发生时,它会根据当前状态和事件,决定要不要跳转到另一个状态,以及执行什么动作。

你想想看,这像不像我们日常生活中的红绿灯?红灯、绿灯、黄灯,三个状态。时间到了(事件),就切换。简单、清晰、不会乱。

核心思想:把复杂的逻辑拆解成“状态 + 事件 + 动作”的组合,让程序变得可预测、可调试。

1.2 状态机三要素

状态机有三个核心要素,我习惯叫它“铁三角”。缺一个,这机器就转不起来。

要素 定义 嵌入式中的例子
状态(State) 系统在某一时刻的稳定情形 空闲、接收中、处理中、发送中
事件(Event) 触发状态切换的外部或内部信号 按键按下、定时器溢出、数据到达
动作(Action) 进入/退出状态时执行的操作 点亮LED、启动定时器、发送数据

我的小习惯:在设计状态机时,我会先把所有可能的状态列出来,画成圆圈。然后再想哪些事件能让状态之间产生连线。最后才写代码。这样不容易漏状态。

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

嵌入式系统里,状态机几乎无处不在。我挑几个最常见的场景说说。

1.3.1 按键消抖与长按检测

这是状态机的经典入门案例。一个按键,有按下、弹起、抖动、长按等多个状态。用状态机来处理,比用延时消抖优雅得多。

// 按键状态机示例(伪代码)
enum KeyState {
    KEY_IDLE,      // 空闲
    KEY_DEBOUNCE,  // 消抖中
    KEY_PRESSED,   // 已按下
    KEY_LONG_PRESS // 长按
};

void key_fsm(KeyEvent event) {
    switch (current_state) {
        case KEY_IDLE:
            if (event == KEY_DOWN) {
                current_state = KEY_DEBOUNCE;
                start_timer(10ms);  // 启动10ms消抖定时器
            }
            break;
        case KEY_DEBOUNCE:
            if (event == TIMEOUT) {
                if (key_still_down()) {
                    current_state = KEY_PRESSED;
                    action_key_press();
                } else {
                    current_state = KEY_IDLE;  // 抖动,回到空闲
                }
            }
            break;
        // ... 后续状态类似
    }
}

我曾经踩过的坑:在消抖状态里,只判断了定时器超时,没考虑按键在超时前就松开了。结果按键明明只按了一下,却触发了两次。后来我在每个状态入口都加了一个“事件有效性检查”,才彻底解决。

1.3.2 通信协议解析

UART、SPI、I2C 这些通信协议,接收数据时天然适合用状态机。比如一个简单的数据帧:帧头(0xAA) + 长度 + 数据 + 校验。用状态机来解析,代码结构非常清晰。

// 串口接收状态机
enum RxState {
    WAIT_HEADER,   // 等待帧头
    WAIT_LENGTH,   // 等待长度字节
    WAIT_DATA,     // 等待数据
    WAIT_CHECKSUM  // 等待校验
};

void uart_rx_fsm(uint8_t byte) {
    switch (rx_state) {
        case WAIT_HEADER:
            if (byte == 0xAA) {
                rx_state = WAIT_LENGTH;
                rx_crc = 0;
            }
            break;
        case WAIT_LENGTH:
            rx_len = byte;
            rx_index = 0;
            rx_state = (rx_len > 0) ? WAIT_DATA : WAIT_CHECKSUM;
            break;
        // ...
    }
}

1.3.3 外设驱动与电源管理

STM32 的很多外设,比如 DMA、ADC、定时器,内部本身就是状态机。我们写驱动时,其实是在跟这些硬件状态机打交道。

电源管理更是状态机的重灾区。待机、睡眠、深度睡眠、运行……每个状态切换都要考虑外设状态、时钟配置、唤醒源。我见过一个同事,在电源切换时忘了关 DMA,结果芯片在睡眠状态下还被 DMA 唤醒,功耗死活降不下来。

1.4 状态机的两种实现方式

嵌入式里,状态机实现主要有两种流派。我两种都用过,各有优劣。

实现方式 优点 缺点 适用场景
switch-case 简单直观,代码量少 状态多时代码膨胀,不易维护 状态少于10个的简单场景
状态表(查表法) 结构清晰,易于扩展 需要额外内存存储表 状态多、事件复杂的系统

我的建议:刚开始学状态机,先用 switch-case 把逻辑跑通。等状态超过七八个,再考虑重构为查表法。别一上来就搞复杂架构,容易把自己绕进去。

1.5 状态机知识体系总览

下面这张图,是我自己整理的状态机知识框架。你可以把它当作本章的“地图”,后面每一节都会对应到其中的一个模块。

状态机知识体系总览 有限状态机 (FSM) 状态 (State) 事件 (Event) 动作 (Action) 实现方式:switch-case 实现方式:查表法 按键消抖/长按 通信协议解析 外设驱动 电源管理 设计原则:单一职责 · 状态穷举 · 事件驱动 说明:本章聚焦“三要素”与“应用场景”,后续章节将深入实现方式和设计原则

1.6 本章小结

状态机不是什么高深的理论,它就是一种编程思想。把复杂逻辑拆成“状态 + 事件 + 动作”,代码会变得非常干净。

  • 状态:系统在某个时刻的稳定情形
  • 事件:触发状态切换的信号
  • 动作:进入/退出状态时执行的操作

嵌入式里,按键、通信、外设驱动、电源管理……到处都是状态机的身影。掌握了它,你的代码会从“一团乱麻”变成“一张清晰的地图”。

记住一句话:好的状态机设计,是让 bug 无处藏身。因为每个状态、每个事件、每个动作都是明确定义的,出问题一眼就能定位。