一、状态机基础概念

什么是状态机?

状态机,说白了就是一种「有记忆的逻辑」。

我刚开始做嵌入式那会儿,觉得状态机这名字挺唬人的。后来用多了才发现,它其实就是描述一个系统在不同阶段的行为方式。你想想看,我们平时写程序,大部分时候都是顺序执行——从上往下跑。但现实世界不是这样的,系统在不同时刻处于不同状态,行为也不一样。

举个例子,一个自动门。门要么开着,要么关着,要么正在开,要么正在关。每种状态下,它对「有人来了」这个事件的反应完全不同。门关着的时候,有人来了就开门;门开着的时候,有人来了就保持不动。这就是状态机的思维。

核心定义:状态机是一种数学模型,它描述了一个对象在生命周期内,如何响应外部事件,从一个状态迁移到另一个状态。

状态机的三要素

状态机有三个核心要素,我管它叫「铁三角」——状态、事件、动作。缺一个都不行。

1. 状态(State)

状态就是系统在某个时刻的「定格画面」。它代表了系统当前所处的模式或阶段。

  • 特点:状态是稳定的,系统会一直待在某个状态里,直到有事件触发它离开。
  • 数量:有限个。这也是「有限状态机」名字的由来。
  • 例子:按键的「按下状态」「释放状态」;通信协议的「空闲状态」「接收状态」「解析状态」。

我的经验:设计状态机时,我习惯先列所有可能的状态,然后问自己一句:「这些状态之间真的互斥吗?」如果两个状态可以同时存在,那说明你还没把状态拆干净。

2. 事件(Event)

事件是推动状态机运转的「燃料」。没有事件,状态机就是一潭死水。

  • 来源:外部输入(按键按下、数据到达)、内部定时(超时、计数满)、系统条件(缓冲区空、队列满)。
  • 特点:事件是瞬时的,发生完就没了。它不像状态那样能持续存在。
  • 例子:串口收到一个字节、定时器溢出、用户按下按钮。

嗯,这里要注意:事件和状态不要搞混。我曾经见过有人把「正在接收数据」既当状态又当事件,结果状态图画得乱七八糟。记住,状态是「待着的地方」,事件是「让你离开的原因」。

3. 动作(Action)

动作是状态机对事件的「回应」。它是在状态迁移过程中执行的具体操作。

  • 类型:
    • 进入动作:进入某个状态时执行(比如打开LED)
    • 退出动作:离开某个状态时执行(比如关闭LED)
    • 迁移动作:迁移过程中执行(比如发送数据)
  • 例子:从「空闲状态」收到「开始帧」事件,执行「清空接收缓冲区」动作,然后进入「接收状态」。

避坑指南:我曾经在项目里把动作写在了事件处理函数里,结果状态还没变,动作先执行了。后来调试了整整一天才发现——动作一定要和状态迁移绑定在一起,而不是和事件绑定。事件只是触发条件,动作是迁移的一部分。

状态机的应用场景

状态机不是理论玩具,它在嵌入式系统里无处不在。我挑三个最常见的场景说说。

1. 协议解析

协议解析是状态机的「主场」。不管是Modbus、CAN、还是自定义的通信协议,本质上都是在解析一串字节流,而字节流在不同阶段含义不同。

// 一个简单的帧接收状态机
typedef enum {
    FRAME_IDLE,
    FRAME_HEADER,
    FRAME_LENGTH,
    FRAME_DATA,
    FRAME_CRC,
    FRAME_DONE
} FrameState_t;

FrameState_t state = FRAME_IDLE;

void ProcessByte(uint8_t byte) {
    switch (state) {
        case FRAME_IDLE:
            if (byte == 0xAA) {  // 帧头
                state = FRAME_HEADER;
                // 进入动作:重置校验和
                checksum = 0;
            }
            break;
        case FRAME_HEADER:
            if (byte == 0x55) {  // 第二个帧头
                state = FRAME_LENGTH;
            } else {
                state = FRAME_IDLE;  // 帧头错误,回到空闲
            }
            break;
        case FRAME_LENGTH:
            dataLen = byte;
            dataIdx = 0;
            state = FRAME_DATA;
            break;
        // ... 后续状态处理
    }
}

