
1. 为什么PLC程序越改越乱状态机是解药不是概念先说个我自己的经历。早年间用CP1H写一台小设备动作流程大概是上料→夹紧→钻孔→退刀→松开→下料。第一版梯形图很规整每个动作一段程序。但客户后来陆续加了“暂停后要回到安全位”“报警复位后要从上一步继续”“手动模式下不允许走到自动流程”这些需求我就在原来的梯形图里到处插入M继电器、SET/RST指令、跳转条件。两个月后程序变成一团乱麻自己都不敢动。后来接触Sysmac Studio开始用NJ/NX系列写程序我才真正把状态机这套思路落到PLC上从那以后写多步骤设备逻辑基本没再翻过车。所以要聊“欧姆龙PLC状态机写法”我先给个结论状态机不是嵌入式软件工程师的专属概念它恰恰是治理PLC程序复杂度最实用的一套思路。PLC程序和状态机的匹配度天然就高——PLC就是按扫描周期不断循环执行的机器而状态机就是“当前处于什么阶段这个阶段做什么事满足什么条件去下一个阶段”。这两者几乎是一一对应的。这篇内容不打算讲空泛的理论就以Sysmac Studio为环境带着大家在NJ/NX系列PLC上用ST语言写一套完整的状态机框架从设计思路到代码结构再到调试方法全部过一遍。看完之后你至少能解决这么几件事多步骤流程不再靠M继电器堆逻辑状态一目了然手动、自动、报警、暂停这些模式切换不再互相干扰新人接手程序时不需要你去“讲三天”才能看懂流程。2. Sysmac Studio里写状态机的环境准备2.1 Sysmac Studio版本与控制器选型在动手写代码之前先把环境理清楚。Sysmac Studio是欧姆龙NJ/NX系列PLC的统一编程环境也能用来配置NB/NT系列HMI、伺服驱动器和视觉系统。我用的是Sysmac Studio 1.56版本配合NX1P2-1140DT这款控制器。这里有个容易踩坑的点需要先说清楚Sysmac Studio和老的CX-One不是一回事儿。CX-One给CP1/CJ2/CJ1这些传统PLC用编程语言以梯形图为主而Sysmac Studio面向的是自动化控制器Automation Controller主打ST、梯形图、结构化梯形图底层基于IEC 61131-3标准。所以你要在欧姆龙平台上尝试状态机最好直接用NJ/NX系列不要在CP1H上用梯形图硬凹状态机概念后者的指令体系和变量管理方式决定了这件事做起来非常别扭。控制器的选择方面控制器型号特点适用场景NJ501系列处理速度快运动控制能力强多轴运动控制为主的中大型设备NJ101系列经济型逻辑控制为主小型设备、单机自动化NX1P2系列体积小内置EtherCAT标准单机设备、老设备改造NX7系列高端高速处理大型产线、复杂算法如果你的项目同时需要状态机逻辑和运动控制NJ/NX系列可以在同一个程序里完成不需要像老方案那样PLC加运动控制卡分开搞。这也是我后来从CP1H转向NJ/NX的一个重要原因——状态机里要触发电机绝对定位、速度控制、位置读取全部在一个变量空间里操作不用考虑跨系统通信的问题。2.2 创建一个干净的工程并划分程序块新建工程的时候在“项目树 → 编程 → 程序”下面会自动生成一个名为Program0的程序。我建议你先不要急着写逻辑而是把程序结构想好。Sysmac Studio里程序块Program之间是可以独立调度和访问全局变量的合理的程序划分比代码本身更重要。我的习惯是建立三个程序块设备控制主程序设备总控里面放状态机主体手动操作程序点动、回原位、单步调试报警处理程序采集报警条件并做锁定/复位。这三个程序块在主任务里按顺序执行。为什么要分块而不是全塞到一个程序里因为状态机一旦跑起来程序扫描周期是个硬约束。如果手动点动逻辑和状态机逻辑混在同一个程序块里那么手动操作时状态机里的条件判断也可能被执行到。虽然可以通过模式变量来屏蔽但代码的阅读难度会成倍增加。分开之后每个程序的职责边界就清楚了。程序块建好之后设置主任务的任务周期。默认是1ms如果你的程序逻辑量较大或者用了大量浮点运算建议调到2ms或4ms。状态机本身不太依赖高速扫描但EtherCAT轴的控制周期会受影响这一点要综合考虑。一般是任务周期越短控制越平滑但如果程序执行时间超过任务周期控制器会报任务超时错误这个在Sysmac Studio里是硬性检查的。3. 状态机在PLC里的具体形态从流程图到代码的翻译过程3.1 传统梯形图写法的痛点先拆一下为什么传统的梯形图写法在流程复杂的场景下会失控。梯形图本质上是个平行执行的电路逻辑每一行都是“条件满足就执行输出”的结构。写简单的单动作控制梯形图确实直观但一旦涉及多步骤流程你需要把“当前的步骤”“步骤切换的条件”“每个步骤的输出”全部翻译成线圈和触点。举个例子一个三步流程“A动作→B动作→C动作”传统写法大概是第1步启动条件成立时置位M1第2步M1为ON且A动作完成信号到位时复位M1置位M2第3步M2为ON且B动作完成信号到位时复位M2置位M3第4步M3为ON且C动作完成信号到位时复位M3输出完成。这个模式写十个步骤以内还清楚写到二十步以上你就要在梯形图里来回找哪个M继电器在哪一步被置位了、在哪一步被复位了。更要命的是暂停功能如果第10步执行到一半要暂停你需要记住当前处于哪一步然后在每一行的条件里加上暂停时的输出禁止逻辑。加上报警复位后要从第8步继续你就需要在报警处理程序里给对应M继电器置位……整套逻辑下来新旧版本的比较、排查问题都非常费劲。3.2 状态机的核心要素拆解状态机在PLC里落地本质上需要四个东西状态寄存器存储当前状态的变量状态转移条件从当前状态进入下一个状态的条件状态动作在每个状态下需要执行的输出状态转移互斥任意时刻只有一个激活状态。第一个要素直接用INT或枚举类型变量就可以实现。第二个要素是状态转移的核心我的做法是专门用一个转移条件函数块来集中管理转移条件。第三个要素一般用CASE语句配合每个状态下的具体输出逻辑实现。第四个要素其实由CASE语句天然保证——程序执行到哪个分支其他分支就不会执行这就是互斥性。状态机里的“状态”和PLC程序里的“步骤”是两回事。步骤是线性的第1步、第2步、第3步做完就结束。状态则可以存在跳转、回退、循环。比如一个设备在自动运行状态检测到异常进入“暂停状态”人工处理后按复位键又能从暂停状态回到“运行状态”。设备永远在某一个状态里这就是状态机比线性步骤强的地方。3.3 用ST语言把状态机翻译成PLC代码Sysmac Studio支持ST语言编程它基本符合IEC 61131-3标准。用ST写状态机非常自然因为CASE语句本身就是为状态机设计的。最开始我先用一个简单的枚举类型来表示设备状态TYPE E_DEVICE_STATE : ( ST_INIT : 10, // 上电初始化 ST_IDLE : 20, // 待机 ST_AUTO_RUN : 30, // 自动运行 ST_AUTO_PAUSE : 40, // 自动暂停 ST_ALARM : 50, // 报警 ST_MANUAL : 60 // 手动模式 ) : INT; END_TYPE注意这里我把枚举的显式值从10开始而不是用0、1、2这么做是有讲究的。在Sysmac Studio的在线监视里变量显示的是枚举名称方便查看。但如果将来你把状态值传到触摸屏上显示数值0可能被当作“无效状态”从10开始可以留出余量也方便排查异常数据。状态机的主逻辑放在一个全局FB功能块里也可以直接写在Program里。我建议封装成FB因为可以把状态机的内部变量私有化外部只能通过定义的接口来操作程序结构会更干净。4. 核心代码架构CASE语句为主体的状态机实现细节4.1 状态机主框架下面这段代码是状态机的主框架它实现了一个标准的“状态判断 → 执行动作 → 检查转移条件”的过程CASE stCurrentState OF ST_INIT: // 初始化动作 bInitComplete : FALSE; bOutputA : FALSE; bOutputB : FALSE; bAlarmReset : FALSE; // 初始化完成条件 IF bPowerOK AND bServoReady THEN bInitComplete : TRUE; stNextState : ST_IDLE; ELSE stNextState : ST_INIT; END_IF; ST_IDLE: // 待机状态下所有输出关闭 bOutputA : FALSE; bOutputB : FALSE; // 手动/自动模式切换 IF bManualModeReq THEN stNextState : ST_MANUAL; ELSIF bAutoStartReq THEN stNextState : ST_AUTO_RUN; autostep : 0; ELSE stNextState : ST_IDLE; END_IF; ST_AUTO_RUN: // 自动运行状态 - 内部再分子步骤 AutoRunStep(); // 暂停请求 IF bPauseReq THEN stNextState : ST_AUTO_PAUSE; // 报警触发 ELSIF bAlarmActive THEN stNextState : ST_ALARM; // 自动流程完成 ELSIF bAutoFinished THEN stNextState : ST_IDLE; bAutoFinished : FALSE; ELSE stNextState : ST_AUTO_RUN; END_IF; ST_AUTO_PAUSE: // 暂停状态停止运动输出但保留当前子步骤记忆 bOutputA : FALSE; bOutputB : FALSE; // 暂停解除 IF NOT bPauseReq AND bResetPause THEN stNextState : ST_AUTO_RUN; ELSE stNextState : ST_AUTO_PAUSE; END_IF; ST_ALARM: // 报警状态所有危险输出关闭 bOutputA : FALSE; bOutputB : FALSE; // 报警复位 IF bAlarmResetCmd THEN stNextState : ST_IDLE; bAlarmResetCmd : FALSE; ELSE stNextState : ST_ALARM; END_IF; ST_MANUAL: // 手动模式 ManualOperate(); // 手动模式退出 IF NOT bManualModeReq THEN stNextState : ST_IDLE; ELSE stNextState : ST_MANUAL; END_IF; ELSE // 未定义状态 - 强制回到待机 stNextState : ST_IDLE; END_CASE; // 状态更新采用双变量切换避免跳变 IF stNextState stCurrentState THEN stLastState : stCurrentState; stCurrentState : stNextState; bStateChanged : TRUE; ELSE bStateChanged : FALSE; END_IF;这里面有几个关键设计我要展开说第一个是stNextState和stCurrentState的分离。这是写状态机最容易忽视的细节。如果你在CASE语句里直接给stCurrentState赋值那么当前扫描周期内后续对stCurrentState的判断就会基于新值整个状态的转移逻辑会变得不可预测。我最开始写的时候直接在一个CASE分支里改了stCurrentState结果监视的时候发现状态跳了两次查了半天才明白是扫描周期内状态变量被提前更新了。后来改用了双变量方案一个存当前状态一个存下一个状态在CASE结束后统一更新逻辑就清晰多了。第二个是ELSE分支。我在所有CASE判断的末尾都加了ELSE把stNextState设置成当前状态。这意味着没有转移条件满足时状态机会停留在原状态。这个习惯很重要因为PLC程序里如果某个状态既没有转移条件也没有ELSE控制器会执行空分支虽然不会报错但状态就悬空了非常难排查。第三个是“未定义状态”的处理。在CASE语句的ELSE里我强制回到待机状态。这是最后一道防线。如果程序被意外的数据写坏了状态变量比如触摸屏误写入了一个50以外的数值设备会自动回到待机而不是继续执行危险动作安全上更可靠。4.2 自动运行子状态复杂流程的状态嵌套自动运行状态内部往往还有子流程这就是状态机的嵌套。设备处于自动运行状态时内部还可分为“取料→定位→加工→检测→下料”这些子步骤。如果把这些子步骤全部平铺在外层状态机里状态列表会非常长而且自动、手动、报警这些外层状态切换时会互相干扰。我的做法是把自动运行的子流程单独封装到一个方法里或者做成内部子状态机。下面是一个带子步骤的自动运行代码// 自动运行子步骤计数器 // 0准备, 1取料, 2定位, 3加工, 4检测, 5下料, 6完成 CASE autostep OF 0: // 准备 bGripperOpen : FALSE; bSpindleRun : FALSE; IF bReady THEN autostep : 1; END_IF; 1: // 取料 bGripperOpen : TRUE; // 打开夹爪 IF bGripperOpened THEN autostep : 2; END_IF; 2: // 定位 MoveToPosition(REAL#100.0, REAL#200.0); IF bPositionReached THEN autostep : 3; END_IF; 3: // 加工 bSpindleRun : TRUE; SetTimer(tProcessTime, T#5S); IF TimerOutput(tProcessTime) THEN autostep : 4; END_IF; 4: // 检测 IF bInspectionOK THEN autostep : 5; ELSE // 检测不合格进入异常处理 bInspectionNG : TRUE; autostep : 6; END_IF; 5: // 下料 bGripperOpen : FALSE; IF NOT bGripperOpened THEN autostep : 6; END_IF; 6: // 完成 bAutoFinished : TRUE; autostep : 0; ELSE autostep : 0; END_CASE;这里有几个细节值得注意。关于定时器的使用Sysmac Studio里定时器不是直接用一个TON功能块实例那么简单因为如果你的设备有二十个子步骤每个步骤都可能有定时需求总不能创建二十个TON实例。我的习惯是封装一个定时器管理功能块——输入时间值和触发条件输出定时结束信号内部用一个系统时钟变量来做计时。这样不管多少个步骤只需要一个功能块实例代码量能省不少。关于状态和输出的关系我在自动运行的每个子步骤里直接写输出而不是在另一个地方集中管理。这样做的优点是调试时打开自动运行这个CASE一眼就能看到当前步骤的输出状态缺点是如果某个输出在多个步骤里都需要开代码会有重复。好在子步骤数量通常不超过十个可读性优先。4.3 输入映射与输出映射状态机与硬件的隔离层状态机在PLC里和嵌入式状态机有一个显著区别PLC程序要直接操作物理输入输出而物理信号存在抖动、毛刺、反相等问题。我强烈建议在状态机代码里不直接使用物理IO地址比如直接读NX单元的输入字节而是先做一次输入映射和输出映射。原因很简单设备调试过程中换IO点、改传感器类型常开改常闭、调整急停逻辑是常态。如果你状态机里直接用了物理地址改一个输入点可能要把代码改一遍。有了映射层只改映射表就行。输入映射// 输入映射 bPowerOK : IO_IN_PowerOK; // 电源正常信号 bServoReady : IO_IN_ServoReady; // 伺服准备好信号 bAutoStartReq : IO_IN_AutoStartBtn; // 自动启动按钮 bAlarmActive : IO_IN_AlarmSensor; // 报警传感器 bPauseReq : IO_IN_PauseBtn; // 暂停按钮 bGripperOpened : IO_IN_GripperOpenSensor; // 夹爪打开到位 bPositionReached : IO_IN_PosReached; // 定位完成信号 bInspectionOK : IO_IN_InspectionOK; // 检测OK信号输出映射// 输出映射 IO_OUT_GripperOpen : bGripperOpen; IO_OUT_SpindleRun : bSpindleRun; IO_OUT_AutoRunningLight : bAutoFinished OR (stCurrentState ST_AUTO_RUN);这里有个输出映射的灵活用法比如“自动运行指示灯”这个输出我直接在映射表里定义逻辑——自动完成或正在自动运行的时候亮灯。输出映射层不仅能隔离硬件地址变更还能组合逻辑。额外提一句输入信号的滤波处理最好也放在映射层里做。比如一个气缸磁性开关可能因为机械振动产生几毫秒的抖动信号。Sysmac Studio的输入单元自带数字滤波参数但我更习惯在程序里做信号确认——要求信号持续10ms以上才认为是有效信号。这样比改硬件滤波参数更灵活调试时也能随时调整。5. 手动/自动/报警三模式切换状态机的高阶应用5.1 模式切换的策略与安全互锁把状态机用在模式切换上最大的好处是所有模式都是“状态”的一部分切换模式就是状态转移天然互斥。手动模式时自动流程不会执行自动模式时手动点动按钮无效这个由CASE分支结构就保证了。但模式切换不能简单粗暴。比如设备正在自动加工过程中操作工突然切到手动模式此时主轴正在高速旋转夹爪夹着工件直接切手动会导致设备失去自动程序保护非常危险。我的经验是模式切换之前必须经过安全状态自动 → 手动必须设备处于待机或停止状态如果自动流程正在执行需要先触发暂停等流程走到安全位置后才能切换手动 → 自动需要手动模式下所有动作都复位设备回到原点后允许切换报警 → 手动报警时允许切换到手动模式但只能执行部分安全动作如打开夹爪、退回Z轴禁止执行加工类动作。这些约束写在哪我建议不要散落在各个CASE分支里而是集中在状态转移条件的计算逻辑里。代码会清晰很多。5.2 暂停与继续记住子步骤的关键实现暂停功能是PLC状态机里比较难做对的一个点。核心问题在于暂停发生时自动运行子步骤可能已经执行了一部分。比如设定的加工时间是5秒暂停时已经过去了3秒如果暂停键按下后直接把autostep从3跳回0那12秒后又得重新走一遍。正确做法是暂停时不改变autostep的值只是暂停状态下的输出逻辑屏蔽自动输出。上面的代码里我正是这么处理的ST_AUTO_PAUSE状态下只做了一件事——关闭输出然后等暂停解除信号。一旦解除状态回到ST_AUTO_RUNCASE继续按原来的autostep值从第3步往下走。关键在ST_AUTO_RUN进入暂停条件时不要修改autostep的值就让它保持暂停前的状态。但这个设计有个隐患如果暂停发生时设备正执行到夹爪动作的中间状态比如夹爪已经打开了但暂停状态把夹爪输出关掉了机械上夹爪会掉落工件。所以必须在暂停时区分“安全暂停”和“紧急暂停”。安全暂停是指设备可以停止输出机械结构不会发生变化紧急暂停则是必须立刻切断所有动力源。如果设备不允许安全暂停应该使用单独的急停回路而不是靠程序来处理。5.3 报警状态下的安全输出策略报警状态处理的核心原则切断危险输出保留安全输出。在ST_ALARM分支里我把bOutputA和bOutputB都置FALSE这代表主轴的运行、气缸的动作等危险输出全部关断。但安全输出比如报警灯、蜂鸣器应该在映射层独立置位不通过状态机管。还有一个容易忽略的点报警信号需要自锁。如果报警条件是“温度过高”温度降下来报警信号消失但设备不能自己恢复运行必须等人为确认。所以报警状态退出不能只判断报警条件是否消失还要判断是否有复位命令。上面的代码里我用了bAlarmResetCmd作为退出报警的条件这个变量来自触摸屏的复位按钮或者硬件复位按钮。报警复位后设备回到ST_IDLE而不是回到自动运行这是一个安全选择。有的设备要求报警处理后要从断点继续那可以改为回到ST_AUTO_RUN并保留之前的autostep但前提是报警没有造成机械损坏或工件位移。我一般默认回到待机只有确认报警不影响安全才改为恢复运行。6. 状态机写法的对比与选型FB封装、梯形图与ST的选择建议6.1 三种实现方式的效果对比在Sysmac Studio里实现状态机不止ST一种方式。我见过有人用梯形图写也见过用SFC顺序功能图写的这里把常见方式做个对比实现方式可读性修改维护适合场景梯形图 M继电器步骤多时很差修改容易出错极度简单的顺序控制小于5步ST CASE很好非常方便多步骤、多模式、逻辑复杂SFC顺序功能图较好较方便线性流程为主且团队熟悉SFC我在Sysmac Studio里使用SFC的机会不多因为SFC更适合工艺步骤非常固定的场景一旦涉及多模式切换、报警跳转、暂停恢复这些操作SFC的表达能力不如ST。当然这是个人偏好也有同行特别擅长用SFC。梯形图写状态机用了M继电器最大的问题我刚才也说了状态的置位和复位散落在不同的网络里可读性差而且梯形图里写状态迁移矩阵非常费劲。如果你刚接触状态机建议直接ST起步不要用梯形图做过渡——反正Sysmac Studio所有语言写的程序都在同一个工程里局部功能用梯形图、主体逻辑用ST完全没问题。6.2 FB封装状态机组件的复用我之前做过的几台设备虽然动作流程不同但都有“手动/自动/报警/暂停”这些基本模式。所以我把状态机框架封装成了一个可复用的FB叫FB_DeviceStateMachine。接口设计如下FUNCTION_BLOCK FB_DeviceStateMachine VAR_INPUT bEnable: BOOL; // 功能块使能 bManualModeReq: BOOL; // 手动模式请求 bAutoStartReq: BOOL; // 自动启动请求 bPauseReq: BOOL; // 暂停请求 bAlarmResetCmd: BOOL; // 报警复位命令 bAlarmActive: BOOL; // 外部报警信号 bPowerOK: BOOL; // 电源正常 bServoReady: BOOL; // 伺服就绪 END_VAR VAR_OUTPUT stCurrentState: E_DEVICE_STATE; // 当前状态 bAutoFinished: BOOL; // 自动完成 bInAutoRun: BOOL; // 处于自动运行 END_VAR VAR stNextState: E_DEVICE_STATE; stLastState: E_DEVICE_STATE; autostep: INT; END_VAR这样封装好之后新设备上只需要根据具体工艺填充ST_AUTO_RUN里的子步骤逻辑模式切换和报警处理的框架代码完全复用。我在三个项目里用了同一套状态机FB每个项目的开发周期至少节省了两三天而且后面维护的时候模式相关Bug基本没有。6.3 状态机与运动控制的配合一个实际案例在做欧姆龙NJ/NX的设备时状态机常常要和运动控制配合。比如自动运行子步骤里“定位”这一步我调用了MoveToPosition这个自己封装的轴控制方法。这个方法内部用MC_MoveAbsolute功能块驱动伺服轴而状态机只需要等待“定位完成”这个信号bPositionReached。这里有个很重要的设计原则状态机只关心轴运动的完成反馈不关心轴控功能块内部的具体参数。轴的速度、加减速时间、当前位置这些参数应该放在轴控功能块内部定义状态机层不需要也不应该知道。这样状态机程序和运动程序解耦如果后期把伺服电机换成步进电机状态机代码一行不用改只改轴控层就行。但要注意一点运动控制功能块的执行需要专门的周期任务并且状态机所在的主任务里直接调用MC_MoveAbsolute会涉及轴控指令的重入问题。如果状态机扫描周期内重复触发同一个运动指令轴可能不会按预期动作。我的做法是在轴控功能块内部对输入指令加沿检测只有在状态机里bPositionReached从FALSE变TRUE的时候才真正执行一次绝对定位。这个细节没处理好程序跑起来就是各种莫名其妙的轴不动、轴乱跑。7. 调试与排错的经验状态机程序的在线监视技巧7.1 怎样用Sysmac Studio的在线监视功能高效调试状态机写完状态机后调试阶段是最考验耐心的。Sysmac Studio的在线监视功能比老的CX-One好用太多这里分享几个我常用的调试技巧。第一个技巧把状态变量放到Watch窗口同时监视stCurrentState和autostep。stCurrentState是枚举类型在线监视时会直接显示状态名称比如ST_AUTO_RUN而不是数值。autostep是INT类型监视到哪个子步骤一目了然。两个变量同时看就能快速定位设备当前到底卡在哪个环节。第二个技巧利用Sysmac Studio的“程序在线编辑”功能。调试过程中我发现某个状态转移条件写错了可以直接在线修改ST代码然后下载不需要整个程序停止。这比传统PLC的离线修改再下载高效很多。但要注意在线编辑会短暂暂停当前的PLC程序扫描如果设备正在动作最好先临时切换到安全状态。第三个技巧强制变量值来模拟信号。调试时没有实际的传感器信号就很头痛比如工件检测传感器没装但我想验证检测之后的状态转移是否正确。Sysmac Studio支持在线强制变量的值直接把bInspectionOK改成TRUE状态机就会往下走。但强制变量不能用在物理输入点上这时需要一个映射变量这也是我之前强调输入映射层的原因之一——调试时强制映射变量比强制物理IO方便得多不会影响下次真实信号的读取。第四个技巧波形图记录的妙用。Sysmac Studio自带数据记录功能可以把状态变量、关键输入信号记录下来。我之前遇到过一个问题设备偶尔在自动运行中突然跳回待机但频率很低人盯着监视器一整天可能都复现不了。后来用数据记录功能记录了stCurrentState和报警信号的波形第二天翻记录发现报警信号有一个大约80ms的干扰脉冲触发了报警状态。没有波形记录这种偶发问题基本不可能找到。7.2 状态机常见故障与排查思路状态机程序写完之后实际运行中容易出现的故障集中在几个地方。我把常见的现象、原因和排查方向整理成一张表故障现象可能原因排查方向状态卡在某个值不动转移条件一直不满足检查转移条件里每个信号的状态用强制变量模拟确认状态跳了两个值状态变量被多处赋值或信号抖动检查是否有其他程序块写了同一个状态变量加入去抖逻辑自动运行子步骤乱跳autostep被多个CASE分支操作统一在同一个方法里管理autostep禁止外部修改模式切换后输出未关断输出映射层未按状态过滤检查输出映射逻辑确认安全输出和危险输出的优先级暂停后再启动动作就错位暂停时改变了autostep或轴位置记忆丢失暂停分支不修改子步骤计数器运动轴保持使能报警复位后设备直接动作报警复位未经过待机状态检查报警退出逻辑是否必须回ST_IDLE加延迟确认这里我挑一个最常见的来展开状态卡住不动。排查的第一步是看stCurrentState的值到底停留在哪个状态然后用Watch窗口看这个状态下所有转移条件涉及信号的实际状态。比如停在ST_AUTO_RUN的第2步定位但bPositionReached是FALSE那问题就出在定位没有完成。接着看轴的实际位置和指令位置如果轴根本没动问题在轴控层如果轴到了位置但反馈信号没到位问题在传感器或反馈逻辑。一层层剥下去大多数卡死问题都能定位。另一种常见问题是信号抖动导致误状态切换。设备自动运行中突然报警但实际没有发生故障只是传感器信号被干扰。这时候除了排查接地和屏蔽程序层面也要加保护。我在输入映射层给关键信号加了防抖判断要求信号保持10ms以上才有效。Sysmac Studio的输入单元本身也支持滤波时间设置配合使用效果更好。7.3 初次接触状态机的常见误区最后聊几个新手写PLC状态机时特别容易踩的坑这些是我自己走过弯路之后总结出来的。第一个误区是觉得状态机只适合ST语言。实际上梯形图写状态机理论上是可行的只是不容易维护。如果你暂时没有条件换NJ/NX系列在CP1H上用梯形图也可以借鉴状态机的思路把每个状态定义成一个STEP编号然后用比较指令做状态判断用置位复位做状态迁移。虽然不如ST优雅但比传统M继电器堆控制还是要好很多。第二个误区是状态写得太多太细。我见过有人把每个气缸的每个动作都定义成独立状态整个状态列表三十多个代码量大得吓人。状态机的价值在于管理复杂的流程和模式而不是替代每一个继电器逻辑。气缸伸缩、电机启停这些底层动作更适合用传统逻辑控制状态机只负责层面上的模式切换和流程编排。第三个误区是忘记在状态机里考虑初始化。设备上电后状态机应该进入一个确定的初始状态并且各个输出应该是安全的。我在ST_INIT状态里把bOutputA、bOutputB都强制为FALSE就是确保任何情况下上电都不会有危险输出。等电源、伺服都就绪了才允许进入待机。这个过程看似简单但没有它设备上电的一瞬间可能会有意外的输出动作。8. 从“初试”到“应用到项目”状态机在不同设备上的落地经验8.1 小型设备全自动螺丝机案例说一个我实际做过的例子。一台四工位转盘式自动锁螺丝机整个流程按工位划分。转盘每次旋转90度四个工位分别是“上料”“锁螺丝”“检测”“下料”。传统写法要用一个计数器加一堆转移条件因为转盘的动作和工装动作互相耦合中途加暂停逻辑非常痛苦。用状态机后我把转盘动作用一个子步骤来管理工装动作在另几个子步骤里做。转盘转到新工位状态机判断工装是否就位、螺丝是否送料到位的条件然后执行锁螺丝动作。再转到下料工位开夹、放料、回位。整个自动运行状态有八个子步骤代码量并不大但逻辑异常清晰。后来客户要求加“锁螺丝不合格时自动跳过”的功能我只在检测子步骤里加了一个判断不合格时把状态直接转移到下料前的安全位置一个分支就解决了。8.2 中型设备包装线案例另一台包装线项目设备本身有上料传送带、称重工位、封箱机和喷码机四个部分。四个部分原来各写各的逻辑联调的时候发现互相干扰。比如封箱机还没完成传送带已经开始送下一箱导致卡箱。用状态机统筹之后把整个设备当作一条完整产线来处理每个部分对应一个子状态。传送带送料完成后必须等待封箱机反馈完成信号状态机才会给出下一步指令。每台子设备内部保留原有逻辑但在总控状态机层面统一协调时序。这次改造后产线的循环节拍从原来的25秒提高到21秒而且卡箱现象完全消失。8.3 老设备改造状态机如何融入存量程序如果你接手的是已经在运行的设备程序是别人写的梯形图堆叠机直接推翻重来风险太大。我的建议是不要着急把全部逻辑改成状态机而是找一个合适的切入点。比如设备原本用M继电器维护自动流程状态我可以观察它的M继电器用到了哪些如果已经用了M100到M130说明自动流程大概有三十个步骤这种程序以后任何改动都要小心翼翼。合理的方式是保持现有的自动流程逻辑不变但在外层加一个总控状态机把自动模式的进入条件、退出条件、报警联动、手动切换统一纳入状态机管理。这样相当于给存量程序增加了一个“调度层”改动范围被限制又能逐步向状态机架构迁移。9. 回看“初试”的三个收获与一个建议这篇文章从环境准备讲到代码结构再讲到调试技巧和项目落地核心其实就三句话状态机用状态变量替代散落的M继电器用CASE分支保证状态互斥用统一的转移条件管理流程跳转。理解了这三件事在Sysmac Studio里写状态机就成功了一大半。最后给想试一下的同行一个建议不要等设备复杂到失控了再改架构。下次接手一台新设备哪怕只有十个步骤的流程也尝试用状态机来写。选一种自己熟悉的语言把状态列表画在纸上然后翻译成代码。第一次写状态机会觉得多此一举但写完第三台设备之后你会发现PLC程序的复杂度终于可以被工具和结构管理住而不是靠人脑硬抗了。状态机的路是越走越宽的从设备状态到产线调度从欧姆龙到其他品牌PLC这套思路都能复用。祝愿各位把状态机练成肌肉记忆少走我当年走过的弯路。