3. 状态机建模方法:Moore型与Mealy型状态机的区别,状态机设计模式(单例、工厂),状态机代码结构(switch-case、状态表、面向对象)

状态机这东西,说白了就是嵌入式系统的“大脑”。你想想看,一个按键按下,LED是亮还是灭?一个串口数据包来了,系统是解析还是丢弃?这些决策背后,都是状态机在起作用。

我做了十几年嵌入式,见过太多人把状态机写得一团糟。要么是if-else嵌套到天荒地老,要么是状态跳转逻辑散落在各个角落。今天咱们就把状态机建模这件事彻底讲透。

3.1 Moore型与Mealy型状态机:核心区别

这两种状态机,名字听着高大上,其实区别就一句话:输出依赖什么。

特性 Moore型 Mealy型
输出依赖 仅依赖当前状态 依赖当前状态 + 输入
输出时机 状态切换后立即生效 输入变化时立即响应
状态数量 通常较多(需要额外状态区分输出) 通常较少(输出由输入组合决定)
响应速度 慢一拍(需要时钟沿同步) 快(组合逻辑直接输出)
安全性 高(输出稳定,不易受毛刺干扰) 低(输入毛刺可能直接传递到输出)

我的经验之谈:在工业控制项目中,我几乎只用Moore型。为什么?因为安全。你想想,一个PLC控制电机正反转,如果输出因为输入抖动而乱跳,那后果...嗯,你懂的。Mealy型更适合高速数据通路,比如SPI从机状态机,需要立即响应主机的时钟边沿。

来看个具体例子。假设我们要设计一个“按键双击检测”的状态机:

// Moore型实现
typedef enum {
    IDLE,       // 空闲,输出=0
    FIRST_PRESS, // 第一次按下,输出=0
    WAIT_RELEASE, // 等待释放,输出=0
    DOUBLE_CLICK // 双击确认,输出=1
} MooreState;

// Mealy型实现
typedef enum {
    IDLE,       // 空闲
    FIRST_PRESS, // 第一次按下
    WAIT_RELEASE // 等待释放
} MealyState;
// 输出 = (当前状态==WAIT_RELEASE && 输入==PRESSED) ? 1 : 0

看到了吗?Moore型多了一个DOUBLE_CLICK状态,专门用来输出“1”。而Mealy型在WAIT_RELEASE状态下,只要检测到PRESSED输入,立即输出“1”。

避坑指南:我曾经在一个无线传感器网络项目中,用了Mealy型状态机做数据包解析。结果现场环境电磁干扰严重,输入信号毛刺导致状态机误触发,输出了一堆错误数据包。后来改成Moore型,加了一个“数据稳定”状态,问题才解决。所以,如果你的输入信号质量不可控,优先选Moore型。

3.2 状态机设计模式:单例与工厂

写嵌入式代码,最怕的就是“全局变量满天飞”。状态机也不例外。我推荐两种设计模式来管理状态机实例。

3.2.1 单例模式

适合系统中只有一个实例的状态机,比如系统主控状态机。

// 单例模式实现
typedef struct {
    State currentState;
    uint32_t timer;
    void (*entryAction)(void);
    void (*exitAction)(void);
} StateMachine;

static StateMachine instance = {0};

StateMachine* SM_GetInstance(void) {
    return &instance;
}

void SM_Init(void) {
    instance.currentState = STATE_IDLE;
    instance.timer = 0;
    // 执行初始状态的entry action
    if (instance.entryAction) instance.entryAction();
}

我个人习惯把单例的获取函数命名为GetInstance,这样其他模块调用时语义清晰。注意,单例模式要配合初始化顺序控制,我曾经见过有人没初始化就调用状态机,结果entryAction是个野指针,直接HardFault。

3.2.2 工厂模式

适合需要动态创建多个同类状态机的场景。比如一个网络服务器,每个连接都需要一个独立的状态机实例。

// 工厂模式实现
typedef struct {
    StateMachine* (*create)(uint8_t id);
    void (*destroy)(StateMachine* sm);
} SMFactory;

StateMachine* SMFactory_Create(SMFactory* factory, uint8_t id) {
    StateMachine* sm = (StateMachine*)malloc(sizeof(StateMachine));
    if (sm) {
        sm->id = id;
        sm->currentState = STATE_INIT;
        sm->timer = 0;
        // 从配置表加载参数
        sm->config = &configTable[id];
    }
    return sm;
}

void SMFactory_Destroy(SMFactory* factory, StateMachine* sm) {
    if (sm) {
        // 执行清理动作
        sm->exitAction = NULL;
        free(sm);
    }
}

小技巧:在资源受限的MCU上,工厂模式最好配合对象池使用,而不是malloc/free。预先分配固定数量的状态机实例,用位图标记空闲/占用。这样既避免了内存碎片,又保证了确定性。

3.3 状态机代码结构:三种主流实现

代码结构的选择,直接影响可维护性和性能。我见过三种主流实现方式,各有优劣。

3.3.1 switch-case结构

最直观的实现方式,适合状态少于20个的简单状态机。

