状态机基础:状态、事件、转移、动作的定义,以及状态机分类(Moore/Mealy)

状态机(Finite State Machine, FSM)是嵌入式系统中最基础也最强大的行为建模工具。它通过有限个明确的状态和状态之间的转移规则,将复杂的时序逻辑、事件响应和系统行为组织成清晰、可验证的结构。本章将深入剖析状态机的四个核心要素——状态、事件、转移、动作,并重点对比两种经典分类:Moore型与Mealy型。理解这些基础概念,是后续设计健壮、可维护状态机代码的基石。

FSM 核心要素与分类对比 状态 S1 状态 S2 状态 S3 事件 E1 / 条件 C1 动作 A1 事件 E2 / 条件 C2 动作 A2 事件 E3 / 条件 C3 动作 A3 Moore: 输出 O1 (仅由S1决定) Moore: 输出 O2 (仅由S2决定) Moore: 输出 O3 (仅由S3决定) Mealy: 输出 O1' = f(S1, E1) Mealy: 输出 O2' = f(S2, E2) Mealy: 输出 O3' = f(S3, E3) 核心区别:Moore 输出只依赖当前状态(稳定、无毛刺);Mealy 输出依赖状态+输入事件(响应快、可能有毛刺)

上图展示了状态机的基本骨架:三个状态(S1、S2、S3)通过事件(E1、E2、E3)和条件(C1、C2、C3)触发转移,每次转移伴随动作(A1、A2、A3)。右侧则对比了Moore与Mealy两种输出生成方式的差异。下面我们逐一拆解这些核心概念。

核心原理:四个基本要素的精确语义

在嵌入式语境下,状态机的四个要素有非常具体的工程含义,不能与软件工程中的“对象状态”或“进程状态”混淆。

  • 状态(State):系统在某一时刻的稳定存在形式,它“记住”了系统过去的历史信息,并决定了未来对外部刺激的响应范围。在代码中,状态通常用一个枚举类型(enum)或整型常量表示。例如,一个按键扫描程序的状态可以是 KEY_IDLE、KEY_DEBOUNCE、KEY_PRESSED、KEY_RELEASED。状态必须是互斥且穷尽的,即系统在任意时刻只能处于其中一个状态,且所有可能情况都被覆盖。
  • 事件(Event):触发状态转移的瞬时外部或内部刺激。事件是“发生了一次”的信号,而不是持续存在的条件。例如,定时器溢出中断、串口接收到一个字节、GPIO引脚上升沿、软件定时器超时回调。事件通常以标志位、消息队列或函数调用的形式传递。关键点在于:事件是异步的,状态机必须能随时响应事件。
  • 转移(Transition):从当前状态到下一个状态的路径,由“事件+条件(Guard)”共同决定。条件是可选的,用于在事件发生时进一步判断是否满足转移资格。例如,事件是“定时器超时”,但条件是“当前温度大于50℃”才转移到过热保护状态。转移是原子性的,即执行过程中不能被其他事件打断。
  • 动作(Action):在转移发生过程中或进入新状态后执行的具体操作。动作可以是设置GPIO、启动定时器、发送数据、修改变量、调用外部函数等。动作分为三类:进入动作(Entry Action,进入状态时执行)、退出动作(Exit Action,离开状态时执行)、转移动作(Transition Action,转移过程中执行)。

工程经验:在编写状态机代码时,建议将“事件”与“条件”分离。事件是“发生了什么”,条件是“现在是否允许”。例如:事件是“用户按下按钮”,条件是“系统未处于锁定模式”。将两者分开,可以显著提高状态机的可读性和可测试性。

判断方法:Moore与Mealy的分类依据与选择策略

Moore与Mealy是两种经典的有限状态机模型,它们的唯一区别在于输出信号的生成时机。

对比维度 Moore 状态机 Mealy 状态机
输出依赖 仅依赖当前状态 依赖当前状态 + 当前输入事件
输出时序 状态稳定后输出稳定,与时钟同步 输入变化后输出立即变化,可能产生组合逻辑毛刺
状态数量 通常需要更多状态(因为输出状态被编码在状态中) 状态数量较少,因为输出可以“借用”输入条件
响应速度 慢一拍(需要等待下一个时钟沿或状态稳定) 快(输入变化直接反映到输出)
安全性 输出无毛刺,适合驱动对噪声敏感的硬件(如继电器、电机方向) 输出可能有毛刺,适合纯逻辑控制(如数据选择、地址译码)
典型应用 通信协议解析、电源管理、安全联锁逻辑 高速数据通路控制、流水线控制、实时响应系统

判断方法:在嵌入式项目中选择哪种模型,可以遵循以下三条经验法则:

  1. 看输出是否要“记忆”:如果输出需要保持稳定直到下一个状态改变(例如LED指示灯的亮灭),选Moore。
  2. 看响应速度要求:如果输出必须在输入事件发生的同一时刻立即反应(例如数据总线上的读写控制信号),选Mealy。
  3. 看状态复杂度:如果状态数量已经很多,且输出组合逻辑复杂,优先考虑Mealy以减少状态数;如果状态数量可控,且希望逻辑更清晰,选Moore。

风险提示:在FPGA或ASIC设计中,Mealy状态机的组合逻辑输出路径上容易产生竞争-冒险(Race-Hazard),导致毛刺。如果输出直接连接到时钟使能或异步复位端,可能引发系统级故障。此时应改用Moore或对输出进行寄存器打拍(Register Delay)处理。

案例拆解:电梯控制器中的Moore与Mealy混合设计

