第一章:状态机基础

各位同学好,我是你们这门课的老朋友。今天咱们聊聊状态机——这个在嵌入式世界里无处不在的东西。

说实话,我刚开始做嵌入式那会儿,对状态机也没什么概念。觉得不就是几个 if-else 嘛,有什么好学的?直到有一次,我负责一个智能门锁项目,客户要求支持指纹、密码、刷卡、手机蓝牙四种开锁方式,还要考虑低电量、防撬报警、多次错误锁定……好家伙,if-else 嵌套了七八层,代码改一次崩一次。后来一位老前辈跟我说:「小伙子,学学状态机吧。」

嗯,从那以后,我就再也没离开过状态机。

什么是状态机

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

你想想看,电梯就是一个典型的状态机:

  • 它要么在「静止关门」状态
  • 要么在「运行中」状态
  • 要么在「开门」状态

不可能同时既开门又运行,对吧?这就是状态机的核心思想——同一时刻只有一个状态

核心定义:有限状态机是一个数学模型,它由有限个状态以及在这些状态之间的转移构成。系统在任何时刻只能处于其中一个状态。

状态机的三要素

状态机有三个核心要素:状态、事件、动作。我习惯把它们叫做「状态机三剑客」,缺一个都不行。

1. 状态(State)

状态就是系统在某个时刻的「模样」。比如一个按键:

  • 空闲态:没人按它
  • 按下态:手指正在按着
  • 释放态:手指刚松开

我在项目中遇到过一个问题:有人把「按下态」和「释放态」合并成一个「按下后释放」状态。结果呢?消抖逻辑全乱了。记住,每个状态必须是一个明确的、互斥的「快照」

2. 事件(Event)

事件是触发状态变化的「导火索」。没有事件,状态机就是一潭死水。

常见的事件类型:

  • 外部事件:按键按下、传感器触发、串口收到数据
  • 内部事件:定时器超时、计数达到阈值、状态内部条件满足
  • 时间事件:经过 100ms、到达某个时刻

举个例子,我做过一个智能灯控系统。灯在「关闭」状态时,收到「人体红外检测到人」这个事件,就切换到「开启」状态。事件来了,状态才变。

我的小技巧:事件命名要「动词+名词」,比如 EVT_BUTTON_PRESSEDEVT_TIMER_EXPIRED。别用 EVT_STATE1 这种,三个月后你自己都看不懂。

3. 动作(Action)

动作是状态切换时「做的事」。动作分三种:

  • 进入动作:进入某个状态时执行(比如开灯)
  • 退出动作:离开某个状态时执行(比如关灯)
  • 转移动作:状态切换过程中执行(比如记录日志)

我曾经犯过一个错:把动作写在了事件处理函数里,而不是状态转移里。结果同一个事件在不同状态下触发了不同的动作,代码逻辑乱成一锅粥。后来我学乖了——动作一定要绑定在状态转移上,而不是事件上。

状态机的应用场景

状态机到底能用在哪儿?我随便列几个,都是实战中遇到的:

应用领域 典型场景 为什么用状态机
嵌入式控制 按键消抖、LED 灯效切换 逻辑清晰,避免 if-else 地狱
通信协议 UART 帧解析、I2C 时序控制 每个字节对应一个状态,好调试
用户界面 菜单导航、多级界面切换 状态与界面一一对应,好维护
自动化设备 洗衣机流程、电梯控制 流程清晰,容易扩展新功能
游戏开发 角色行为、AI 决策 每个行为一个状态,好管理

你想想看,这些场景有什么共同点?都是「根据当前情况,决定下一步做什么」。状态机就是干这个的。

避坑指南:我曾经接手过一个项目,状态机里定义了 50 多个状态。一问才知道,他们把「等待 1ms」「等待 2ms」「等待 3ms」都当成了独立状态。其实用「等待中 + 计时器」一个状态就够了。记住:状态是「质」的变化,不是「量」的变化

一个简单的例子:按键消抖

光说不练假把式。咱们看一个最经典的状态机例子——按键消抖。

按键按下时,机械触点会抖动,产生多个高低电平变化。如果用 if-else 直接判断,一次按下可能被误判成十几次。状态机怎么解决?

// 按键状态机 - 三个状态
typedef enum {
    KEY_IDLE,       // 空闲态:等待按键按下
    KEY_DEBOUNCE,   // 消抖态:等待抖动结束
    KEY_PRESSED     // 按下态:确认按键按下
} KeyState_t;

// 事件定义
#define EVT_KEY_DOWN    0x01  // 检测到低电平
#define EVT_KEY_UP      0x02  // 检测到高电平
#define EVT_TIMEOUT     0x03  // 消抖计时到

KeyState_t currentState = KEY_IDLE;

void KeyFSM_Handler(uint8_t event) {
    switch (currentState) {
        case KEY_IDLE:
            if (event == EVT_KEY_DOWN) {
                StartDebounceTimer(10);  // 启动10ms消抖定时器
                currentState = KEY_DEBOUNCE;
            }
            break;

        case KEY_DEBOUNCE:
            if (event == EVT_TIMEOUT) {
                // 10ms后再次检测电平
                if (ReadKeyPin() == LOW) {
                    currentState = KEY_PRESSED;
                    NotifyKeyPressed();  // 通知按键按下
                } else {
                    currentState = KEY_IDLE;  // 抖动,回到空闲
                }
            } else if (event == EVT_KEY_UP) {
                // 消抖期间按键释放,说明是抖动
                currentState = KEY_IDLE;
            }
            break;

        case KEY_PRESSED:
            if (event == EVT_KEY_UP) {
                currentState = KEY_IDLE;
                NotifyKeyReleased();  // 通知按键释放
            }
            break;
    }
}

看到了吗?三个状态,三个事件,逻辑清清楚楚。每个状态只处理自己关心的事件,不会乱。这就是状态机的魅力——把复杂逻辑拆成小块,每块只管自己的事

我的习惯:画状态转移图。画图的时候,你就能发现「哎呀,这个状态怎么没出口?」或者「这两个状态是不是可以合并?」。纸上谈兵总比代码里改来改去强。

小结

这一章咱们聊了状态机的基础:

  • 什么是状态机——同一时刻只有一个状态,根据事件切换
  • 三要素——状态、事件、动作,缺一不可
  • 应用场景——从按键消抖到电梯控制,无处不在

下一章,咱们会深入状态机的两种实现方式——switch-case 和查表法。我会告诉你哪种更适合你的项目。嗯,到时候见。