|
业务状态一多,if-else 很快变成条件网:启动、停止、过温、低电量、过流、复位互相缠在一起。FSM 的价值不是炫技,而是把状态、事件、转换规则、入口动作、退出动作集中管理。小业务用 switch-case 就够了,状态和事件明显变多时,再用表驱动 FSM 接管复杂度。 --- 一、if-else失控:业务状态被flag拆碎了 很多固件都是从几行判断开始的: if (powerOn) { if (startCommand) { // 启动电机 } if (stopCommand) { // 停止电机 }}第一版没问题。问题是量产需求会继续加: 温度超过 80°C,禁止启动 电量低于 20%,禁止启动 运行中过流,立刻进故障态 故障次数少于 3 次,允许复位 任意状态收到掉电事件,都回到关机态 继续堆 if-else,逻辑会变成这样: if (powerOn) { if (!fault) { if (startCommand && temperatureOk && batteryOk) { // 启动电机 } else if (stopCommand) { // 停止电机 } else if (overCurrent) { // 进入故障态 } } else { if (resetCommand && faultCount < 3U) { // 清除故障 } else if (powerOff) { // 进入关机态 } }}危险点不是缩进难看,而是状态被多个 flag 拆碎了。 问题 | 现场表现 | 后果 | | 状态分散 | powerOn 、motorRunning、fault 到处改 | 当前模式不清楚 | | 组合爆炸 | 每加一个条件,多一批路径 | 测试覆盖困难 | | 非法转换难拦 | 故障态还能启动 | 偶发问题难复现 | | 动作分散 | 清速度散落在多个分支 | 某条路径容易漏 | | 审查靠脑补 | 代码像流程迷宫 | 新人接手慢 |
FSM 要解决的就是:把“当前状态、收到事件、能否转换、切到哪里、切换前后做什么”集中表达。 --- 二、FSM建模:先把状态和事件说清楚 以电机控制为例,先定义 4 个状态: 状态 | 含义 | | FSM_STATE_OFF | 关机态 | | FSM_STATE_READY | 待机态 | | FSM_STATE_RUNNING | 运行态 | | FSM_STATE_FAULT | 故障态 |
再定义 6 个事件: 事件 | 含义 | | FSM_EVENT_POWER_ON | 上电 | | FSM_EVENT_START | 启动 | | FSM_EVENT_STOP | 停止 | | FSM_EVENT_OVER_CURRENT | 过流 | | FSM_EVENT_RESET | 复位 | | FSM_EVENT_POWER_OFF | 掉电 |
核心流程先画清楚: 图上能直接看出两件事:合法路径是什么,异常路径在哪里。代码只是这张图的落地。 --- 三、struct+函数指针:把状态规则做成表 FSM 的核心是转换表。每一行都描述一次“状态 + 事件 -> 新状态”。 typedef struct { FsmState_t current; // 当前状态 FsmEvent_t event; // 收到事件 FsmState_t next; // 目标状态 GuardFunction_t guard; // 判断是否允许切换 ActionFunction_t onExit; // 离开旧状态前执行 ActionFunction_t onEntry;// 进入新状态后执行} FsmTransition_t;转换表可以写成这种结构: static const FsmTransition_t motorTransitions[] = { // 当前状态 事件 目标状态 条件函数 退出动作 进入动作 { FSM_STATE_OFF, FSM_EVENT_POWER_ON, FSM_STATE_READY, NULL, NULL, NULL }, { FSM_STATE_READY,FSM_EVENT_START, FSM_STATE_RUNNING,guardCanStart, NULL, entryRunning }, { FSM_STATE_RUNNING,FSM_EVENT_STOP, FSM_STATE_READY, NULL, exitRunning, NULL }, { FSM_STATE_RUNNING,FSM_EVENT_OVER_CURRENT,FSM_STATE_FAULT,NULL, exitRunning, entryFault }, { FSM_STATE_FAULT,FSM_EVENT_RESET, FSM_STATE_READY, guardFaultRecoverable,NULL, NULL },};这张表的价值很直接:如果没有 FAULT + START -> RUNNING 这一行,故障态收到启动事件就不会启动。 --- 四、完整demo:保留代码结构,省掉无关细节 完整 demo 可以拆成 5 块:状态、事件、上下文、转换表、事件分发。 typedef enum { FSM_STATE_OFF, FSM_STATE_READY, FSM_STATE_RUNNING, FSM_STATE_FAULT} FsmState_t; // 定义设备主状态typedef enum { FSM_EVENT_POWER_ON, FSM_EVENT_START, FSM_EVENT_STOP, FSM_EVENT_OVER_CURRENT, FSM_EVENT_RESET, FSM_EVENT_POWER_OFF} FsmEvent_t; // 定义外部事件typedef struct { int32_t motorSpeedRpm; int32_t temperatureC; int32_t batteryPercent; uint32_t faultCount;} MotorContext_t; // 保存业务数据,避免散落成全局变量static bool guardCanStart(Fsm_t *fsm){ // 判断电机是否允许从 READY 切到 RUNNING return (fsm->context->temperatureC < 80) && (fsm->context->batteryPercent >= 20);}static void exitRunning(Fsm_t *fsm){ // 离开运行态时统一清速度 fsm->context->motorSpeedRpm = 0;}事件分发只做一件事:查表,然后按规则执行。 void fsmDispatch(Fsm_t *fsm, FsmEvent_t event){ // 遍历转换表,找到 currentState + event 对应的规则 // guard 不通过:拒绝切换 // 有 onExit:先执行退出动作 // 更新 state = next // 有 onEntry:再执行进入动作 // 没有匹配规则:交给当前状态兜底处理}测试场景也按状态转换来设计: 场景 | 事件序列 | 关注点 | 正常启停 | POWER_ON -> START -> STOP | 基础路径 | 高温拦截 | temperatureC=95 -> START | guard 是否生效 | 过流故障 | START -> OVER_CURRENT -> RESET | 故障进入和复位 | 复位失败 | faultCount=3 -> RESET | 故障次数限制 |
--- 五、框架拆解:复杂度被压在哪里 5.1 状态枚举:只允许一个主状态 powerOn、motorRunning、fault 这些 flag 可以保留,但不要让它们承担“设备主模式”的职责。 powerOn = falsemotorRunning = truefault = true这种组合很难解释。FSM 用 currentState 表示主状态,同一时刻只允许一个主模式,很多非法组合自然消失。 5.2 转换表:合法路径集中审查 转换表是业务规则说明书。 FAULT + START -> RUNNING // 表里没有这行,所以故障态不能启动这不是靠某个程序员记得加 if (!fault),而是靠结构把非法路径挡在外面。 5.3 guard函数:条件不满足就不切换 guardCanStart 只判断能不能启动。 bool guardCanStart(void){ // 温度和电量都满足,才允许 START -> RUNNING return (temperatureC < 80) && (batteryPercent >= 20);}以后要加“盖板关闭才允许启动”,只改 guard,不改主调度。 5.4 entry/exit动作:状态边界统一收口 运行态离开时要清速度。靠 if-else 写,STOP、POWER_OFF、OVER_CURRENT 都要记得清;FSM 只要把清速度挂到 exitRunning。 void exitRunning(void){ // 离开 RUNNING 时统一清速度 motorSpeedRpm = 0;}只要离开 RUNNING,就统一执行退出动作。 --- 六、if-else vs FSM:维护差距在量产后出现 维度 | 漫天if-else | struct+函数指针FSM | 状态表达 | 多个 flag 拼出来 | 单一状态枚举 | 合法路径 | 藏在分支里 | 转换表集中表达 | 非法事件 | 容易漏拦截 | 没有表项就不转换 | 启停动作 | 散落在多个分支 | entry/exit 统一处理 | 测试方式 | 按代码路径猜 | 按状态转换表覆盖 | 代码审查 | 看嵌套分支 | 看转换表和 guard | 扩展成本 | 每加功能都改主逻辑 | 多数情况只加表项和动作 |
FSM 的收益不是“代码高级”,而是需求越改越多时,业务主循环不会越改越乱。 --- 七、嵌入式落地:别把FSM写成新负担 这套 struct + 函数指针 更适合状态超过 4 个、事件超过 5 个、有明确非法转换、有 entry/exit 动作,或者同类设备要创建多个实例的场景。 内存:表驱动不等于动态分配。 状态表、事件表、转换表、函数指针表都可以是 static const。 中断:只投递事件,不跑业务状态机。 中断里记录硬件事件或投递消息,真正的状态切换放到主循环或 RTOS 任务里。 状态数量:会停留才叫状态。 会等待事件、会影响后续行为,才适合建成状态;只执行一下就结束,多半只是动作。 --- 八、常见问题 Q1:函数指针会不会拖慢性能? 业务状态机一般跑在主循环或 RTOS 任务里,事件频率远低于中断和控制环。一次函数指针调用通常不是瓶颈。 Q2:多个电机能不能复用同一套表? 可以。每个电机一个上下文和一个 FSM 实例,共用同一份状态处理函数和转换表。 Q3:状态很多以后怎么办? 先用平面 FSM。只有当某个大状态下面还有子状态,比如运行态下面还有低速、高速、校准,再考虑层次状态机。 Q4:这和设计模式里的状态模式是什么关系? 思路类似,都是把状态相关行为从主流程里拆出来。嵌入式 C 更关注静态内存、可审查函数指针、低运行时开销,所以通常会做成转换表和回调函数。 --- 九、总结:FSM是业务状态的刹车片 嵌入式业务代码失控,往往不是因为 C 语言不够用,而是状态规则没有显式表达。 struct + 函数指针 + 转换表 这套写法,把关键点拆开: 状态枚举:表达设备当前模式 事件枚举:表达外部输入 转换表:表达合法路径 guard函数:拦截不满足条件的转换 entry/exit动作:统一处理状态边界动作 主循环里少一层漫天 if-else,量产维护时就少一堆靠运气排查的问题。 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |