设备状态机怎么写才不乱:挡门、动作、选路,比框架更先要学会
这篇解决一个问题:设备软件里几乎每个模块都是while + switch,写着写着变成面条。判断该放哪、失败该不该改步、结批该塞哪个case。先把每个状态的形状固定下来,再谈要不要上框架。
一、设备流程为什么天然是状态机
设备不是一次性函数。搬运台车、手臂、料道、测试台,都是「做完 A 再做 B,中间还要等、还可能改道」。
一条很常见的物理路径:
去取料位 → 取料 → 去工作位 → 等对方接手 → 去出料位 → 再回到取料位
中间还会插例外:
- 这盘是空盖盘,取完直接出,不进工作位。
- 已经开始结批,车上还有料,跳过工作位直接出。
- 工位被别人占着,这一拍先别动,下一圈再抢。
所以设备控制几乎都会落成状态机:当前在哪一步、这一步能不能开始、做完去哪、失败怎么办。
问题不在「用不用状态机」,而在 一个case里把所有事情搅在一起。
二、面条式状态机为什么难改
典型写法是一个大循环套一个大switch:
while (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 该放哪:四问清单
以后每加一个判断,按这个顺序问:
| 问 | 是 | 放哪 |
|---|---|---|
这是让设备「现在先别动」吗? | 资源没抢到、对方没就绪、结批后不该再取新料 | ① 挡门 |
这是动作执行失败吗? | 轴超时、真空失败、气缸不到位 | ② 动作中间 |
这是动作成功后「下一步去哪」吗? | 空盖盘走捷径、正常盘去工作位 | ③ 选路 |
这是和所有步都无关的全局条件吗? | 急停、整机已结批、暂停 |
|
不要凭感觉往最近的case里塞。位置错了,比条件写错更难查。
五、全局闸门不要进 case
急停、整机结批完成、暂停,属于「不管现在哪一步,都该先处理」。放到每个case里会重复、会漏。
while (running) { Sleep(1); if (急停) { 停机; continue; } if (整机已结批) { Stop(); continue; } if (!允许运行) { continue; } switch (step) { ... } }循环头管「整机还让不让跑」。case里只管「这一步自己的事」。
六、资源锁不是业务动作
设备里很多工位是共享的:顶升、Buffer、压台。常见写法是占用协议:
bool SetUsedBy(Actor* who) { lock_guard<mutex> lk(mtx); if (owner == nullptr || owner == who) { owner = who; return true; } return false; }含义只有三句:
- 没人占用 → 登记为自己,成功
- 本来就是自己 → 成功(允许重入)
- 别人在用 → 失败,本轮不要继续动
锁失败和动作失败不是一类事。
| 锁失败 | 动作失败 | |
|---|---|---|
含义 | 现在还不能开始 | 已经开始,但没做成 |
处理 |
| 报警,通常停机 |
改不改步 | 不改 | 通常不改,停在原步方便排查 |
最常见的错法:锁失败也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 |
.NET | Stateless |
模型驱动 | Stateflow、SCXML 画图生成代码 |
PLC | SFC / 状态图 |
自研平台 | 事件队列 + 状态注册 + 转移注册 |
框架带来的变化是真实的:
| 面条式 | 框架式 |
|---|---|
| 状态是类型 |
转移散落在 |
|
进入/退出靠人记 | 框架调 |
结批是到处 | 可以升成父状态 |
历史靠自己记 | 父状态切换可重置子状态 |
但老项目不要一上来全量迁框架。原因也很实际:
- 编译器、模板报错、团队习惯可能跟不上。
- 一台设备往往有十几套状态机,全迁等于几个月加一批新 bug。
- 框架解决的是「结构」,解决不了「判断放错位置」。判断放错,换框架一样乱。
更稳的路径:
| 阶段 | 做法 |
|---|---|
现在 | 老模块用「挡门-动作-选路」整理,不换框架 |
新模块 | 新写的台车/手臂可以直接上层次状态机或自研小框架 |
提公共能力 | 资源占用包成 Guard,动作结果包成统一 |
平台化 | 结构稳定后,再决定要不要统一框架 |
先把每个case写干净,再谈框架。 顺序反了,是用新语法写旧面条。
十一、一份可直接贴进规范的 checklist
每个状态机都应满足:
- 一个
case对应一个可命名的物理状态或动作。 - 改步只出现在:跳过本步的挡门改道、失败后不改、成功后选路。
- 急停、整机结批、暂停放到
while循环头。 - 资源锁放在
case最开头,失败只重试。 - 失败先报警/停机,再决定是否改步;默认不改步。
- 一个
case里不要串两个完整动作。 - 结批先分类(总闸 / 拦截 / 改道 / 催下游),再落点。
常见错误对照:
- 动作中间写结批跳步 → 资源遗留。
- 一个
case两个动作 → 失败不知道卡在哪。 - 全局闸复制进每个
case→ 漏改。 - 锁失败和动作失败同样报警 → 误报。
- 下一步来源太多且没有默认站 → 读不懂主干。
十二、可复用结论
- 设备状态机的核心不是框架,而是 每个状态内部形状统一:挡门、动作、选路。
- 新判断先问四句:现在别动、做砸了、做完去哪、是不是全局闸。
- 资源锁失败是重试,动作失败是报警;两类
break不能混。 - 结批不是一个 bool,而是总闸、拦截、改道、催下游四类规则。
- 一步里有子流程,就拆子状态,避免暂停恢复后重复动作。
- 老项目先整理
case,新项目再上层次状态机;不要用框架掩盖结构混乱。
设备软件的状态机追求的不是「看起来高级」,而是改规则时知道改哪、失败时知道停在哪、新人敢动。while + switch可以很差,也可以很干净。差在把所有 if 倒进一个case;干净在先固定三段,再让框架成为可选项,而不是第一步。