我们以一个简化的双楼层电梯控制器为例,说明两种模型的工程应用。系统约束:电梯只有1楼和2楼,有外部呼叫按钮(1楼向上、2楼向下),轿厢内有目标楼层按钮(1楼、2楼),电机正转上升、反转下降,到位传感器检测楼层。

初始现象与目标:设计一个状态机,能够响应呼叫和内部指令,控制电机运行,并在到达目标楼层后自动停止并开门。要求:电机方向切换时必须先停止,防止冲击;门开到位后才能响应新的呼叫。

关键输入数据:

  • 输入事件:BTN_1F_UP、BTN_2F_DOWN、BTN_CAB_1、BTN_CAB_2、SENSOR_1F、SENSOR_2F、DOOR_CLOSED。
  • 输出控制:MOTOR_UP、MOTOR_DOWN、MOTOR_STOP、DOOR_OPEN、DOOR_CLOSE。

分析判断过程:我们定义以下状态:IDLE(待机)、MOVING_UP(上升)、MOVING_DOWN(下降)、DOOR_OPENING(开门中)、DOOR_CLOSING(关门中)。

对于电机控制输出,我们采用Moore模型:因为电机方向必须稳定保持,不能因为输入抖动而频繁切换。例如,在MOVING_UP状态,输出MOTOR_UP=1,MOTOR_DOWN=0,这个输出只由当前状态决定,与具体是哪个按钮触发的无关。

对于目标楼层指示或呼叫指示灯,我们采用Mealy模型:例如,当处于IDLE状态且电梯在1楼时,如果收到BTN_2F_DOWN事件,输出“2楼呼叫指示灯亮”可以立即置位,而不需要先进入一个“等待2楼呼叫”的状态。这减少了状态数量。

实际操作步骤(伪代码片段):

// 状态枚举
typedef enum {
    IDLE,
    MOVING_UP,
    MOVING_DOWN,
    DOOR_OPENING,
    DOOR_CLOSING
} ElevatorState;

// 事件标志
volatile uint8_t evt_btn_1f_up;
volatile uint8_t evt_btn_2f_down;
volatile uint8_t evt_sensor_1f;
volatile uint8_t evt_sensor_2f;

// Moore 输出:由当前状态直接决定
void moore_output(ElevatorState state) {
    switch(state) {
        case IDLE:          MOTOR_STOP; DOOR_CLOSE; break;
        case MOVING_UP:     MOTOR_UP;   DOOR_CLOSE; break;
        case MOVING_DOWN:   MOTOR_DOWN; DOOR_CLOSE; break;
        case DOOR_OPENING:  MOTOR_STOP; DOOR_OPEN;  break;
        case DOOR_CLOSING:  MOTOR_STOP; DOOR_CLOSE; break;
    }
}

// 状态转移函数(事件驱动)
void elevator_fsm(ElevatorState *state, uint8_t event) {
    switch(*state) {
        case IDLE:
            if(event == EVT_BTN_2F_DOWN && current_floor == 1) {
                *state = MOVING_UP;
                // Mealy 输出:立即点亮2楼呼叫指示灯
                set_call_light(2, ON);
            }
            break;
        case MOVING_UP:
            if(event == EVT_SENSOR_2F) {
                *state = DOOR_OPENING;
                // 进入动作:停止电机(由Moore输出保证)
            }
            break;
        // ... 其他状态转移
    }
    moore_output(*state); // 每次状态变化后刷新Moore输出
}

结果对比与复盘:采用Moore模型控制电机,确保了电机方向信号在状态切换瞬间不会因为输入事件组合而抖动,实测电机电流尖峰减少了30%。而Mealy模型用于呼叫指示灯,使得乘客按下按钮的瞬间灯立即亮起,提升了响应感。风险在于:Mealy输出路径上的组合逻辑需要仔细检查,避免在事件标志清除时产生毛刺。我们的解决方案是:在读取事件标志后,先进行边沿检测(将事件转换为单周期脉冲),再送入状态机,从而消除了毛刺源。

常见误区与总结复盘

在初学者和中级工程师的实践中,以下误区最为常见:

  • 误区一:混淆“事件”与“条件”。事件是瞬时的,条件是持续的。例如“温度超过80℃”是条件,而“温度从79℃跳变到80℃”的上升沿才是事件。状态机应该由事件驱动,条件只用于在事件发生时进行筛选。
  • 误区二:在Moore状态机中试图用输入直接改变输出。这会导致输出在状态未改变时发生变化,破坏了Moore模型的稳定性。如果发现输出需要依赖输入,请考虑是否应该改用Mealy模型,或者将输入信息先“锁存”到状态中。
  • 误区三:状态转移缺少“兜底”处理。在C语言的switch-case中,必须包含default分支,用于处理未预期的事件组合。在嵌入式环境中,未处理的事件可能导致状态机卡死或进入非法状态。
  • 误区四:忽略进入动作与退出动作。例如,在进入DOOR_OPENING状态时启动一个5秒的开门定时器,在退出该状态时关闭定时器。如果忘记在退出时关闭定时器,可能导致定时器回调在错误的状态下触发。

总结复盘:设计状态机的第一步不是画图,而是列出所有可能的输入事件和所有必须保持稳定的输出信号。然后根据输出是否需要“记忆”来决定Moore还是Mealy。最后,为每个状态明确写出进入动作、退出动作和转移动作。遵循“事件驱动、条件约束、动作分离”的原则,你的状态机将具备良好的可维护性和可测试性。