
做自动化这几年最头疼的莫过于设备动作多了以后程序开始不受控。我之前做一台三工位压装检测设备用欧姆龙NJ系列PLC梯形图写了两三个月程序干到四千多步。看着是能跑可每次改工艺都得从头捋一遍时序稍不留神就动错线圈。后来我痛下决心用Sysmac Studio重写把整个控制流程改成状态机。第一天重构第二天仿真跑通第三天现场切换测试效果完全不一样——程序从四千多步缩到不到一千步逻辑全在一屏CASE里哪个条件没满足、卡在哪个工步一眼就能找到。如果你也在用欧姆龙PLC尤其是NJ/NX/NY系列设备流程又比较像“串行顺序动作”那我强烈建议你试试状态机写法。这篇就是我在Sysmac Studio里第一次完整实践状态机的总结从状态怎么拆、状态值怎么定义到CASE怎么写、定时器怎么复位、报警怎么加都会讲到位。看完你就能在自己的项目里直接套用这套框架。1. 状态机设计与设备流程建模1.1 为什么设备程序会越改越乱传统梯形图在设备简单的时候很顺手一个气缸配两个传感器置位、复位、互锁、自锁几分钟就写完。但设备一复杂麻烦就来了。我见过很多老设备程序气缸动作的置位散落在好几个程序段里复位又放在另外几个地方。现场调试的时候工艺说“这个气缸放行条件改一下”你得全局搜索这个线圈一个一个看上下文改了A处忘了B处是家常便饭。更麻烦的是互锁逻辑A气缸没到原位B气缸不能动这种条件多起来之后梯形图变得又长又绕。状态机解决的是这个根子上的问题把程序从“散点互锁”变成“一维流程”。设备每一个稳定状态都对应一个清晰的位置程序执行到哪一步当前状态变量就是多少不会出现“线圈A已经置位但其实流程还没走到这一步”这种混乱局面。我重构完之后最大的感受是程序不再是“一堆线圈的迷宫”而是一条可以顺着走完的路径。1.2 状态机的核心模型状态、条件、迁移状态机的模型并不复杂核心就三个词状态、条件、迁移。状态设备当前处于哪个稳定位置。比如待机、运行、报警。条件发生迁移需要满足的信号或时间。比如启动按钮按下、气缸到位、定时器到点。迁移满足条件后从当前状态切换到下一个状态的过程。拿自动售货机举例等待投币是一个状态投入硬币并按下商品按钮后进入出货状态出货完成后回到等待状态。整个过程只有“当前站在哪个状态”和“满足什么条件能走到下一个状态”两件事。PLC程序本身是循环扫描机制每一圈扫描都执行同样的逻辑而状态机模型天然是“每个扫描周期判断一次当前状态和迁移条件”两者时间粒度正好匹配。这就是为什么状态机特别适合PLC尤其是欧姆龙NJ/NX这类支持ST结构化文本的高性能控制器。这里有一个关键点状态指的是“稳定状态”不是“正在动作的过程”。比如气缸正在伸出这个动作不是状态“等待气缸伸出到位”才是状态。动作是状态的输出而不是状态本身。很多新手会在这一步搞混结果状态拆得特别碎程序反而更乱。1.3 三工位压装检测设备的状态拆解实战我以当时那台设备为例整个流程是上料→压装→检测→下料。拆完状态之后是这样的状态含义进入条件离开条件0上电初始化上电初始化完成10待机复位完成启动按钮按下20上料启动上料气缸到位30压装上料完成压装完成信号40检测压装完成检测OK50下料检测OK下料完成60循环完成下料完成自动回到待机90错误任一超时/异常复位按钮按下拆解的时候我给自己定了几条规则每个状态必须能停住并等待不能是“一闪而过”的动作。每个状态的离开条件必须明确要么是传感器信号要么是定时器到点要么是按钮。每个状态之内只做这一件事不要把多个工位的动作放在同一个状态里。这套规则看起来简单实际操作中很管用。拆完状态之后整个设备的动作流程在脑子里已经是一条线了后面写代码只是把这条线翻译成ST语言。2. Sysmac Studio工程准备与变量规划2.1 新建工程与ST语言选择在Sysmac Studio里新建工程选择对应的NJ/NX系列CPU这个不用多说。关键是程序POU的语言类型我建议顺序流程逻辑用ST外围IO处理和轴控制可以保留梯形图也可以都用ST。Sysmac Studio支持同一个工程里挂多种语言的POU还能在同一个程序里混合调用这个非常灵活。我的习惯是建三个程序PRG_IO输入输出映射、传感器滤波、按钮处理。PRG_StateMachine状态机主体全部用ST写。PRG_Axis如果需要伺服轴控制单独放轴控制和原点回归逻辑。如果设备没有轴PRG_Axis可以去掉PRG_IO和PRG_StateMachine基本就够了。这样分的好处是状态机程序干净不会看到一堆传感器映射代码调试工步的时候只开PRG_StateMachine不会被外围逻辑干扰。2.2 变量规划状态变量、定时器、IO信号状态机程序里的变量要提前规划清楚不要写到哪里加到哪里否则程序变成一锅粥。我一般分三组。第一组是状态变量。核心就是当前状态变量我习惯叫stStep类型用UDINT。另外会准备一个显示副本uStepDisplay专门给HMI读。第二组是定时器。每个稳定状态对应一个超时定时器类型全是TON。比如tLoadTmr、tPressTmr、tInspectTmr、tUnloadTmr。定时器的时间可以直接写T#5S这种字面量也可以注册成变量方便HMI调整。第三组是IO信号。输入信号以b开头命名比如bStartBtn、bResetBtn、bLoadReady、bPressDone输出信号也用b开头比如bCylinderLoad、bVacuumOn、bPressCylinder。Sysmac Studio支持在全局变量表里集中定义也可以作为程序POU的局部变量。我建议设备级信号放全局状态机内部变量放局部这样复用程序方便。变量命名的规范很重要。状态机程序里变量一多命名不统一的话过两个月自己都看不懂。我的命名规则是前缀b表示BOOLt表示定时器u表示UDINTdw表示DWORD输入信号带后缀In或者约定好是输入变量。2.3 状态值编码规则与扩展性很多人写状态机喜欢用枚举类型Sysmac Studio里也支持自定义枚举。枚举类型确实可读性好在Watch窗口能看到状态名但实际工程里我更喜欢用UDINT加常量值的方式。原因有两个一是HMI读取方便很多触摸屏读枚举类型要额外转换二是状态值之间可以预留空隙方便以后插入新状态。我建议状态值按10步进// 状态常量定义可用全局常量或变量 CST_INIT : UDINT : 0; CST_IDLE : UDINT : 10; CST_LOAD : UDINT : 20; CST_PRESS : UDINT : 30; CST_INSPECT : UDINT : 40; CST_UNLOAD : UDINT : 50; CST_COMPLETE : UDINT : 60; CST_ERROR : UDINT : 90; // 预留70、80给别的扩展状态为什么预留10的间隔因为设备后期很可能要插入新状态。比如原来的上料是直接上料后来工艺要求先定位再上料那就可以加一个25“定位”不用改动原来所有状态的编号。如果你用的是连续的0、1、2、3插一个状态进去后面全要改。这个经验我是踩过坑的在设备调试阶段工艺变更特别频繁状态值预留空间能省很多事。3. 核心代码实现CASE状态机完整示例3.1 定时器驱动避免状态机卡死的第一步梯形图转状态机最容易踩的坑就是定时器不会复位。很多新手会在CASE分支里这样写CST_LOAD: tLoadTmr(IN : TRUE, PT : T#5S); IF tLoadTmr.Q THEN stStep : CST_PRESS; END_IF;表面看没问题实际运行几次就会发现状态跳出CST_LOAD之后再跳回来tLoadTmr的Q还是上一次的TRUE状态机直接跳走了根本不等5秒。原因是TON定时器要检测到IN从FALSE变TRUE才会重新计时而这里IN一直是TRUE。解决办法很简单把定时器的使能条件绑定到当前状态而不是在CASE分支里临时调用。这样状态一离开IN自动变FALSE定时器自动复位状态一进来IN变TRUE定时器从零开始计。代码放在CASE之前// 状态定时器驱动放在CASE语句之前 tLoadTmr(IN : (stStep CST_LOAD), PT : T#5S); tPressTmr(IN : (stStep CST_PRESS), PT : T#8S); tInspectTmr(IN : (stStep CST_INSPECT), PT : T#3S); tUnloadTmr(IN : (stStep CST_UNLOAD), PT : T#5S);这个写法还有个额外好处所有状态超时时间一目了然不用进每个分支里去翻。后期调整超时时间直接在顶部改一行就行。我后面几十个项目都是这么写的没有再出现过定时器残留在状态机里引起误动作的问题。3.2 状态迁移代码CASE语句完整骨架状态迁移是状态机的核心用ST语言的CASE语句来实现最直观。下面是一段可以直接套用的完整示例以刚才的三工位设备为例// // PRG_StateMachine - 状态机主体 // CASE stStep OF // ---------- 上电初始化 ---------- CST_INIT: bCylinderLoad : FALSE; bVacuumOn : FALSE; bPressCylinder : FALSE; bAlarm : FALSE; dwErrorCode : 0; IF bResetBtn OR bStartBtn THEN stStep : CST_IDLE; END_IF; // ---------- 待机 ---------- CST_IDLE: // 输出已经复位等待启动按钮 IF bStartBtn THEN stStep : CST_LOAD; END_IF; // ---------- 上料 ---------- CST_LOAD: bCylinderLoad : TRUE; bVacuumOn : TRUE; IF bLoadReady THEN stStep : CST_PRESS; ELSIF tLoadTmr.Q THEN stStep : CST_ERROR; dwErrorCode : 16#110; // 上料超时 END_IF; // ---------- 压装 ---------- CST_PRESS: bPressCylinder : TRUE; IF bPressDone THEN stStep : CST_INSPECT; ELSIF tPressTmr.Q THEN stStep : CST_ERROR; dwErrorCode : 16#120; // 压装超时 END_IF; // ---------- 检测 ---------- CST_INSPECT: IF bInspectOK THEN stStep : CST_UNLOAD; ELSIF tInspectTmr.Q THEN stStep : CST_ERROR; dwErrorCode : 16#130; // 检测超时或不合格 END_IF; // ---------- 下料 ---------- CST_UNLOAD: IF bUnloadDone THEN stStep : CST_COMPLETE; ELSIF tUnloadTmr.Q THEN stStep : CST_ERROR; dwErrorCode : 16#140; // 下料超时 END_IF; // ---------- 循环完成 ---------- CST_COMPLETE: // 一个循环结束自动回到待机 stStep : CST_IDLE; // ---------- 错误处理 ---------- CST_ERROR: bAlarm : TRUE; // 此时危险输出已经在输出映射中被强制断开 IF bResetBtn THEN bAlarm : FALSE; dwErrorCode : 0; stStep : CST_IDLE; END_IF; // ---------- 非法状态保护 ---------- ELSE stStep : CST_ERROR; dwErrorCode : 16#E00; // 非法状态编号 END_CASE;这段代码有几个细节值得强调。第一每个状态的离开条件用IF/ELSIF的顺序判断先把正常条件写在前面超时条件写在ELSIF里。这样正常流程优先超时只是兜底逻辑清晰。第二错误状态CST_ERROR里我没有写各个输出的复位而是放在后面的输出映射段统一处理。这么做的原因是一旦进入错误状态HSM上的其他输出要立刻断开不能等CASE分支慢慢处理。输出映射段里用“输出等于状态”的写法可以天然保证。第三ELSE分支处理非法状态非常关键。如果程序被干扰或者变量被意外修改stStep变成了一个没有定义的值CASE语句不会执行任何分支设备会停在原地不动。加了ELSE之后至少会跳进错误状态并记录一个错误码排查起来有线索。3.3 输出映射让输出成为状态的函数状态机写完迁移逻辑之后还有一个很重要的步骤输出控制。我强烈建议不要在CASE分支里到处置位复位输出而是把CASE写成一个纯状态迁移器输出统一放到CASE后面的输出映射段。什么叫输出映射简单说就是输出 f(当前状态)。状态一到20上料气缸就动状态一到90所有动作全停。代码写出来是这样// // 输出映射 - 根据当前状态直接驱动输出 // bCylinderLoad : (stStep CST_LOAD); bVacuumOn : (stStep CST_LOAD); bPressCylinder: (stStep CST_PRESS); bHmiAlarm : (stStep CST_ERROR); uStepDisplay : stStep; // 状态值同步给HMI如果某个输出需要在多个状态为TRUE用OR连接。比如搬运气缸既要在上料时动又要在下料时动就写成bTransferCylinder : (stStep CST_LOAD) OR (stStep CST_UNLOAD);这种写法的最大优势是杜绝输出残留。传统置位复位方式很容易出现“状态已经跳走了但某个线圈还置位着”的情况特别是在报警急停之后特别危险。用输出映射之后状态值是唯一决定输出的因素状态跳到90除了报警输出其他危险输出全部强制断开哪怕某个CASE分支忘了写复位也没关系。不过这个写法有个需要注意的地方如果状态很多输出映射段会变成一长串逻辑表达式。我的经验是把同一工位的输出写在一起用注释分组看起来不会乱。另外如果某个输出控制特别复杂涉及一堆安全联锁那就不适合在状态机程序里写了应该单独放一个梯形图POU做安全逻辑状态机程序只负责流程控制不负责安全保护。3.4 错误处理报警、故障码与复位流程设备调试阶段错误处理比正常流程更重要。状态机的错误处理设计得好不好直接决定现场排查问题的效率。我通常在每个状态里都加超时判断超时后进入CST_ERROR状态同时写入一个故障码。故障码用16进制高位代表工位低位代表具体原因。比如16#110表示1号工位上料超时16#120表示2号工位压装超时16#130表示3号工位检测异常。这样HMI上显示故障码一眼就能看出是哪个工位、什么类型的问题。错误状态里的复位逻辑也要考虑周全。最简单的是直接按复位按钮回到待机。但有些设备要求复位前必须手动回原点那就要在CST_ERROR分支里判断原位条件不满足就不允许复位。我的建议是第一版先把“按复位回待机”做出来运行稳定之后再根据工艺加回原点条件。不要让报警复位逻辑一开始就搞太复杂否则很容易出现“报错了但复不了位”的尴尬情况。4. 状态机调试与常见问题4.1 在线调试如何观察当前状态状态机写完之后调试比梯形图简单得多。在Sysmac Studio里把stStep和dwErrorCode添加到Watch窗口在线运行的时候直接看stStep的值就知道设备当前卡在哪个状态。如果设备不动了第一步就是看stStep不用像以前那样满程序找信号。Sysmac Studio有一个很实用的功能模拟器。不接PLC就能在电脑上仿真运行状态机这种纯逻辑程序特别适合先仿真验证。我每次写完状态机都会先在模拟器里把按钮信号强制置位、复位把整个流程跑一遍。确认状态迁移顺序没问题之后再下载到真实PLC里接IO调试。这个习惯帮我省了无数次现场时间因为状态机的逻辑问题在仿真阶段就能暴露。在线模式下Sysmac Studio还支持临时修改状态变量。调试某个工步的时候不需要重新走一遍完整流程可以直接在Watch窗口把stStep改成目标状态然后运行。这样做的前提是输出映射逻辑能正确处理而且要注意安全千万别在设备有动作的情况下强制改状态。4.2 状态“卡死”的排查思路设备现场最常遇到的问题就是设备停在一个状态不走了。很多工程师遇到这种情况就开始乱猜系统性地排查非常关键。我的排查顺序是看stStep停在哪个状态。看这个状态的离开条件里哪一个信号没有满足。如果离开条件里包含传感器信号去I/O映射程序里看这个信号实际有没有反应过来。很多时候是传感器松动、接线脱了甚至IO映射地址写错了。如果离开条件里包含定时器看定时器有没有在跑。定时器没跑说明定时器的IN条件有问题也就是状态变量没有正确等于当前状态值。看dwErrorCode有没有被写入。如果已经写入了故障码说明状态机已经跳到错误状态只是HMI没有显示出来。有一次现场设备卡在上料状态怎么查都找不到原因后来发现是上料传感器的IO映射表里地址写错了上位信号根本没进到程序变量里。状态机本身没问题但配合的IO层有问题一样会导致卡死。所以排查不顺利的时候要跳出状态机程序去检查输入信号链路传感器→接线→IO模块→IO映射变量。4.3 HMI状态显示与枚举类型HMI上显示当前工步是状态机名副其实的杀手级应用。以前用梯形图写程序想显示当前工步非常麻烦要维护好几个中间变量还要防止显示更新不及时。用状态机之后只需要把stStep传给HMIHMI端做一个文本列表映射1就显示“待机”20就显示“上料”30就显示“压装”非常直观。我在变量规划里提到过uStepDisplay这个副本变量就是为了给HMI用的。为什么不直接把stStep给HMI因为stStep是一个状态机内部变量如果后续把状态变量从UDINT改成枚举类型HMI端读取就会遇到类型兼容问题。用uStepDisplay这个副本变量不管内部状态怎么变HMI端读到的始终是一个稳定的UDINT值改状态机不影响HMI通讯。Sysmac Studio也支持用户枚举类型可以在数据类型里定义枚举成员指定数值。用枚举做状态变量的好处是代码里不用记数字直接写CST_LOAD这种名字可读性很好。我早期项目用过后来为了HMI通讯统一性改回了UDINT加常量。两者都可以关键是和HMI交互时注意类型一致。4.4 状态机与梯形图混合编程的边界状态机不是万能的有些逻辑不适合塞进状态机。第一安全逻辑必须独立。急停、安全门、双手启动这类安全回路应该放在独立的梯形图POU里用纯硬逻辑输出不经过状态机。安全逻辑要求的是“无论程序跑到哪里急停按下输出必须立刻断”状态机是串行逻辑中间隔着一个CASE分支响应速度和安全等级都不够。第二轴控制不一定放在状态机里。欧姆龙的运动控制功能块MC_MoveAbsolute、MC_Home等本身就是状态机再封装一层业务状态机就要注意层级关系。我的习惯是轴的运动指令在PRG_Axis里管理状态机只负责给PRG_Axis发目标位置或启动信号不在状态机里直接调用轴功能块。第三传感器滤波和按钮防抖放在状态机外面。状态机程序里看到的信号应该是已经滤波、防抖之后的干净信号。如果状态机里还要处理脉宽只有50毫秒的信号会让状态迁移变得不可靠。状态机真正擅长的是串行顺序控制比如工装夹具、多工位装配、检测流程这类业务逻辑。要记住状态机负责“流程管理”不负责“具体执行”。具体执行交给IO程序和轴控制程序安全逻辑独立于所有流程之外。5. 写在最后的一点经验状态机写法不是银弹但确实是把多工位顺序控制程序从“一团乱麻”变成“一根线”的最有效方式。第一版写的时候可能会觉得CASE语句不如梯形图直观但真正跑起来尤其是第一次现场排故障通过stStep一眼定位问题的时候你会觉得这个转换非常值。我个人实际操作中的体会是状态机的回报周期比想象中快得多不需要等到完整项目做完才能看到收益。一个三工位设备半天就能把状态拆完、代码写完、仿真跑通第二天就能在真实设备上调试。更重要的是后续工艺调整带来的痛苦大幅减少因为流程是收敛的改一个状态不会殃及其他状态。最后再分享一个小技巧状态机和HMI的工步显示配合着做是调试效率提升最明显的组合。现场人员报障时说“设备停在上料超时”比你赶到现场翻半天IO表定位要快得多。如果你也在受设备程序越来越乱的困扰找个机会从一个小工位开始试着用状态机重新实现一遍相信你会回来感谢这个决定。