这篇解决一个问题设备软件里几乎每个模块都是while switch写着写着变成面条。判断该放哪、失败该不该改步、结批该塞哪个case。先把每个状态的形状固定下来再谈要不要上框架。一、设备流程为什么天然是状态机设备不是一次性函数。搬运台车、手臂、料道、测试台都是「做完 A 再做 B中间还要等、还可能改道」。一条很常见的物理路径去取料位 → 取料 → 去工作位 → 等对方接手 → 去出料位 → 再回到取料位中间还会插例外这盘是空盖盘取完直接出不进工作位。已经开始结批车上还有料跳过工作位直接出。工位被别人占着这一拍先别动下一圈再抢。所以设备控制几乎都会落成状态机当前在哪一步、这一步能不能开始、做完去哪、失败怎么办。问题不在「用不用状态机」而在 一个case里把所有事情搅在一起。二、面条式状态机为什么难改典型写法是一个大循环套一个大switchwhile (running) { Sleep(1); switch (step) { case 某步: if (资源锁失败) break; if (结批) { step 另一步; break; } ret 动作(); if (ret 失败) { Alarm(); break; } if (特殊盘) step A; else step B; break; } }一个case里同时出现五件事挡门现在能不能开始结批改道真正动作失败处理成功后选路看起来省事维护时会痛在这几处不知道新规则该塞哪个case。同样是break有的表示「下一圈重试」有的表示「报警停住」有的表示「已经改道」。动作做一半突然跳步气缸还顶着、锁还占着。新人不敢改因为改一步不知道会不会把别的路径带崩。这就是面条式状态机流程能跑结构不可读。三、最小可用手法每个 case 只做三件事先不换框架、不拆类。 只规定每个状态内部的形状挡门 → 动作 → 选路case 某步: { // ① 挡门这一步现在能不能开始 if (资源没抢到) break; // 重试不改步 if (该跳过的前置条件) { step 下一站; break; } // 改道 if (结批且本步不该开始) { 收尾; break; } // 拦截 // ② 动作这一步真正要干的活 auto ret 执行动作(); if (!ret.IsOK()) { Alarm(ret); StopMachine(); break; // 失败通常不改步停在原处 } // ③ 选路成功后下一步去哪 if (特殊分支) step 特殊下一站; else step 默认下一站; break; }记住一句就够没开始 → 挡门做砸了 → 失败处理做完了 → 选路。四、一个新 if 该放哪四问清单以后每加一个判断按这个顺序问问是放哪这是让设备「现在先别动」吗资源没抢到、对方没就绪、结批后不该再取新料① 挡门这是动作执行失败吗轴超时、真空失败、气缸不到位② 动作中间这是动作成功后「下一步去哪」吗空盖盘走捷径、正常盘去工作位③ 选路这是和所有步都无关的全局条件吗急停、整机已结批、暂停while循环头不要凭感觉往最近的case里塞。位置错了比条件写错更难查。五、全局闸门不要进 case急停、整机结批完成、暂停属于「不管现在哪一步都该先处理」。放到每个case里会重复、会漏。while (running) { Sleep(1); if (急停) { 停机; continue; } if (整机已结批) { Stop(); continue; } if (!允许运行) { continue; } switch (step) { ... } }循环头管「整机还让不让跑」。case里只管「这一步自己的事」。六、资源锁不是业务动作设备里很多工位是共享的顶升、Buffer、压台。常见写法是占用协议bool SetUsedBy(Actor* who) { lock_guardmutex lk(mtx); if (owner nullptr || owner who) { owner who; return true; } return false; }含义只有三句没人占用 → 登记为自己成功本来就是自己 → 成功允许重入别人在用 → 失败本轮不要继续动锁失败和动作失败不是一类事。锁失败动作失败含义现在还不能开始已经开始但没做成处理break留原步下圈再抢报警通常停机改不改步不改通常不改停在原步方便排查最常见的错法锁失败也Alarm或者锁失败直接跳到下一步。前者误报后者还没干活就假装做完了。七、结批不是一个 if而是一类规则「结批」在状态机里至少有几种完全不同的意思不能写成同一个判断到处粘贴。类型放哪做什么改不改步整机已经结完循环头停本模块不改空车且不该再取新料挡门标记本模块结批完成、放锁不改车上还有料但工作位不必再去挡门改道跳过工作位去出料改道正在等对方接手挡门副作用催对方结束自己先等通常不改以后加结批规则先问这是总闸、拦截、改道还是催下游 再决定放循环头还是某个case的挡门段。不要在动作做到一半时突然if (结批) step ...。动作做一半就跳最容易留下锁没放、气缸还顶着、真空还开着。八、一个 case 里塞了多个动作就该拆子状态取料经常不是一个动作而是一串到取料位 → 顶升 → 建真空 → 分盘/松夹 → 下降全写在一个case里会有三个后果失败时不知道卡在顶升还是真空。暂停后再进来会从头执行可能重复顶升或重复吸真空。界面只能显示「正在取料」调试信息太粗。做法是拆子状态enum class PickSub { MoveToPos, JackUp, BuildVacuum, Separate, JackDown, Done }; case Pick: { switch (sub) { case PickSub::JackUp: if (cylinderInPosition) sub PickSub::BuildVacuum; break; case PickSub::BuildVacuum: if (vacuumOk) sub PickSub::Separate; break; // ... } if (sub PickSub::Done) step NextStation; break; }父状态表示「我在取料」子状态表示「取料进行到哪一小步」。失败、暂停、恢复都有落点。原则一个 case 对应一个可命名的物理状态或原子动作。 一个名字里说不清就该拆。九、改步只能出现在少数位置一个状态里step ...最多三类挡门改道这一步确定不该做直接去另一站。失败后不改步停在原处方便看现场、看日志。成功后选路给一个默认下一站特殊分支另写。反例一个case末尾四五处赋值没有默认下一站。读代码的人要靠猜「哪条路才是主干」。正例// 成功后选路 step 默认下一站; if (空盖盘) step 出料; if (结批且有盘) step 出料;主干清晰例外是例外。十、大厂怎么写和现在该不该上框架业界常见做法场景常见选择C 设备控制HFSM2、Boost.Statechart、Boost.MSM、sml.NETStateless模型驱动Stateflow、SCXML 画图生成代码PLCSFC / 状态图自研平台事件队列 状态注册 转移注册框架带来的变化是真实的面条式框架式step是整数状态是类型转移散落在ifchangeTo显式转移进入/退出靠人记框架调enter/exit结批是到处if可以升成父状态历史靠自己记父状态切换可重置子状态但老项目不要一上来全量迁框架。原因也很实际编译器、模板报错、团队习惯可能跟不上。一台设备往往有十几套状态机全迁等于几个月加一批新 bug。框架解决的是「结构」解决不了「判断放错位置」。判断放错换框架一样乱。更稳的路径阶段做法现在老模块用「挡门-动作-选路」整理不换框架新模块新写的台车/手臂可以直接上层次状态机或自研小框架提公共能力资源占用包成 Guard动作结果包成统一ActionResult平台化结构稳定后再决定要不要统一框架先把每个case写干净再谈框架。 顺序反了是用新语法写旧面条。十一、一份可直接贴进规范的 checklist每个状态机都应满足一个case对应一个可命名的物理状态或动作。改步只出现在跳过本步的挡门改道、失败后不改、成功后选路。急停、整机结批、暂停放到while循环头。资源锁放在case最开头失败只重试。失败先报警/停机再决定是否改步默认不改步。一个case里不要串两个完整动作。结批先分类总闸 / 拦截 / 改道 / 催下游再落点。常见错误对照动作中间写结批跳步 → 资源遗留。一个case两个动作 → 失败不知道卡在哪。全局闸复制进每个case→ 漏改。锁失败和动作失败同样报警 → 误报。下一步来源太多且没有默认站 → 读不懂主干。十二、可复用结论设备状态机的核心不是框架而是 每个状态内部形状统一挡门、动作、选路。新判断先问四句现在别动、做砸了、做完去哪、是不是全局闸。资源锁失败是重试动作失败是报警两类break不能混。结批不是一个 bool而是总闸、拦截、改道、催下游四类规则。一步里有子流程就拆子状态避免暂停恢复后重复动作。老项目先整理case新项目再上层次状态机不要用框架掩盖结构混乱。设备软件的状态机追求的不是「看起来高级」而是改规则时知道改哪、失败时知道停在哪、新人敢动。while switch可以很差也可以很干净。差在把所有 if 倒进一个case干净在先固定三段再让框架成为可选项而不是第一步。