一、状态机基础概念

什么是状态机?

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

我经常跟团队里的新人说:你想想看,一个开关只有开和关两种状态,按一下切换一次——这就是最简单的状态机。只不过在实际的嵌入式系统里,状态可能多得多,逻辑也复杂得多。

状态机的全称叫「有限状态机」(Finite State Machine, FSM)。核心思想就一句话:系统在任意时刻只能处于有限个状态中的一个,并且根据输入事件,从一个状态跳转到另一个状态。

嗯,这里要注意:不是所有能跳转的东西都叫状态机。真正的状态机必须满足三个条件——状态是有限的、跳转是有条件的、行为是可预测的。

核心定义:有限状态机是一种数学模型,用于描述系统在有限个状态之间的行为逻辑。它由状态、事件和动作三要素构成。

状态机的三要素

我在项目中遇到过不少新手,一上来就写 if-else 嵌套,写到后面自己都看不懂。其实只要掌握了三要素,状态机的设计就清晰了。

1. 状态(State)

状态就是系统在某个时刻的「快照」。比如一个门禁系统,可以有「锁定」、「验证中」、「已开锁」三个状态。

状态有几个特点:

  • 互斥性——同一时刻只能处于一个状态
  • 稳定性——没有事件触发时,状态不会自己变
  • 可枚举——所有状态都是预先定义好的

我的习惯:设计状态机时,我一般先用枚举把所有状态列出来。这样后面写代码时,编译器还能帮你检查有没有漏掉状态。

2. 事件(Event)

事件是触发状态跳转的「导火索」。可以是外部输入(比如按键按下),也可以是内部信号(比如定时器超时)。

举个例子:

  • 按键按下 → 从「待机」跳转到「唤醒」
  • 传感器数据异常 → 从「正常运行」跳转到「故障处理」
  • 通信超时 → 从「等待应答」跳转到「重发」

你想想看,如果没有事件,状态机就是死的。事件才是让系统「活」起来的关键。

3. 动作(Action)

动作是状态跳转时执行的「副作用」。比如:

  • 进入新状态时点亮LED
  • 退出旧状态时关闭外设
  • 跳转过程中记录日志

动作可以发生在三个时机:进入状态时、退出状态时、跳转过程中。我个人习惯把动作尽量放在「进入状态」时执行,这样逻辑更清晰。

我曾经踩过的坑:有一次做智能家居项目,我在状态跳转过程中做了太多动作——开灯、关空调、发通知、写日志全挤在一起。结果跳转延迟了200ms,用户体验极差。后来我把动作拆成「必须立即执行」和「可以延迟执行」两类,问题就解决了。

状态机的应用场景

状态机在嵌入式系统里几乎无处不在。我列几个典型的场景:

应用场景 典型状态 典型事件
按键消抖 空闲、按下确认、按下、释放确认 电平变化、定时器超时
通信协议 空闲、发送中、等待应答、重发 数据到达、超时、应答收到
电源管理 运行、休眠、深度休眠、唤醒 空闲超时、唤醒中断、电量低
菜单导航 主菜单、子菜单、设置、确认 按键上、按键下、确认键、返回键
设备控制 待机、启动、运行、故障、停止 启动指令、停止指令、故障信号

为什么这些场景适合用状态机?因为它们都有一个共同点:行为逻辑是「状态依赖」的。同一个按键按下,在不同状态下做的事情完全不同——这就是状态机的用武之地。

状态机的图形化表示

下面我用一个简单的按键消抖状态机来展示三要素之间的关系。这个图我画了很多次,每次给新人讲状态机都会用到。

空闲 IDLE 按下确认 PRESS_CONFIRM 按下 PRESSED 释放确认 RELEASE_CONFIRM 电平变低 超时(10ms) 电平变高 电平变高 超时(10ms) 电平变低 按键消抖状态机 状态:系统快照 事件:电平变化/超时 动作:输出按键值

这个图里,每个椭圆代表一个状态,箭头代表事件触发的跳转,而动作就写在跳转的「副作用」里。你看,三要素一目了然。

状态机的两种实现方式

实际编码时,状态机主要有两种写法:

方式一:switch-case 状态机

typedef enum {
    STATE_IDLE,
    STATE_PRESS_CONFIRM,
    STATE_PRESSED,
    STATE_RELEASE_CONFIRM
} state_t;

state_t current_state = STATE_IDLE;

void fsm_run(event_t event) {
    switch (current_state) {
        case STATE_IDLE:
            if (event == EVENT_PIN_LOW) {
                current_state = STATE_PRESS_CONFIRM;
                // 动作:启动定时器
            }
            break;
        case STATE_PRESS_CONFIRM:
            if (event == EVENT_TIMEOUT) {
                current_state = STATE_PRESSED;
                // 动作:输出按键按下
            } else if (event == EVENT_PIN_HIGH) {
                current_state = STATE_IDLE;
                // 动作:取消定时器
            }
            break;
        // ... 其他状态
    }
}

方式二:状态表驱动

typedef struct {
    state_t  current_state;
    event_t  event;
    state_t  next_state;
    void     (*action)(void);
} transition_t;

transition_t table[] = {
    {STATE_IDLE,           EVENT_PIN_LOW,  STATE_PRESS_CONFIRM,  start_timer},
    {STATE_PRESS_CONFIRM,  EVENT_TIMEOUT,  STATE_PRESSED,        key_pressed},
    {STATE_PRESS_CONFIRM,  EVENT_PIN_HIGH, STATE_IDLE,           cancel_timer},
    // ...
};

我的建议:状态少于10个时,用switch-case就够了,简单直观。状态多了,比如20个以上,我建议用状态表,维护起来方便很多。

小结

状态机不是什么高深的东西。说白了就是:系统在有限个状态之间,根据事件做跳转,跳转时执行动作。三要素——状态、事件、动作——就是它的全部。

我见过太多人把状态机搞复杂了。其实你只要记住:状态是「在哪里」,事件是「发生了什么」,动作是「要做什么」。想清楚这三件事,状态机就设计好了。

下一节我们会聊状态机的设计原则,包括怎么划分状态、怎么定义事件、怎么避免状态爆炸。这些都是我在实际项目中踩过的坑,到时候跟你细说。

可可精品资料库