『7x24小时有问必答』
业务状态一多,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 = false

motorRunning = true

fault = 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,量产维护时就少一堆靠运气排查的问题。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码