
去年年底我给一台老设备改动作流程梯形图里密密麻麻几十个网络光找一条互锁线就翻了半天改完还不敢马上跑又坐在屏幕前反复确认了一个多小时。从那一刻起我开始认真琢磨欧姆龙PLC上能不能也像嵌入式软件那样用状态机把逻辑“收一收”。后来我在Sysmac Studio里用结构化文本ST给设备主流程写了一版状态机实测下来比想象中顺利也比以前那套梯形图好维护得多。这篇文章就把第一次在欧姆龙PLC上写状态机的完整思路整理出来。内容围绕Sysmac Studio环境硬件以NJ/NX系列为主但就算你手上还是老款CJ2M这套思路同样通用只是软件换成CX-ProgrammerST支持的细节略有差异而已。我会从为什么用状态机、状态表怎么建、ST代码怎么写、常见坑怎么避这几个角度展开。适合正在调试多气缸、多工位设备又被逻辑互锁折磨得够呛的自动化工程师也适合刚接触欧姆龙NJ/NX、想进阶结构化编程的同行参考。1. 为什么非要给PLC程序引入状态机1.1 梯形图的痛动作一多逻辑就糊先说说我踩过的坑。早期做设备程序基本思路是“条件满足就置位输出条件不满足就复位输出”。单看某个动作没什么问题可一旦动作多了比如三个气缸加两个传感器再混上手动、自动、复位、报警梯形图就开始变成一碗面条。今天在这里加一个线圈明天在那里补一个触点三个月后回来看程序自己都不知道当年为什么要串一个常闭点进去。更怕的是改流程。客户说“取料完成后先横移再下降别先下降再横移”听起来只是换一下顺序但你要在梯形图里找到所有跟“下降”“横移”有关的互锁网络逐个改条件。运气好一两个小时运气不好改完以后联动测试又冒出新的逻辑漏洞。问题的根源在于梯形图的本质是“并行扫描”每个网络都在刷存在感而工艺流程本质是“串行步骤”。把串行流程强行铺开成一张并行扫描的网复杂度和动作数量是指数上升的不是线性上升。1.2 状态机的本质把“混乱的判断”变成“查表”状态机这个概念搞过嵌入式的人应该不陌生其实它的核心思想就一句话系统在任何时刻都处于某个明确的状态只有在特定事件发生时才会跳转到另一个状态。剩下的所有逻辑都围绕“当前状态”这个唯一事实展开。我用自动售货机类比一下你投币之前它是“待机”状态投入足够金额跳到“可选商品”状态按下商品按钮跳到“出货中”状态出货完成再回到“待机”。不管顾客怎么折腾售货机内部永远只需要回答一个问题——我现在处于哪个状态当前状态下该干什么。程序里不再需要同时判断“有没有投币”“投了多少”“有没有按下按钮”“货道有没有空”这些互相纠缠的条件。放到PLC上也是一样。设备主流程拆成几十个状态每个扫描周期只要做一个动作查看当前状态执行当前状态对应的输出判断当前状态的离开条件是否满足满足就切换状态。原来散落在几十个网络里的互锁逻辑大部分被收拢成一张状态表和几个CASE分支逻辑从“面条”变成了“表格”。1.3 从FPGA三段式到PLC同一个思路的迁移写过Verilog的同事看到我这套写法可能会觉得眼熟FPGA里有个经典的三段式状态机一段负责状态寄存器更新一段负责组合逻辑判断次态一段负责输出。PLC上的ST语言虽然不是硬件描述语言但这个思想可以原封不动搬过来。我在Sysmac Studio里就是把一个状态机POU拆成三个段落动作输出段只根据当前状态驱动气缸、真空阀、轴指令状态转移段只判断离开条件、计算下一状态状态更新段在最后统一把下一状态写回当前状态。这样写的好处是出问题时你不需要在几十个网络里猜打开代码就能直接定位是输出不对、条件不对还是状态根本没切过去。2. 动手之前先把状态模型画明白2.1 从一个具体工况说起自动搬运演示单元拿我实际做过的演示单元举个例子。设备结构很简单一个气缸负责升降Z轴升降到位各有一个磁性开关一个真空吸盘负责抓料带真空压力开关上料位有一个工件检测传感器启动和停止按钮各一个。整机只有两个执行机构听起来不复杂但如果不做状态机光“启动后先取料、取完放料、放完回原点”这个流程梯形图里就得十几个网络来回互锁。在画状态机之前我习惯先在纸上把整个工艺流程像流程图一样走一遍走完再抽象成状态。不要一上来就写代码这是最容易被跳过的步骤也是最重要的步骤。状态机是骨架流程图是骨骼和关节骨架画错了后面写再多代码都是给错误的结构打补丁。然后把这套流程拆成五个稳定状态待机ST_IDLE、取料ST_PICK、提升ST_LIFT、放料ST_PLACE、返回ST_RETURN。所谓“稳定状态”就是设备必须停下等待某个条件成立才能继续动作的位置。拆状态的时候有个经验一个状态里尽量只做一个事做到某个传感器到位或某个定时器到点就切换出去别让一个状态里同时等三四个条件。2.2 状态表与事件表的设计状态拆完了下一步是画状态表。这是整个状态机设计里最值钱的一张纸后面所有代码都是这张表的翻译。我的表长这样状态含义进入后动作离开条件事件下一状态ST_IDLE待机气缸缩回真空关闭启动按钮按下且原点传感器有信号ST_PICKST_PICK取料气缸伸出打开真空气缸伸出到位且真空压力到位ST_LIFTST_LIFT提升气缸缩回保持真空气缸缩回到位ST_PLACEST_PLACE放料关闭真空启动放料延时放料延时到时ST_RETURNST_RETURN返回气缸缩回真空关闭原点传感器有信号ST_IDLE这张表看着简单但每一个格子都值得反复推敲。比如ST_IDLE离开条件为什么必须是“启动按钮按下”加“原点传感器有信号”因为如果设备还没回到原点就启动取料位根本不在这气缸一伸出去就可能撞东西。这种细节必须写进表里因为写代码的时候很容易只顾着响应启动信号忽略了位置联锁。事件来源也要梳理清楚。状态机的“事件”在PLC世界里无非四类按钮和旋钮DI信号、传感器和开关DI信号、定时器到点内部事件、运动控制功能块的Done/Error信号指令完成事件。我这套演示单元用到了前三类后面接入伺服轴以后还会用到第四类。2.3 Sysmac Studio中变量规划与类型选择状态模型确定以后在Sysmac Studio里第一件事不是写CASE而是把变量规划好。欧姆龙NJ/NX系列用Sysmac Studio开发支持ST语言但它和纯文本编程环境不太一样变量必须先声明类型约束严格不允许像C语言那样随便拿一个int硬凑。我习惯把变量分成几个块输入信号设备按钮、传感器、输出信号气缸阀、真空阀、状态相关变量当前状态、下一状态、上一状态、辅助变量定时器实例、沿触发实例。命名上沿用欧姆龙社区常见的匈牙利前缀风格BOOL类型的输入用x开头输出用y开头状态用e开头这样后面写代码一目了然。还有一个细节状态变量建议用WORD类型不要用INT。原因有两个一是Sysmac Studio里CASE语句对WORD的处理更干净二是状态号本身不参与数学运算用无符号更符合语义。状态号的分配也留出间隔比如0、10、20这样排不要用连续的0、1、2不然以后要在中间插一个状态就得改一长串常量编号。3. Sysmac Studio实战用ST写一个可用状态机3.1 新建工程与任务配置打开Sysmac Studio新建工程选择对应的NJ/NX CPU型号。工程建好以后第一个动作是检查任务设置。欧姆龙的任务机制和传统PLC的扫描周期不完全一样NJ/NX可以跑多个周期性任务每个任务独立设置周期。状态机放在哪个任务里我的建议是放在主周期性任务里周期根据设备实时性要求来定一般1ms到4ms都能接受。这里要说一个关键点状态机循环不适合放在事件任务里因为事件任务由特定中断触发触发间隔不可控状态机的本质是“每个扫描周期都查一次状态”应该老老实实放在固定周期任务里跑。轴控制的情况稍微特殊一点。如果设备带伺服且状态机里有轴运动指令那么在欧姆龙里MC功能块和状态机最好放在同一个任务里调度避免跨任务访问轴变量带来的一致性困扰。我这台演示设备前期只用气缸任务配置一步就到位了后面加轴再调整即可。3.2 状态机核心代码三个段落的分工下面直接给出一版能编译、能仿真的最小模板。不要小看这段代码它的结构是从FPGA三段式搬过来的麻雀虽小五脏俱全。// 变量声明POU局部变量 VAR eCurState : WORD; // 当前状态 eNextState : WORD; // 下一状态 eLastState : WORD; // 上一状态用于沿检测和显示 xStartBtn : BOOL; // 启动按钮 xStopBtn : BOOL; // 停止按钮 xHomeSensor : BOOL; // 原点传感器 xCylOut : BOOL; // 气缸伸出到位 xCylIn : BOOL; // 气缸缩回到位 xVacuumOK : BOOL; // 真空压力到位 yCylOutVal : BOOL; // 气缸伸出阀 yVacuumVal : BOOL; // 真空阀 bTimeout : BOOL; // 状态超时报警 bWatchRun : BOOL; // 看门狗运行标志 rTrigChange : R_TRIG; // 状态变化沿检测 tonWatch : TON; // 状态超时看门狗 tonPlace : TON; // 放料延时 END_VAR VAR CONSTANT ST_IDLE : WORD : 16#0000; ST_PICK : WORD : 16#0010; ST_LIFT : WORD : 16#0020; ST_PLACE : WORD : 16#0030; ST_RETURN : WORD : 16#0040; END_VAR接下来是状态机的核心逻辑我把它放在同一个POU的ST段落里分成三段来写。// 第一段动作输出段 // 只根据当前状态驱动输出不修改当前状态 CASE eCurState OF ST_IDLE: yCylOutVal : FALSE; yVacuumVal : FALSE; tonPlace(IN : FALSE, PT : T#3S); ST_PICK: yCylOutVal : TRUE; yVacuumVal : TRUE; ST_LIFT: yCylOutVal : FALSE; yVacuumVal : TRUE; ST_PLACE: yVacuumVal : FALSE; // 真空关断后开始计时放料缓冲3秒 IF NOT xVacuumOK THEN tonPlace(IN : TRUE, PT : T#3S); END_IF; ST_RETURN: yCylOutVal : FALSE; yVacuumVal : FALSE; tonPlace(IN : FALSE, PT : T#3S); END_CASE;// 第二段状态转移段 // 只计算下一状态不直接改当前状态 CASE eCurState OF ST_IDLE: IF xStartBtn AND xHomeSensor THEN eNextState : ST_PICK; END_IF; ST_PICK: IF xCylOut AND xVacuumOK THEN eNextState : ST_LIFT; END_IF; ST_LIFT: IF xCylIn THEN eNextState : ST_PLACE; END_IF; ST_PLACE: IF tonPlace.Q THEN eNextState : ST_RETURN; END_IF; ST_RETURN: IF xHomeSensor THEN eNextState : ST_IDLE; END_IF; END_CASE;// 第三段状态更新段 // 所有状态改写统一在这里发生 eLastState : eCurState; IF eNextState eCurState THEN eCurState : eNextState; END_IF;解释一下为什么非要分三段。如果在第一段里直接写eCurState : ST_LIFT那么在同一个扫描周期里CASE后面可能已经没有机会执行ST_LIFT分支的条件判断了但下一段的动作输出却会在本周期就按照ST_LIFT去驱动这会导致动作和状态“抢跑”。把状态更新放最后保证每个扫描周期内动作输出、转移判断都基于同一个旧状态等判断完毕再统一切换这样状态机才不会“跳飞”。3.3 沿信号、定时器与完成条件的处理状态机里最常用的两个辅助手段就是沿触发和定时器这两样用不好状态机很容易出灵异现象。先说沿触发。启动按钮这类信号如果直接用xStartBtn : TRUE作为离开条件那么只要按钮不放每个扫描周期都会满足条件可能导致状态机在状态之间反复横跳。正确做法是只取按钮从FALSE变TRUE的那一个扫描周期。Sysmac Studio里可以直接实例化R_TRIG功能块rTrigStart(CLK : xStartBtn); IF rTrigStart.Q THEN // 这里才是我真正想要的“一次启动指令” END_IF;定时器的处理同样有讲究。我见过很多初学状态机的人喜欢在状态转移段里写“延时3秒以后跳转”然后把TON的调用一股脑放在判断里结果定时器一到时间又因为状态已经切走而被复位状态根本跳不过去。我的经验是定时器的调用放在动作输出段由当前状态启动它转移段只判断定时器的Q输出是否到点。这样定时器的启停逻辑和转移逻辑各管一摊彼此不打架。3.4 气缸与伺服轴动作的联动写法如果你以为状态机只能驱动气缸那就太小看它了。实际上自动设备里真正需要状态机镇场的恰恰是多轴联动的复杂流程。给我前面这个演示单元加一个伺服Z轴逻辑完全不冲突只需要把“气缸缩回到位”这个完成条件换成“轴运动指令Done”或者“轴实际位置与目标位置比较在容差范围内”。Sysmac Studio里调用MC_MoveAbsolute这类运动控制功能块在ST中的习惯写法是先把参数赋给功能块实例再执行调用。比如MC_MoveAbs_Z.Execute : (eCurState ST_LIFT); MC_MoveAbs_Z.Position : 0.0; MC_MoveAbs_Z.Velocity : 100.0; MC_MoveAbs_Z( Axis : Axis_Z, Execute : MC_MoveAbs_Z.Execute, Position : MC_MoveAbs_Z.Position, Velocity : MC_MoveAbs_Z.Velocity, Done bMoveZDone, Error bMoveZError, ErrorID wMoveZErrorID );然后在状态转移段里ST_LIFT的离开条件从xCylIn改成bMoveZDone就可以了。这个改造再次证明状态机的一个核心优势执行机构换了状态模型不需要大改只改完成条件的来源和动作输出的位置。这里诚心提醒一句运动控制功能块的Error信号一定也要接进状态机里。等轴指令Done等的再熟练如果Error出现了还在那里傻等Done设备就会僵在原地这个问题我后面在常见问题里细说。3.5 超时保护状态机才不会卡死状态机最怕的故障是“死等”。气缸磁性开关坏了、真空压力建立不起来、轴卡住不动任何一个完成条件失效状态机就会卡在当前状态设备表面上毫无反应实际上程序已经死循环。解决办法是给每一层状态加一个看门狗定时器超过设定时间还没离开当前状态就报超时故障并回到安全状态。我的实现思路是用R_TRIG检测状态变化状态每切换一次看门狗定时器就从零开始如果某个状态停留时间超过了阈值看门狗Q输出TRUE置报警标志并强制把下一状态设为ST_IDLE。核心代码大致是这样rTrigChange(CLK : (eCurState eLastState)); IF rTrigChange.Q THEN bWatchRun : FALSE; // 状态变化瞬间先停一个周期让定时器归零 END_IF; tonWatch(IN : bWatchRun, PT : T#10S, Q bTimeout); bWatchRun : TRUE; IF bTimeout THEN bAlarmTimeout : TRUE; eNextState : ST_IDLE; END_IF;超时时间的选择也要动脑子。太短会把正常流程误判成故障比如放料延时3秒你给看门狗设定4秒还没放完就报警了。太长又失去了保护意义。我的经验是先按每个状态最长耗时乘1.5到2倍来定设备跑稳定以后再看报警记录微调。4. 常见问题与排查技巧实录4.1 状态“跳飞”一个扫描周期跳了两个状态这个问题我当年调试时真遇到过现象是按下启动按钮以后状态从ST_IDLE直接跳到ST_LIFTST_PICK好像被跳过去了。一开始以为是逻辑写错了查了半天才发现问题出在状态更新段我把eCurState : eNextState写在了转移判断的前面导致同一个扫描周期里CASE先按旧状态算出一个次态然后立刻更新当前状态但代码还在继续往下跑后面的CASE又用新状态再判了一次结果一个周期内跳了两级。解法就是前面反复强调的状态更新必须放在所有转移判断执行完之后最好放在POU的末尾。养成这个习惯以后“跳飞”几乎绝迹。4.2 定时器被提前复位延时永远到不了点另一个高频问题表现为放料延时永远不结束或者延时时间明显不准。排查代码发现定时器TON的IN引脚在ST_PLACE里被我写成了xVacuumOK的反向看起来没问题但状态机每个扫描周期都重新执行一次定时器调用如果条件不稳定IN会在TRUE和FALSE之间抖动TON的累计计时就会不断被打断。我后来统一改成“状态内部先判断是否需要计时需要就始终让IN为TRUE直到超时或状态切走”。如果你发现定时器行为诡异第一件事就是打开Watch窗口看TON的ET值看看它是一直在涨还是反复清零。能看到ET的时候问题基本就定位一半了。4.3 在线调试Sysmac Studio里怎么观察状态流转Sysmac Studio支持模拟器功能这点必须夸一下。不接PLC也能把状态机逻辑跑起来。在线调试时我会同时在Watch窗口添加这几个变量eCurState、eNextState、eLastState以及几个关键的输入信号和定时器Q输出。调试手法很重要把eCurState和你手上的状态表对照着看一步一步走。比如现在eCurState是16#0010ST_PICK我强制把xCylOut和xVacuumOK置为TRUE刷新一下状态应该跳到16#0020ST_LIFT。如果没跳再看eNextState是否计算正确如果eNextState还是ST_PICK说明转移段的条件没走到再往前查事件信号。这种“状态表对照法”比盯着一大堆梯形图网络猜效率高太多了。顺带提一个坑在模拟器中强制BOOL变量时有些信号会被程序每个扫描周期覆写导致你明明强制了TRUE一刷新又变回FALSE。这是因为你在程序里给它赋值了。模拟器调试时最好把强制点放在POU外部或者先从程序里临时注释掉该信号的写入逻辑再手动强制。4.4 报警后怎么退出别让状态机“原地去世”我最开始写的状态机没有考虑异常退出结果一报警设备就停在当前状态不动手动复位也没反应。后来我把报警处理统一改成任何严重报警产生时eNextState : ST_IDLE同时在ST_IDLE里等待复位信号复位后再重新走流程。这样设备至少能回到一个已知的、安全的位置操作人员不用慌着去断电。这里要强调报警跳转不能直接eCurState : ST_IDLE还是要走eNextState通道保持三段式纪律。报警只是一个特殊的转移事件和其他条件一样最后交给状态更新段统一处理。5. 从初试到落地状态机的工程化建议5.1 用“状态动作条件”三层分离代替大杂烩第一版状态机写出来以后我发现代码确实能跑但看起来还是有点像大杂烩动作输出、转移条件、报警判断全搅在一起。后来看了一些做嵌入式状态机框架比如QP状态机的经验借鉴了“状态、动作、转移”分离的思路把代码从一个大CASE拆成了多个更小的逻辑块。具体到Sysmac Studio里你完全可以不用一个POU写到底。建一个“逻辑”POU负责状态转移再建一个“输出”POU负责动作输出中间用全局变量串联。这样后期加一个新状态只需要动状态表、转移逻辑和输出逻辑各对应的小段落其他地方一行都不用碰。当然小型设备这么做可能显得“重”但设备动作一旦超过十个状态这种分层绝对是值得的。5.2 上层通讯让组态王或HMI直接显示当前状态状态机理顺以后还有一个很大的好处HMI和上位机可以非常直观地显示“设备当前在干什么”。你只需要把eCurState这个WORD变量映射到触摸屏然后在HMI里做文本切换ST_PICK显示“取料中”ST_PLACE显示“放料中”现场操作人员一眼就能看懂设备卡在哪里。跟组态王这类上位机组态软件通讯也是同理。Sysmac Studio支持把全局变量表导出成XML或CSV格式你可以在工程设置里把符号导出再交给上位机那边导入省去一行一行手动敲变量名的麻烦。状态码是一个WORD对应关系做成一张表放在工程文档里上位机开发时直接查表就行。5.3 别把所有东西都塞进一个状态机最后想聊一个真正从工程里长出来的体会不要把什么都塞进同一个状态机。设备的主流程放在主状态机里但手动模式、回原点模式、报警处理这些流程我后来都单独拆成了独立的状态机或独立逻辑块。否则主状态机被手动和自动、报警和正常混杂在一起状态数量会爆炸维护难度反而比梯形图还高。我的做法是主状态机只管自动流程手动模式下强制eCurState : ST_IDLE同时用一个“运行模式”变量区别自动/手动。自动状态机内部只在上电初始化时负责一次回原点回原点完成后再进入待机。这样每个状态机都保持很小的体积逻辑清爽得让人上瘾。说到回原点其实也是一个典型的子状态机先查是否需要回Z再回X再回Y每走一步都判断当前位置。我当时偷懒没有拆结果主状态机里混进了七八个回原点的中间状态状态表瞬间膨胀。后来老老实实拆成独立的回原点子状态机主状态机顿时瘦身。这是我这轮实践里利润最高的一次重构。最后一句话给正准备动手的你状态机在欧姆龙PLC上完全可行Sysmac Studio的ST环境支持得很到位甚至比某些老牌PLC环境还舒服。我的建议是别指望一次写完就完美先在模拟器里把状态表从头到尾“走”三遍再下载到设备上试。第一遍看正常流程第二遍故意把传感器信号断掉看报警能不能触发第三遍把手动、自动、复位来回切换看状态机会不会乱。这三遍走完你再回头看当初那碗面条一样的梯形图大概率会和我一样再也回不去了。