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 知识结构总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作状态机建模的“地图”。
最后说一句:状态机建模没有银弹。我见过有人用switch-case写出了优雅的代码,也见过有人用面向对象写出了灾难。关键不是选什么技术,而是理解你的系统——状态数量、响应要求、团队规模、维护周期,这些因素综合起来,才能选出最适合的方案。