void SM_ProcessEvent(Event* evt) {
    switch (currentState) {
        case STATE_IDLE:
            if (evt->type == EVT_START) {
                // 执行exit action
                SM_ExitAction(STATE_IDLE);
                currentState = STATE_RUNNING;
                // 执行entry action
                SM_EntryAction(STATE_RUNNING);
            }
            break;
            
        case STATE_RUNNING:
            if (evt->type == EVT_STOP) {
                SM_ExitAction(STATE_RUNNING);
                currentState = STATE_IDLE;
                SM_EntryAction(STATE_IDLE);
            } else if (evt->type == EVT_TIMEOUT) {
                SM_ExitAction(STATE_RUNNING);
                currentState = STATE_ERROR;
                SM_EntryAction(STATE_ERROR);
            }
            break;
            
        // ... 更多状态
    }
}

优点:代码直白,调试方便。缺点:状态多了以后,一个函数几百行,维护起来想哭。

我曾经踩过的坑:在一个空调控制项目中,我写了2000多行的switch-case状态机。后来要加一个“除湿模式”,我改了3天,改了3次才改对。因为状态跳转逻辑散落在各个case里,牵一发而动全身。从那以后,超过10个状态我就换方案了。

3.3.2 状态表结构

用二维表描述状态跳转,数据驱动,代码量少。

// 状态表定义
typedef struct {
    State   nextState;
    Action  action;
} Transition;

// 状态表:[当前状态][事件类型] -> {下一状态, 动作}
static const Transition stateTable[STATE_COUNT][EVENT_COUNT] = {
    // STATE_IDLE
    [STATE_IDLE] = {
        [EVT_START]   = {STATE_RUNNING, ACTION_START_MOTOR},
        [EVT_STOP]    = {STATE_IDLE,    ACTION_NONE},
        [EVT_TIMEOUT] = {STATE_IDLE,    ACTION_NONE},
    },
    // STATE_RUNNING
    [STATE_RUNNING] = {
        [EVT_START]   = {STATE_RUNNING, ACTION_NONE},
        [EVT_STOP]    = {STATE_IDLE,    ACTION_STOP_MOTOR},
        [EVT_TIMEOUT] = {STATE_ERROR,   ACTION_ALARM},
    },
    // STATE_ERROR
    [STATE_ERROR] = {
        [EVT_START]   = {STATE_ERROR,   ACTION_NONE},
        [EVT_STOP]    = {STATE_IDLE,    ACTION_RESET},
        [EVT_TIMEOUT] = {STATE_ERROR,   ACTION_NONE},
    },
};

void SM_ProcessEvent(Event* evt) {
    Transition t = stateTable[currentState][evt->type];
    if (t.action != ACTION_NONE) {
        ExecuteAction(t.action);
    }
    currentState = t.nextState;
}

优点:状态跳转逻辑集中在一张表里,增删改查一目了然。缺点:事件类型和状态类型必须是连续的整数,扩展性受限。

3.3.3 面向对象结构

用函数指针实现多态,每个状态是一个独立的对象。

// 状态接口
typedef struct StateVtable {
    void (*entry)(void);
    void (*exit)(void);
    State* (*handleEvent)(Event* evt);
} StateVtable;

// 具体状态
static StateVtable idleState = {
    .entry = Idle_Entry,
    .exit  = Idle_Exit,
    .handleEvent = Idle_HandleEvent,
};

static StateVtable runningState = {
    .entry = Running_Entry,
    .exit  = Running_Exit,
    .handleEvent = Running_HandleEvent,
};

// 状态机核心
void SM_ProcessEvent(Event* evt) {
    StateVtable* next = currentState->handleEvent(evt);
    if (next != currentState) {
        currentState->exit();
        currentState = next;
        currentState->entry();
    }
}

优点:每个状态独立编译,适合大型项目多人协作。缺点:函数指针调用有额外开销,代码量较大。

我的推荐:

  • 小型项目(<10个状态):switch-case,简单直接
  • 中型项目(10-30个状态):状态表,数据驱动,易于维护
  • 大型项目(>30个状态):面向对象,模块化,适合团队协作

3.4 知识结构总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作状态机建模的“地图”。

状态机建模知识结构图 状态机建模方法总览 Moore型 vs Mealy型 设计模式 代码结构 输出仅依赖状态 输出依赖状态+输入 单例模式 工厂模式 switch-case 状态表 状态多 输出稳定 适合安全场景 状态少 响应快 适合高速数据 全局唯一实例 适合系统主控 注意初始化顺序 动态创建实例 适合多连接场景 配合对象池使用 直观易调试 适合<10个状态 状态多时难维护 数据驱动 适合10-30状态 扩展性受限 核心原则:根据项目规模选择合适方案,没有银弹 Moore型/Mealy型选择 → 设计模式选择 → 代码结构选择 三者层层递进,共同构成完整的状态机建模方案

最后说一句:状态机建模没有银弹。我见过有人用switch-case写出了优雅的代码,也见过有人用面向对象写出了灾难。关键不是选什么技术,而是理解你的系统——状态数量、响应要求、团队规模、维护周期,这些因素综合起来,才能选出最适合的方案。