4. 状态机设计模式:从Moore到分层状态机

状态机的设计模式,说白了就是解决「输出怎么产生」这个问题。我刚开始做嵌入式时,觉得状态机嘛,switch-case一把梭就完了。后来项目越做越复杂,才发现选错模式,后期维护简直是噩梦。

这一节,我们把这四种模式掰开揉碎讲清楚。你想想看,搞懂它们,你写出来的状态机代码会清晰很多。

4.1 Moore型状态机:输出只跟状态有关

Moore型状态机的核心规则很简单:输出只取决于当前状态。输入信号只决定状态怎么跳,不直接决定输出。

核心特征:输出 = f(当前状态)

我在项目中遇到过一个小型温控器,就用Moore型实现的。温度传感器每隔100ms采样一次,状态机根据采样值决定是「加热」「保温」还是「冷却」。输出(加热器开关、风扇转速)完全由当前状态决定,跟采样时刻的输入值无关。这样做的好处是——输出非常稳定,不会因为输入抖动而频繁切换。

Moore型状态机的优缺点:

维度 说明
输出稳定性 高,输出只在状态切换时变化
状态数量 通常比Mealy型多(因为输出信息编码在状态里)
时序分析 简单,输出与时钟同步
典型场景 按键消抖、LED控制、协议解析中的帧头检测

我的习惯:如果输出需要「干净」的信号,比如驱动继电器、控制电机启停,我优先选Moore型。输出毛刺少,省去很多滤波麻烦。

4.2 Mealy型状态机:输出看状态也看输入

Mealy型状态机就灵活多了。它的输出不仅取决于当前状态,还取决于当前的输入信号。说白了,输出是状态和输入的组合函数。

核心特征:输出 = f(当前状态, 当前输入)

为什么会有人用Mealy型?因为它可以用更少的状态完成同样的功能。我记得做一个UART接收器,用Moore型需要5个状态,换成Mealy型3个状态就搞定了。状态少了,代码量自然就下来了。

但要注意:Mealy型的输出可能产生毛刺。因为输入信号变化会直接反映到输出上,如果输入有抖动,输出也会跟着抖。

维度 说明
状态效率 高,用更少状态实现相同功能
输出响应 快,输入变化立即反映到输出
毛刺风险 较高,需要额外处理
典型场景 高速数据通路、协议解析中的字节边界检测

我曾经踩过的坑:在一个SPI从机设计中用了纯Mealy型,结果输出信号在时钟边沿附近出现毛刺,导致主机采样错误。后来在输出端加了寄存器打一拍,问题才解决。所以,Mealy型输出最好用寄存器同步一下。

4.3 混合型状态机:取长补短

实际项目中,纯Moore或纯Mealy都不一定是最优解。混合型状态机就是把两者结合起来:部分输出由状态决定,部分输出由状态+输入决定。

我个人的习惯是:把稳定的控制信号(比如模式切换、使能信号)用Moore方式产生,把需要快速响应的信号(比如数据选通、中断标志)用Mealy方式产生。这样既保证了稳定性,又兼顾了响应速度。

混合型的设计原则:

  • 状态相关的输出:用Moore方式,寄存器输出,干净稳定
  • 输入相关的输出:用Mealy方式,组合逻辑输出,响应快
  • 关键路径:对Mealy输出加寄存器打拍,消除毛刺

避坑指南:混合型状态机容易让代码变得混乱。我建议在状态定义里明确标注每个输出是Moore型还是Mealy型,或者在代码注释里写清楚。不然三个月后你自己都看不懂。

4.4 分层状态机(HSM):对付复杂逻辑的利器

当状态数量超过20个,或者状态之间有明显的「父子关系」时,扁平状态机就力不从心了。这时候,分层状态机(Hierarchical State Machine, HSM)就派上用场了。

HSM的核心思想是:状态可以嵌套。子状态继承父状态的属性和行为,子状态处理不了的输入,交给父状态处理。

举个例子:一个通信协议栈,顶层状态是「空闲」「连接中」「已连接」「断开」。在「已连接」状态下,又可以分为「等待数据」「接收数据」「发送数据」等子状态。子状态共享父状态的超时处理、错误恢复等逻辑。

HSM的优势:

  • 减少重复代码:公共行为放在父状态,子状态只需关注差异
  • 逻辑清晰:状态层次结构反映了系统的真实结构
  • 易于扩展:新增子状态不影响其他分支

我的经验:HSM特别适合实现通信协议的状态机。比如Modbus、CANopen、MQTT这些协议,状态层次天然就是分层的。用HSM实现,代码量能减少40%以上。

下面这张图展示了四种状态机模式的核心区别:

