核心痛点:随着项目变大,全局标志位(Flags)越来越多,代码里充斥着 if (flag1 && !flag2) ... else if (flag3) 的逻辑,牵一发而动全身,改一个 Bug 冒出来三个 Bug。
解决方案:将系统抽象为 状态 (State) + 事件 (Event) -> 动作 (Action) + 新状态。
1. 初阶:Switch-Case 法 (最常见)
这是大多数人入门 FSM 的写法。以一个简单的“电子门锁”为例。 状态:LOCKED (锁定), UNLOCKED (解锁), ALARM (报警)。
typedef enum { STATE_LOCKED, STATE_UNLOCKED, STATE_ALARM } State_t;
State_t CurrentState = STATE_LOCKED;
void FSM_Run(Event_t event) {
switch (CurrentState) {
case STATE_LOCKED:
if (event == EVENT_PASSWORD_CORRECT) {
Unlock_Door();
CurrentState = STATE_UNLOCKED; // 状态迁移
} else if (event == EVENT_TAMPER) {
CurrentState = STATE_ALARM;
}
break;
case STATE_UNLOCKED:
if (event == EVENT_TIMEOUT) {
Lock_Door();
CurrentState = STATE_LOCKED;
}
break;
// ... 省略 ALARM 处理
}
}
-
优点:直观,逻辑简单时很好用。
-
缺点:当状态有 20 个,事件有 10 个时,这个
switch会变成几千行的怪物,维护极其痛苦。
2. 进阶:二维状态表 (空间换时间)
高手为了消除 switch-case 和嵌套 if,会使用 函数指针矩阵。
这也叫“表驱动法 (Table-Driven)”。我们将逻辑抽象为一个二维数组:Table[当前状态][发生事件]。数组中的元素是一个函数指针,指向“该情况下要执行的动作”。
第一步:定义函数指针类型
// 定义状态处理函数的原型:输入参数可选,返回新的状态
typedef State_t (*StateHandler_t)(void);
第二步:实现具体的动作函数
把原本塞在 case 里的逻辑拆分成独立的小函数。
// 1. 锁定状态下,收到正确密码
State_t Action_Unlock(void) {
Unlock_Door();
return STATE_UNLOCKED; // 返回新状态
}
// 2. 锁定状态下,收到拆机信号
State_t Action_Alarm(void) {
Horn_On();
return STATE_ALARM;
}
// 3. 什么都不做 (保持原状态)
State_t Action_DoNothing(void) {
return CurrentState;
}
第三步:构建二维查找表 (The Magic Matrix)
这是 FSM 的灵魂
// 假设有 3 种状态,3 种事件
// 这里的行代表 State,列代表 Event
const StateHandler_t FSM_Table[3][3] = {
// EVENT_PWD_OK EVENT_TIMEOUT EVENT_TAMPER
/* LOCKED */ {Action_Unlock, Action_DoNothing, Action_Alarm},
/* UNLOCKED*/ {Action_DoNothing, Action_Lock, Action_Alarm},
/* ALARM */ {Action_Reset, Action_DoNothing, Action_DoNothing}
};
第四步:驱动引擎 (极简)
无论逻辑多复杂,驱动代码只有一行!
void FSM_Run(State_t state, Event_t event) {
// 越界保护(生产环境必备)
if (state >= MAX_STATE || event >= MAX_EVENT) return;
// 核心:查表 -> 执行 -> 更新状态
// O(1) 的时间复杂度,快如闪电
StateHandler_t handler = FSM_Table[state][event];
CurrentState = handler();
}
3. 这种写法的好处
-
消除分支:代码里没有
if-else,也没有switch。逻辑流就是查表。 -
维护性无敌:
-
如果想修改“锁定状态下收到密码”的行为,只需要去改
Action_Unlock函数。 -
如果想修改逻辑(比如报警后不能直接复位),只需要修改
FSM_Table里的函数指针,完全不动驱动代码。
-
-
可视化:
FSM_Table本身就是一张代码版的“状态转移图”,一眼就能看出系统的逻辑全貌。
4. 结构体状态机 (面向对象风格)
如果你的每个状态除了动作,还有“进入时执行(Entry)”和“退出时执行(Exit)”的需求,可以定义一个结构体:
typedef struct {
void (*Entry)(void); // 进入该状态时跑一次
void (*Run)(Event_t evt); // 状态持续期间跑
void (*Exit)(void); // 离开该状态时跑一次
} FSM_Node_t;
const FSM_Node_t StateList[] = {
{LCD_On, Menu_Loop, LCD_Off}, // 菜单状态
{Motor_On, PID_Loop, Motor_Off} // 运行状态
};
关键点小结
-
Switch-Case 适合简单逻辑(< 5 个状态)。
-
查表法 (Table-Driven) 是中大型 FSM 的标准解法,它用空间(数组)换取了时间(O(1)查找)和可维护性。
-
思想:把变化的逻辑(Action)和不变的框架(Engine)分离开来。
/***************************************************
* 本文为作者《嵌入式开发基础与工程实践》系列文章之一。
* 关注即可订阅后续内容更新,翻阅往期信息,采用异步推送机制,无需主动轮询。
* 转发本文可视为一次网络广播,有助于更多节点接收该信息。
***************************************************/
1024

被折叠的 条评论
为什么被折叠?



