1. 状态机基础:什么是状态机、状态机的三要素、应用场景

大家好,我是你们的嵌入式系统讲师。今天咱们聊聊状态机——这个在嵌入式开发里绕不开的核心概念。

说实话,我刚开始做嵌入式那会儿,对状态机也是一知半解。直到有一次,我接手一个同事留下的按键处理代码——那叫一个乱啊!各种if-else嵌套,全局变量满天飞。调试了三天,愣是没搞明白某个按键按下后系统到底会跳到哪个分支去。后来我痛定思痛,用状态机重写了那套逻辑,代码量少了三分之一,bug也基本清零了。

从那以后,我养成了一个习惯:但凡涉及流程控制、模式切换、协议解析,第一反应就是——用状态机。

什么是状态机?

状态机,全称有限状态机(Finite State Machine, FSM)。说白了,就是一个系统在任意时刻只能处于有限个状态中的一个。它根据当前状态和输入事件,决定下一步要做什么、跳到哪个状态。

你想想看,我们生活中的很多事物都是状态机:

  • 红绿灯:红灯→绿灯→黄灯→红灯,循环往复
  • 电梯:静止、上行、下行、开门、关门
  • 自动售货机:等待投币、已投币、出货中、找零中

嵌入式系统里更是无处不在。按键消抖、通信协议解析、菜单导航、电源管理……几乎每个模块都能用状态机来建模。

核心要点:状态机是一种思维模型,它强迫你把复杂的逻辑拆解成「状态」和「事件」两个维度。一旦你习惯了这种思维方式,写代码会变得异常清晰。

状态机的三要素

状态机有三个核心要素:状态事件动作。缺一不可。

1. 状态(State)

状态就是系统在某个时刻所处的「模式」或「阶段」。状态必须是互斥的——同一时刻只能处于一个状态。

举个例子,一个简单的按键消抖程序,我通常会定义这么几个状态:

typedef enum {
    KEY_STATE_IDLE,      // 空闲状态
    KEY_STATE_PRESSED,   // 按下检测中
    KEY_STATE_CONFIRMED, // 确认按下
    KEY_STATE_RELEASED   // 释放检测中
} KeyState_t;

嗯,这里要注意:状态的数量要尽量少。我见过有人把状态定义成几十个,结果状态图画出来跟蜘蛛网似的,调试起来特别痛苦。我个人习惯是:一个状态机不超过10个状态,超过就考虑拆分。

2. 事件(Event)

事件是触发状态切换的「导火索」。事件可以是外部输入(比如按键按下、串口收到数据),也可以是内部条件(比如定时器超时、计数达到阈值)。

我曾经在一个项目中踩过坑:把「定时器超时」和「按键按下」混在一起处理,结果状态机逻辑乱成一团。后来我学乖了,把所有事件统一枚举,清晰明了:

typedef enum {
    EVENT_NONE,
    EVENT_KEY_PRESS,      // 按键按下
    EVENT_KEY_RELEASE,    // 按键释放
    EVENT_TIMEOUT_10MS,   // 10ms定时超时
    EVENT_TIMEOUT_50MS,   // 50ms定时超时
    EVENT_DATA_RECEIVED   // 收到数据
} Event_t;

我的经验:事件定义要「原子化」。一个事件只代表一个单一的变化,不要把多个条件组合成一个事件。比如不要定义「按键按下且定时器超时」这种复合事件,否则状态机就失去了它的简洁性。

3. 动作(Action)

动作就是状态切换时系统要执行的操作。动作可以发生在:

  • 进入状态时(entry action)
  • 离开状态时(exit action)
  • 状态内部(internal action)
  • 状态切换时(transition action)

我习惯把动作写成函数指针,这样状态机引擎可以统一调度。来看一个典型的按键消抖状态机实现:

// 状态机表:每个状态对应的事件处理
typedef struct {
    KeyState_t   currentState;
    Event_t      event;
    KeyState_t   nextState;
    void (*action)(void);  // 动作函数指针
} StateTransition_t;

// 状态转移表
const StateTransition_t keyFsmTable[] = {
    // 当前状态       事件            下一状态        动作
    {KEY_STATE_IDLE,   EVENT_KEY_PRESS,   KEY_STATE_PRESSED,   startDebounceTimer},
    {KEY_STATE_PRESSED, EVENT_TIMEOUT_10MS, KEY_STATE_CONFIRMED, keyPressedConfirmed},
    {KEY_STATE_CONFIRMED, EVENT_KEY_RELEASE, KEY_STATE_RELEASED, startReleaseTimer},
    {KEY_STATE_RELEASED, EVENT_TIMEOUT_10MS, KEY_STATE_IDLE,    keyReleasedDone},
    // ... 其他转移
};

你看,这样写出来的代码,逻辑一目了然。加一个新状态或者新事件,只需要在表格里加一行就行,完全不用动核心逻辑。

避坑指南:我曾经在一个项目中,动作函数里又调用了状态机切换函数,结果造成了递归调用,栈溢出了。记住:动作函数里不要直接或间接触发状态切换,否则状态机就变成了「状态机套娃」。

状态机的应用场景

状态机在嵌入式系统里到底能用在哪些地方?我给大家列几个典型的:

应用场景 典型状态 典型事件
按键消抖 空闲、按下检测、确认按下、释放检测 按键按下、按键释放、定时超时
UART协议解析 等待帧头、接收长度、接收数据、校验 收到字节、超时、校验错误
菜单导航 主菜单、子菜单、设置项、编辑模式 上翻、下翻、确认、返回
电源管理 运行、空闲、休眠、深度休眠 无操作超时、唤醒中断、电量低
电机控制 停止、加速、匀速、减速、急停 启动指令、速度到达、故障信号

我个人觉得,判断一个场景是否适合用状态机,有个简单标准:如果这段逻辑里有很多「当...时,就...」的句子,那它大概率适合用状态机

比如:「当按键按下时,启动定时器」「当定时器超时时,确认按键有效」「当按键释放时,停止定时器」——你看,这不就是天然的状态机吗?

总结一下:状态机不是什么高深的理论,它就是一种把复杂逻辑「结构化」的工具。三要素——状态、事件、动作——就像乐高积木的三个基本形状。你只要掌握了这三个要素,就能搭出任何复杂的系统。

下一章,我会带大家手写一个轻量级的状态机引擎,看看怎么把今天讲的理论落地成代码。到时候我会分享一个我在实际项目中用过的框架,保证你学了就能用。

嗯,今天就到这里。记住:状态机不是万能的,但没有状态机是万万不能的。