2、状态机基础理论:有限状态机(FSM)概念、状态、事件、转移、动作

好,咱们正式开始讲状态机的核心理论。说实话,很多嵌入式工程师写了几年代码,天天跟状态机打交道,但真要他说清楚「状态机到底是什么」,反而支支吾吾。我当年刚入行时也这样,直到被一个老领导逼着画了三个月状态图,才算真正开了窍。

有限状态机,英文叫 Finite State Machine,简称 FSM。说白了,它就是一套「在有限个状态之间,根据事件驱动进行跳转」的数学模型。你想想看,体温计这东西,它不可能同时处于「测量中」和「待机」两个状态吧?这就是有限状态机最朴素的道理——任何时刻,系统只能处于一个确定的状态

2.1 状态(State)——系统的「定格画面」

状态是什么?我习惯把它理解为「系统在某个时刻的稳定快照」。比如体温计开机后,要么在待机,要么在测量,要么在显示结果。每个状态都对应着一组确定的行为和输出。

我在项目中遇到过一个问题:有个同事把「正在校准」和「校准完成」混在一起处理,结果状态机跑飞了。后来我帮他梳理,发现他漏了一个关键状态。所以记住:状态必须互斥且完备——任何时刻只能在一个状态里,而且所有可能的情况都要被覆盖到。

状态的三要素:
  • 名称:唯一标识,比如 IDLE、MEASURING
  • 行为:在该状态下系统做什么(比如待机时关闭屏幕)
  • 持续时间:可以是无限长(等待事件),也可以是有限长(定时退出)

2.2 事件(Event)——触发变化的「导火索」

事件就是让状态机动起来的东西。没有事件,状态机就死在那了。体温计上的按键按下、定时器超时、温度传感器数据就绪……这些都是事件。

我个人习惯把事件分为三类:

事件类型 举例 特点
外部事件 按键按下、红外信号 来自硬件外设,不可预测
内部事件 定时器超时、数据就绪 由系统自身产生,可预测
时间事件 经过 5 秒、到达某个时刻 本质是定时器,但逻辑上独立

嗯,这里要注意:事件本身不携带状态信息。它只是一个信号,告诉状态机「该干活了」。至于怎么干,那是转移和动作的事。

2.3 转移(Transition)——状态的「跳跃规则」

转移就是「当某个事件发生时,从当前状态跳到下一个状态」的规则。它像一条有方向的线,连接着两个状态。

我曾经犯过一个低级错误:在体温计项目里,我写了从「测量中」到「待机」的直接转移,但忘了加「测量完成」这个条件。结果每次按键都会打断测量,直接回到待机。用户反馈说「体温计测到一半就停了」……嗯,从那以后我每次画转移都会问自己三个问题:

  1. 触发条件是什么?——哪个事件会引发这次转移?
  2. 从哪来?到哪去?——源状态和目标状态分别是什么?
  3. 有没有保护条件?——比如「测量中」状态下,按键事件是否应该被忽略?
避坑指南:我曾经接手过一个项目,状态图里画了 20 多条转移线,但有一半是多余的。后来我用「状态转移矩阵」重新梳理,才发现很多转移根本不会发生。建议你在设计初期就用表格列出所有可能的(状态,事件)组合,看看有没有遗漏或冲突。

2.4 动作(Action)——转移时的「副作用」

动作是状态机里最容易被人忽略的部分。很多人以为状态机就是「状态 + 转移」,其实动作才是真正干活的东西。

动作分为三种:

  • 进入动作(Entry Action):进入某个状态时执行一次。比如进入「显示结果」状态时,点亮屏幕并刷新数值。
  • 退出动作(Exit Action):离开某个状态时执行一次。比如退出「测量中」状态时,关闭传感器电源。
  • 转移动作(Transition Action):在转移过程中执行。比如从「待机」到「测量中」时,启动定时器。

你想想看,如果把动作写错了地方,后果很严重。我见过一个案例:有人把「关闭传感器」写在了进入动作里,结果每次进入「待机」状态都关一次传感器——明明传感器早就关了,又关一次,导致硬件报错。

重要提醒:动作应该是「无状态」的。什么意思?就是动作执行的结果不应该依赖于系统当前的其他状态。否则你的状态机就会变成「薛定谔的状态机」——同一个事件,同样的状态,执行结果却不一样。这在嵌入式系统里是灾难。

2.5 一个完整的 FSM 示例

光说不练假把式。咱们拿体温计的一个简化场景来演示:

// 体温计状态机 - 简化版
typedef enum {
    STATE_IDLE,       // 待机
    STATE_MEASURING,  // 测量中
    STATE_DISPLAY,    // 显示结果
    STATE_ERROR       // 错误
} State_t;

typedef enum {
    EVENT_BUTTON_PRESS,    // 按键按下
    EVENT_MEASURE_DONE,    // 测量完成
    EVENT_TIMEOUT,         // 超时
    EVENT_ERROR_OCCURRED   // 发生错误
} Event_t;

State_t current_state = STATE_IDLE;

void fsm_handle_event(Event_t event) {
    switch (current_state) {
        case STATE_IDLE:
            if (event == EVENT_BUTTON_PRESS) {
                // 转移动作:启动测量
                start_sensor();
                start_timer(5000);  // 5秒超时
                current_state = STATE_MEASURING;
            }
            break;

        case STATE_MEASURING:
            if (event == EVENT_MEASURE_DONE) {
                // 转移动作:读取数据
                read_temperature();
                current_state = STATE_DISPLAY;
            } else if (event == EVENT_TIMEOUT) {
                // 转移动作:停止传感器
                stop_sensor();
                current_state = STATE_ERROR;
            }
            break;

        case STATE_DISPLAY:
            if (event == EVENT_BUTTON_PRESS) {
                // 退出动作:关闭显示
                turn_off_display();
                current_state = STATE_IDLE;
            } else if (event == EVENT_TIMEOUT) {
                // 自动返回待机
                turn_off_display();
                current_state = STATE_IDLE;
            }
            break;

        case STATE_ERROR:
            if (event == EVENT_BUTTON_PRESS) {
                // 复位
                reset_system();
                current_state = STATE_IDLE;
            }
            break;
    }
}

这段代码虽然简单,但已经包含了状态机的全部要素。你看,每个 case 就是一个状态,每个 if 就是一个事件判断,状态赋值就是转移,函数调用就是动作。

2.6 状态机的两种实现风格

我个人用过两种主流实现方式,各有优劣:

风格 优点 缺点 适用场景
switch-case 式 简单直观,容易调试 状态多了代码膨胀 状态数 < 10 的小型系统
状态表驱动式 结构清晰,易于扩展 需要额外维护表格 状态数 > 10 的复杂系统

对于体温计这种状态数不超过 10 个的设备,我建议用 switch-case。为什么?因为简单。你想想看,一个体温计总共就待机、测量、显示、校准、错误这么几个状态,用状态表反而增加了维护成本。

核心总结:
  • 状态是系统的「快照」,必须互斥且完备
  • 事件是「导火索」,触发状态变化
  • 转移是「规则」,定义从哪来到哪去
  • 动作是「副作用」,在进入、退出或转移时执行
  • 一个状态机 = 有限状态 + 有限事件 + 转移规则 + 动作

好了,这一章的内容就到这里。下一章咱们会深入讨论状态机的设计方法,包括怎么画状态图、怎么避免常见的设计陷阱。到时候我会拿一个真实的体温计项目案例来拆解,保证让你看完就能上手。