你看,每个状态只关心自己该干什么。收到帧头之前,其他字节一概不理。这就是状态机的威力——把复杂的协议解析拆成一个个清晰的小步骤。

2. 按键消抖

按键消抖是每个嵌入式工程师都会遇到的「老朋友」。机械按键按下和释放时,会产生抖动——电平在几毫秒内反复跳变。如果用普通逻辑去读,一次按键可能被误判成十几次。

状态机解决这个问题非常优雅:

typedef enum {
    KEY_UP,       // 按键释放状态
    KEY_DEBOUNCE, // 消抖等待状态
    KEY_DOWN      // 按键按下状态
} KeyState_t;

KeyState_t keyState = KEY_UP;

void KeyScan(void) {
    uint8_t level = ReadKeyPin();
    
    switch (keyState) {
        case KEY_UP:
            if (level == 0) {  // 检测到低电平(按下)
                keyState = KEY_DEBOUNCE;
                debounceTimer = 10;  // 启动10ms消抖定时
            }
            break;
            
        case KEY_DEBOUNCE:
            if (--debounceTimer == 0) {
                if (level == 0) {
                    keyState = KEY_DOWN;
                    // 确认按键按下,触发事件
                    KeyPressEvent();
                } else {
                    keyState = KEY_UP;  // 抖动干扰,回到释放状态
                }
            }
            break;
            
        case KEY_DOWN:
            if (level == 1) {  // 检测到高电平(释放)
                keyState = KEY_UP;
                KeyReleaseEvent();
            }
            break;
    }
}

我的习惯:按键消抖用状态机,比用延时函数优雅太多了。延时会阻塞CPU,而状态机可以和其他任务共存。我曾经在一个项目里同时处理了8个按键,每个按键都有自己的状态机实例,互不干扰。

3. 游戏AI

游戏AI也是状态机的经典应用。一个敌人的行为可以拆成几个状态:巡逻、追击、攻击、逃跑。每个状态下,它对玩家位置的反应完全不同。

当前状态 事件(玩家进入视野) 事件(玩家离开视野) 事件(血量低于20%)
巡逻 → 追击状态 保持巡逻 → 逃跑状态
追击 保持追击 → 巡逻状态 → 逃跑状态
攻击 保持攻击 → 巡逻状态 → 逃跑状态
逃跑 保持逃跑 保持逃跑 保持逃跑

你看,用状态机描述AI行为,逻辑清晰,扩展也方便。想加一个新状态?直接加一行就行,不会影响其他状态。

本章知识体系

下面这张图,是我画的状态机知识体系。它把三要素和应用场景串在了一起,方便你理解整体脉络。

状态机 状态 (State) 事件 (Event) 动作 (Action) 系统所处的稳定阶段 有限且互斥 触发迁移的外部输入 瞬时发生 迁移时执行的操作 进入/退出/迁移动作 协议解析 字节流分阶段处理 按键消抖 消除机械抖动干扰 游戏AI 行为模式切换 状态 + 事件 + 动作 = 有限状态机 把复杂逻辑拆成简单状态的组合

状态机不是什么高深莫测的东西。它就是一套「遇到什么事,就做什么事」的规则。你只要把状态列清楚,事件定义好,动作写明白,剩下的就是水到渠成的事。

我个人觉得,状态机是嵌入式工程师必须掌握的基本功。它不挑平台,不挑语言,C语言能写,Python也能写。学会了,你写代码的思路会清晰很多。

一句话总结:状态机就是把「什么时候该干什么事」这件事,用结构化的方式表达出来。状态是「在哪」,事件是「发生了什么」,动作是「做什么」。

可可精品资料库