四种状态机设计模式对比 Moore型 输出 = f(当前状态) 状态A → 输出X 状态B → 输出Y 状态C → 输出Z Mealy型 输出 = f(状态, 输入) 状态A + 输入1 → 输出X 状态A + 输入2 → 输出Y 状态B + 输入1 → 输出Z 混合型 Moore + Mealy 组合 Moore输出: 使能信号 Mealy输出: 数据选通 寄存器同步Mealy输出 分层HSM 状态嵌套 + 继承 父状态: 已连接 子状态: 等待数据 子状态: 接收数据 子状态: 发送数据 选型建议 • 输出需要干净稳定 → 选Moore型(继电器、电机控制) • 状态数量敏感、需要快速响应 → 选Mealy型(高速数据通路) • 既有稳定输出又有快速响应需求 → 选混合型(通信协议控制) • 状态超过20个、有父子关系 → 选分层HSM(协议栈、设备管理) • 我个人习惯:优先用Moore型,必要时局部用Mealy,复杂系统上HSM • 记住:没有银弹,根据实际场景灵活选择

4.5 四种模式的代码对比

光说不练假把式。下面我用一个简单的「按键控制LED」例子,展示四种模式的实现差异。假设按键按下时LED亮,松开时LED灭,但需要消抖。

Moore型实现(C语言伪代码):

typedef enum {
    STATE_IDLE,      // 输出: LED_OFF
    STATE_DEBOUNCE,  // 输出: LED_OFF
    STATE_ON,        // 输出: LED_ON
    STATE_RELEASE    // 输出: LED_ON
} moore_state_t;

void moore_fsm(moore_state_t *state, uint8_t key_input) {
    switch(*state) {
        case STATE_IDLE:
            if(key_input == PRESSED) *state = STATE_DEBOUNCE;
            break;
        case STATE_DEBOUNCE:
            if(key_input == PRESSED) *state = STATE_ON;
            else *state = STATE_IDLE;
            break;
        case STATE_ON:
            if(key_input == RELEASED) *state = STATE_RELEASE;
            break;
        case STATE_RELEASE:
            if(key_input == RELEASED) *state = STATE_IDLE;
            else *state = STATE_ON;
            break;
    }
    // 输出完全由状态决定
    LED_OUTPUT = (*state == STATE_ON || *state == STATE_RELEASE) ? LED_ON : LED_OFF;
}

Mealy型实现(更少状态):

typedef enum {
    STATE_IDLE,  // 输出由输入决定
    STATE_ON
} mealy_state_t;

void mealy_fsm(mealy_state_t *state, uint8_t key_input) {
    uint8_t output = LED_OFF;
    
    switch(*state) {
        case STATE_IDLE:
            if(key_input == PRESSED) {
                *state = STATE_ON;
                output = LED_ON;  // 状态+输入决定输出
            }
            break;
        case STATE_ON:
            if(key_input == RELEASED) {
                *state = STATE_IDLE;
                output = LED_OFF;
            } else {
                output = LED_ON;
            }
            break;
    }
    LED_OUTPUT = output;  // 组合逻辑输出,可能有毛刺
}

你看出来了吗? Mealy型只用2个状态就实现了Moore型4个状态的功能。但Mealy型的输出是组合逻辑,如果按键信号有抖动,LED可能会闪烁。Moore型因为输出是寄存器同步的,就不会有这个问题。

4.6 分层状态机的代码结构

HSM的实现稍微复杂一些,但结构很清晰。核心是父状态处理公共逻辑,子状态处理特殊逻辑。

// 父状态:处理超时和错误
void parent_state_handler(event_t event) {
    switch(event.type) {
        case EVENT_TIMEOUT:
            // 所有子状态共享的超时处理
            transition_to(STATE_IDLE);
            break;
        case EVENT_ERROR:
            // 所有子状态共享的错误处理
            transition_to(STATE_ERROR);
            break;
        default:
            // 交给子状态处理
            child_state_handler(event);
            break;
    }
}

// 子状态:只关心自己的逻辑
void child_state_handler(event_t event) {
    switch(event.type) {
        case EVENT_DATA_RECEIVED:
            // 子状态特有的处理
            process_data(event.data);
            break;
        case EVENT_SEND_COMPLETE:
            transition_to(STATE_WAITING);
            break;
    }
}

我曾经犯过的错:第一次用HSM时,我在子状态里直接处理了超时事件,结果父状态的超时处理被覆盖了。后来才明白,子状态应该先调用父状态的处理函数,再处理自己的逻辑。顺序搞反了,整个状态机就乱套了。

4.7 选型总结

最后,我整理了一个选型速查表,方便你实际项目中快速决策:

场景 推荐模式 理由
按键消抖、LED控制 Moore型 输出稳定,不需要快速响应
高速数据解析(SPI、I2C) Mealy型 状态少,响应快
通信协议控制(Modbus、CAN) 混合型 控制信号稳定,数据信号快速
复杂设备管理(打印机、基站) 分层HSM 状态多,层次清晰,易于维护
FPGA/ASIC设计 Moore型为主 时序容易满足,综合结果好

嗯,这四种模式就讲到这里。记住,没有的好坏,只有合不合适的场景。你想想看,一个只有5个状态的简单协议,非要用HSM,那就是杀鸡用牛刀。反过来,一个50个状态的复杂系统,用扁平状态机,那就是给自己挖坑。