1. 为什么PLC程序里要用状态机
先说个普遍现象:很多做非标设备、小型自动化项目的工程师,一开始写的PLC程序都是“一锅烩”。什么叫一锅烩?就是整个程序里到处都是M继电器和置位复位指令,气缸伸出去一个SET,缩回来一个RST,电机启动一个线圈,停止又是另一个线圈。程序写的多了,气缸多了,工位多了,你会发现M继电器根本不够用,而且最要命的是——你根本不知道某个M继电器在哪些地方被置位了,又在哪些地方被复位了。排查问题的时候,只能一个程序段一个程序段地翻,翻到头晕。
这个问题的本质,在于程序里的“状态”是隐性的、分散的。每个M继电器都代表一个隐含的中间状态,但没有任何地方能总览整个设备当前处于什么阶段。一旦设备动作逻辑复杂一些,比如有自动、手动、回原点、暂停、复位、报警恢复这些模式来回切换,或者有多个工位的节拍互相配合,程序逻辑就会迅速膨胀,变成一团乱麻。
状态机就是用来解决这个问题的。状态机的核心思想,就是把设备的运行过程拆解成若干个明确的“状态”,然后规定状态与状态之间的跳转条件。按照这种方式写出来的程序,在任何时候你都清清楚楚地知道设备在干什么、下一步能去哪一步、什么条件触发这个跳转。好处有三点,第一是逻辑清晰,代码的可读性大大提升,换一个人来维护也不会骂娘;第二是排查故障方便,现场出了问题,你只需要看PLC当前处于哪个状态,结合跳转条件就能很快定位问题;第三是结构稳定,不会出现那种“这个M继电器不知道被谁复位了”的莫名其妙的偶发故障。
Sysmac Studio是欧姆龙NJ/NX系列PLC的编程软件,它支持IEC 61131-3标准,可以写ST语言(结构化文本),也支持FB(功能块)、面向对象编程(局部类等)这些高级特性。相比传统CX-Programmer的梯形图,Sysmac Studio里做状态机是非常顺手的,因为它有ENUM(枚举类型),有STRUCT(结构体),还有完整的ST文本编辑环境。你可以用非常接近高级语言的写法去组织状态逻辑,而不是像用梯形图那样靠辅助继电器堆砌。
这篇文章就是我自己的一个实践记录。我用欧姆龙NX系列PLC,在Sysmac Studio里写了一个小的状态机Demo,用这个例子来讲清楚状态机的写法思路、具体代码实现、调试方法和踩坑心得。如果你以前用梯形图写逻辑写够了,想换个思路,这篇文章应该能给你不少参考。
2. 在Sysmac Studio里搭建状态机的基础环境
2.1 创建工程与选择PLC型号
在动手写状态机之前,先把软件环境和工程建好。Sysmac Studio的界面和传统CX-Programmer差别很大,整体风格更接近Visual Studio那种IDE的感觉。左侧是“多视图资源浏览器”,变量表、程序、任务、设备配置都在这里管理,不是像老旧软件那样一上来就是一段一段的梯形图。
打开Sysmac Studio后,新建工程,选择PLC系列。以我手头的NX102-1200为例,这个型号属于NX1P2之后的进阶版,支持EtherCAT总线,最多带4轴,价格和性能都比较适合中小型设备。在“创建工程”对话框里,选择“NJ/NX系列”,然后选择对应的CPU型号,再设置好单元版本。这里有个细节,单元版本会影响指令集和功能块支持,如果你的Sysmac Studio版本比较新,建议直接选最新的单元版本,功能更全。
建完工程之后,Sysmac Studio会自动生成一个“程序”区,默认有一个Program0。在这个Program0下面会有一个“梯形图/ST”的目录,里面默认建了一个名为“Program0”的程序体。你可以右键重命名,比如把主程序命名为“Main”。这个MainProgram体可以切换语言——右键程序体,在属性里可以选择“梯形图/Ladder”或“ST”。我建议状态机的主逻辑用ST,全局变量和I/O映射用梯形图或者其他结构化方式。原因很简单,状态跳转逻辑用ST的CASE语句写起来非常顺手,梯形图写状态跳转会给你带来无穷无尽的烦恼。
2.2 任务周期配置与看门狗设置
状态机的运行离不开任务周期的设定。在Sysmac Studio里,所有程序都是在任务(Task)里周期执行的。打开“配置和设置”→“任务设置”,可以看到系统默认有“主任务”和“定期任务”两类。主任务是最高优先级的,EtherCAT刷新、运动控制指令都在主任务里执行;定期任务则是按设定的周期来执行,比如1ms、2ms、4ms、10ms等。
状态机程序一般放在主任务或者优先级较高的定期任务里,任务周期的选择取决于你的设备对响应速度的要求。如果是气缸、电机启停这种毫秒级的控制,1ms到4ms都可以;如果涉及EtherCAT轴的运动控制,最好放在主任务里,跟运动控制刷新频率保持一致。
还有一点要特别注意:Sysmac Studio默认开启了看门狗(Watchdog)功能。看门狗的目的是监测程序是否在设定时间内完成了扫描。如果你的状态机逻辑里写了Wait循环、或者用了FOR循环等待什么条件,很容易触发看门狗超时,导致CPU报错停机。这个坑我踩过,后面会详细说。所以,在开始写状态机之前,先把任务周期和看门狗时间设定合理,免得调试过程中频繁报警。
2.3 变量规划与I/O映射
写状态机之前,还有一件事要做:规划好输入输出变量。我习惯的做法是,把所有的物理输入输出点单独做一层映射,程序内部不直接引用物理地址,而是用专门的全局变量去对应。比如:
- 物理输入:
_IO_StartButton(启动按钮,接在X0上) - 物理输出:
_IO_CylinderA_Out(气缸A伸出,接在Y0上)
程序里所有状态判断都用这些带前缀的映射变量,而不是直接写X0和Y0。这么做的目的是方便以后改接线。现场改一个传感器的常开常闭,很多时候只需要改映射,不用动程序逻辑。
在“全局变量表”里把I/O映射、中间变量、状态变量都定义好。状态机相关的变量也要在这里提前规划好,比如:
- 当前状态变量
- 跳转条件用到的输入信号
- 输出控制变量
我建议把状态变量搞成枚举类型,这样在ST里看起来非常直观,调试的时候状态值也是可读的符号,而不是一堆数字。枚举类型怎么定义,下一节详细说。
3. 状态机建模:先把设备流程拆清楚再动手写
3.1 用枚举类型定义设备状态
状态机的第一步,不是写代码,而是做状态建模。什么叫做状态建模?就是把设备从启动到停止,甚至从断电到上电,这一整段过程里可能处于的每一个阶段都罗列出来,然后思考清楚每个阶段的进入条件、驻留条件、退出条件。
我习惯用一个具体的设备来练习。假设现在有一个简单的自动送料机构,它由两个气缸和一个传送带组成。设备的工作流程是这样的:启动按钮按下后,传送带开始运转,把工件送到挡停位置;挡停位置的光电传感器检测到工件后,传送带停止,气缸A伸出把工件夹紧;夹紧到位后,气缸B伸出,把工件推入下一个工位;推料到位后,气缸B先缩回,然后气缸A松开;最后传送带重新启动,开始下一轮的送料。整个过程中,如果按下急停,所有动作立即停止;急停复位后,设备需要回到初始状态,然后重新启动。
就这么一个简单的流程,如果按照传统的梯形图写法,你可能需要七八个M继电器来记住“现在走到哪里了”。而用状态机来建模,只需要把流程中的几个稳定阶段提取出来:
- S_IDLE:待机状态,传送带停止,气缸都在原位,等待启动信号。
- S_RUN_CONVEYOR:传送带运转,等待工件到位。
- S_CLAMP_CYL_A:工件到位后,传送带停止,气缸A夹紧。
- S_PUSH_CYL_B:气缸A到位后,气缸B推出。
- S_RETRACT_CYL_B:气缸B到位后,气缸B缩回。
- S_RELEASE_CYL_A:气缸B缩回到位后,气缸A松开。
- S_WAIT_NEXT:气缸A松开到位后,这里是给操作员或下一道工序留一个缓冲,然后再回到待机状态或直接进入下一轮循环。
你可能会问,S_WAIT_NEXT和S_IDLE有什么区别?在功能上可能有时候是一样的,但是在语义上它们是有区别的。S_WAIT_NEXT意思是“本周期喂料流程已经完成,等待允许下一轮的条件”;S_IDLE是“设备处于上电后的初始待机,等待启动按钮”。把这两个状态分开,以后想加“连续自动循环”功能就非常方便——在S_WAIT_NEXT里直接跳回S_RUN_CONVEYOR,就是连续模式;跳回S_IDLE,就是单次模式。
这个区分很关键。状态机的好处之一就是:状态的划分越贴近设备的客观流程,后续扩展功能就越容易。
把这些状态定义为枚举类型,在Sysmac Studio里可以这样写:
在程序的“变量”区域里新建一个枚举类型,或者在全局变量表里右键“新建数据类型”,选择“枚举”。
枚举类型定义如下:
TYPE ST_AutoFeedState : ( S_IDLE := 0, S_RUN_CONVEYOR := 1, S_CLAMP_CYL_A := 2, S_PUSH_CYL_B := 3, S_RETRACT_CYL_B := 4, S_RELEASE_CYL_A := 5, S_WAIT_NEXT := 6 ); END_TYPE注意,枚举类型的底层本质是整数,这里我显式地给每个枚举值赋了初始编号,从0开始。这样做的目的是,以后如果要在HMI上显示状态或者用数值做诊断,你可以明确知道整数0到6分别对应什么状态,不至于因为编译器自动编号的规则变化而乱了。而且以后如果要在状态序列中间插入一个新的状态,显式编号也能避免后面的枚举值因为插入而整体偏移,这个习惯建议一开始就养成。
3.2 状态机的大脑:CASE语句结构
有了枚举类型,状态机的主程序结构就很清晰了。Sysmac Studio支持IEC 61131-3标准,所以可以用CASE语句对当前状态做分支处理。一个最基础的状态机主循环长这样:
// 状态机主程序,每个扫描周期执行一次 CASE stateFeed OF S_IDLE: // 待机状态:输出全部复位,等待启动条件 bConveyorRun := FALSE; bCylinderA_Out := FALSE; bCylinderB_Out := FALSE; IF bStartButton THEN stateFeed := S_RUN_CONVEYOR; END_IF; S_RUN_CONVEYOR: // 运转传送带,等待工件到位信号 bConveyorRun := TRUE; IF bWorkpieceDetect THEN bConveyorRun := FALSE; stateFeed := S_CLAMP_CYL_A; END_IF; S_CLAMP_CYL_A: // 夹紧气缸A bCylinderA_Out := TRUE; IF bCylinderA_OutDetect THEN stateFeed := S_PUSH_CYL_B; END_IF; // ……其余状态按此模式填充 END_CASE;这个结构非常直观。每个状态下面,第一件事是设定这个状态下各个输出的值,第二件事是判断是否满足跳转条件、应该跳转到哪个状态。这种写法的好处是,你打开程序一眼就能看到“当前状态接下来能去哪些状态”,整个状态跳转的逻辑都在一个CASE块里,不需要到处找M继电器。
可能有人会担心CASE语句这种集中式写法会导致代码行数暴涨。确实,如果一个设备有三十个状态,这个CASE块会非常长。但长不是问题,可读性差才是问题。状态多的情况下,你可以按功能把CASE块拆分成多个,比如一个“自动流程状态机”,一个“手动模式状态机”,一个“报警处理状态机”,然后用一个更上层的“运行模式状态机”来管理它们。这种嵌套式状态机是后面的大杀器,先按住不表。
3.3 状态机的时间基准:什么时候用定时器,什么时候别用
在状态机里,经常遇到需要等待某个动作完成、或者某个信号稳定维持一段时间的情况。这里有两个选择:用定时器,或者用计数累加。
Sysmac Studio里用TON(接通延时定时器)是常规操作,但它有一个特点:TON的定时器实例需要保持前面的条件持续为TRUE。在状态机里使用TON,需要注册一个TON实例的变量(FB实例),然后在每个状态里调用。比如在S_RUN_CONVEYOR状态里启动一个TON,延时2秒后如果工件还没到位,就判定为缺料报警,跳转到S_ALARM。
在Sysmac Studio的ST里,TON是这样的:
// 全局或局部声明 TON_bWorkpieceMissing : TON; // 在状态S_RUN_CONVEYOR中 S_RUN_CONVEYOR: bConveyorRun := TRUE; TON_bWorkpieceMissing( In := TRUE, PT := T#2S, Q => bWorkpieceMissingAlarmStart ); IF bWorkpieceDetect THEN bConveyorRun := FALSE; stateFeed := S_CLAMP_CYL_A; // 复位计时器 TON_bWorkpieceMissing( In := FALSE, PT := T#2S ); ELSIF bWorkpieceMissingAlarmStart THEN stateFeed := S_ALARM; END_IF;这里有一个关键细节,TON的In信号必须在离开这个状态之前给到FALSE,否则定时器会保持记忆。很多人第一次用会踩坑:状态都已经跳走了,定时器还在那跑,定时到点之后莫名其妙触发了一个跳转。所以,每次在状态内部使用定时器,离开状态之前一定要把定时器条件切断。
我用得更多的其实是另一种方式:利用扫描周期的累加来实现延时。状态机程序有自己的任务周期(比如1ms),你可以用一个整数变量来做计数,在状态内部每周期加1,达到设定阈值就执行跳转。这样做的好处是,延时的精确度不受定时器实例管理的限制,而且占用的变量就是一个INT或者DINT,不会因为同时开了很多定时器而搞不清哪个定时器是干什么用的。
如果你对定时精度有要求,那么用任务周期累加的方式完全可以保证精度。举个例子,在MODE=1ms的任务周期下,一个从0累加到5000的DINT变量,就是5000个扫描周期,对应5秒延时。需要注意,用任务周期累加,要求这个状态必须每一次扫描周期都被执行到,如果状态机里存在某种跳转把这段逻辑跳过,那计时就不准了。这一点要心里有数。
4. 状态机的几种典型写法对比与演进
4.1 用IF嵌套写状态机:最简单但容易失控
很多从梯形图转过来的工程师,第一次用ST写状态机,最容易写出来的其实是IF嵌套的写法,类似于:
IF stateFeed = S_IDLE THEN // 待机处理 ELSIF stateFeed = S_RUN_CONVEYOR THEN // 传送带处理 ELSIF stateFeed = S_CLAMP_CYL_A THEN // 气缸A处理 END_IF;这种写法思路很直接,用一连串的IF...ELSIF...来区分不同状态。在小程序里,比如只有两三个状态,这样写也没什么问题。但如果状态超过十个、十几个,这种写法会变得非常难维护。主要问题在于,IF...ELSIF的结构本质上是一个线性判断链,每次扫描周期从第一个IF开始一路判断到最后,效率不是问题,问题是阅读程序的人需要从第一个IF开始一路看到最后才能看到某个状态的处理逻辑,不方便快速定位。
而且,IF嵌套写状态机的时候,人很容易手滑把ELSIF写错,或者遗忘某个ELSE分支,导致状态落到真空区。这里说的真空区,就是没有任何一个IF分支匹配当前状态值,程序就静默地什么都不执行,输出全部保持前一个状态的值,设备就可能卡在那里不动。排查起来还特别麻烦,因为你看状态变量值,既不是状态0也不是状态1,而是一个什么都不是的数。
CASE语句从编译器的语义上就保证了“当前值匹配哪一个分支就执行哪一段”,如果值不在枚举范围内,编译器会提示或者你可以通过ELSE分支兜底。所以在Sysmac Studio的ST环境里,我建议一律用CASE语句作为状态机的骨架,不要用IF嵌套。
4.2 用BOOL触点组表示状态:梯形图思维的最后倔强
还有一种在梯形图里常见的“状态机”,就是定义一组BOOL变量,每个BOOL代表一个状态,通过置位复位来实现状态转移。
比如说,定义变量M0 = 待机,M1 = 传送带运行,M2 = 气缸夹紧等等,然后在梯形图里写:
- 待机条件下,SET M1,RST M0
- 下一条件下,SET M2,RST M1
这种方式本质上是状态机的“标志位法”,在我之前用CX-Programmer的时候也常这么干。它的问题在哪里?最突出的问题是:状态数目一多,M标志的置位和复位逻辑就会交叉引用,程序里出现“M100在任何地方都可能被复位”的情况。到了这时候就回到了文章开头说的那个问题——一锅烩,越维护越乱。
它的好处也有,就是梯形图的直观性,很多电工出身的朋友对置位复位非常熟悉,改用这种方式上手快。但如果你的项目是用Sysmac Studio做的,我强烈建议直接用ST + ENUM的方式来做状态机,步子迈大一点,用习惯之后你会发现回不去了,梯形图写状态跳转实在是太费劲了。
4.3 函数块封装:让状态机变成可复用的功能块
状态机写到后面,你会发现其实很多设备的状态流程是有共性的。比如送料机构的状态机、夹紧机构的状态机、出料机构的状态机,它们的逻辑骨架都一样,只是输入输出不同、状态数量不同。这个时候,你就可以把状态机封装成一个功能块(FB),在多个设备上复用,这就是Sysmac Studio面向对象思想的用武之地。
封装一个状态机FB,需要注意的点和写普通状态机不太一样。FB的内部变量要区分好:输入变量(VAR_INPUT)、输出变量(VAR_OUTPUT)、内部变量(VAR)。输入变量主要是跳转条件,输出变量主要是控制指令,内部变量就放当前状态。类的实例化之后,你需要选择是单实例还是数组实例。在Sysmac Studio里,FB可以定义为数组,这在多工位设备上是神器——一个工位一个FB实例,程序写一次,所有工位通用。
第一次封装状态机FB的时候,我建议拿一个最简单设备先练手,把FB的输入输出接口想清楚,再逐步增加复杂度。这个过程对后续的大型项目非常有帮助,因为你会发现,其实很多设备的动作逻辑都是状态机的排列组合。
4.4 嵌套状态机:如何管理模式和子流程
设备往往不止有自动流程,还有手动操作、回原点、报警处理、故障复位这些流程。如果你把所有流程都塞进一个大状态机里,状态数会爆炸,程序会变得极其臃肿。更合理的思路是用嵌套状态机:顶层的“运行模式状态机”只管运行模式,比如自动模式、手动模式、报警模式,而在“自动模式”内部再嵌套一个“自动流程状态机”,管具体的走料流程。
这在Sysmac Studio里实现起来并不复杂。顶层状态机用一个变量比如stateMode来区分模式,内部自动流程用stateFeed来区分流程步骤。在顶层状态机的AUTO模式下,才执行自动流程状态机的CASE;在手动模式下,自动流程状态机不参与输出。
嵌套状态机的好处是层次清晰,每个状态机只负责自己那层逻辑,修改任何一个层级的逻辑都不会影响其他层。排查问题的时候,先看顶层状态机当前处于什么模式,再看底层自动流程状态机当前处于哪个状态,问题定位一下子就缩小了。
关于嵌套状态机的层级深度,我的建议是不要超过三层。层次太深会让程序的可读性下降,除非你对整个结构非常熟悉,否则三层以上自己都会被绕晕。三层对于绝大多数自动化设备已经足够了。
5. 实操过程:一个自动送料Demo的完整实现
5.1 全局变量与I/O映射
接下来,我把前面说的自动送料机构,从头到尾写一遍,记录完整的实操过程。这里是完整变量表规划:
| 变量名 | 类型 | 注释 |
|---|---|---|
| bStartButton | BOOL | 启动按钮(物理输入映射) |
| bStopButton | BOOL | 停止按钮(物理输入映射) |
| bWorkpieceDetect | BOOL | 工件到位光电传感器(物理输入映射) |
| bCylinderA_InDetect | BOOL | 气缸A缩回到位传感器(物理输入映射) |
| bCylinderA_OutDetect | BOOL | 气缸A伸出到位传感器(物理输入映射) |
| bCylinderB_InDetect | BOOL | 气缸B缩回到位传感器(物理输入映射) |
| bCylinderB_OutDetect | BOOL | 气缸B伸出到位传感器(物理输入映射) |
| bConveyorRun | BOOL | 传送带运行输出(物理输出映射) |
| bCylinderA_Out | BOOL | 气缸A伸出电磁阀(物理输出映射) |
| bCylinderB_Out | BOOL | 气缸B伸出电磁阀(物理输出映射) |
| stateFeed | ST_AutoFeedState | 自动流程状态 |
| eStopReset | BOOL | 急停复位信号 |
| bRunEnable | BOOL | 自动运行允许 |
变量表在Sysmac Studio的“全局变量表”里定义。我习惯加前缀:b开头是布尔量,st开头是状态枚举变量,i开头的整数,r开头的实数。这样在ST程序里扫一眼变量名就能知道数据类型。
5.2 初始化逻辑
程序上电后,第一个扫描周期需要做一些初始化操作:把所有输出复位、状态机回到初始状态、定时器清零。在ST里,可以利用Sysmac Studio的“初始步进”或“事件任务”来做。更简单的做法是,在状态机主程序开头用一个首次扫描标志:
// 首次扫描初始化 IF NOT bInitComplete THEN bConveyorRun := FALSE; bCylinderA_Out := FALSE; bCylinderB_Out := FALSE; stateFeed := S_IDLE; bInitComplete := TRUE; END_IF;bInitComplete是全局变量,默认FALSE,程序第一次扫描时进入初始化块,执行完之后置为TRUE,之后再也不会进入。这个方式在Sysmac Studio里完全可行,需要注意的是,如果用其他程序段也想在首次扫描时初始化,这个变量要共享,避免各段重复初始化覆盖彼此的变量。
5.3 自动流程状态机完整代码
以下是我在Sysmac Studio里实际写的自动流程状态机完整代码。这个代码在ST语言里可以直接编译通过:
// ===================================================== // 自动送料机构状态机 // ===================================================== CASE stateFeed OF S_IDLE: // 待机状态:输出全部复位 bConveyorRun := FALSE; bCylinderA_Out := FALSE; bCylinderB_Out := FALSE; // 启动条件:启动按钮按下 + 所有气缸在原位 IF bStartButton AND bCylinderA_InDetect AND bCylinderB_InDetect THEN stateFeed := S_RUN_CONVEYOR; END_IF; S_RUN_CONVEYOR: // 传送带运行,等待工件到位 bConveyorRun := TRUE; bCylinderA_Out := FALSE; bCylinderB_Out := FALSE; // 工件到位后停止传送带,进入夹紧 IF bWorkpieceDetect THEN bConveyorRun := FALSE; stateFeed := S_CLAMP_CYL_A; END_IF; S_CLAMP_CYL_A: // 气缸A夹紧工件 bCylinderA_Out := TRUE; bCylinderB_Out := FALSE; // 夹紧到位后,进行推料 IF bCylinderA_OutDetect THEN stateFeed := S_PUSH_CYL_B; END_IF; S_PUSH_CYL_B: // 气缸B推出,把工件推到下一工位 bCylinderA_Out := TRUE; // A保持夹紧状态 bCylinderB_Out := TRUE; // 推料到位后,退出推料过程 IF bCylinderB_OutDetect THEN stateFeed := S_RETRACT_CYL_B; END_IF; S_RETRACT_CYL_B: // 气缸B缩回 bCylinderA_Out := TRUE; // A保持夹紧 bCylinderB_Out := FALSE; // B缩回到位后,松开A IF bCylinderB_InDetect THEN stateFeed := S_RELEASE_CYL_A; END_IF; S_RELEASE_CYL_A: // 气缸A松开 bCylinderA_Out := FALSE; bCylinderB_Out := FALSE; // A松开到位后,进入下一轮等待 IF bCylinderA_InDetect THEN stateFeed := S_WAIT_NEXT; END_IF; S_WAIT_NEXT: // 一个送料循环完成,在这里选择单次模式还是连续模式 bConveyorRun := FALSE; bCylinderA_Out := FALSE; bCylinderB_Out := FALSE; // 如果启动按钮还按着(或者一个允许循环的信号存在),直接进入传送带运转 IF bStartButton OR bRunEnable THEN stateFeed := S_RUN_CONVEYOR; ELSE stateFeed := S_IDLE; END_IF; ELSE: // 兜底分支:如果状态值异常,回到待机 stateFeed := S_IDLE; END_CASE;这段代码里有两个细节值得注意。一个是状态内对输出变量的赋值。我在每个状态里都对三个输出变量做了统一赋值,这样即使状态之间跳转发生误差,输出也不会产生意料之外的保持。比如在S_RUN_CONVEYOR里,bCylinderA_Out和bCylinderB_Out必须显式写FALSE,否则如果上一个状态是S_CLAMP_CYL_A,B的输出会一直保持TRUE。这就是在状态机里最需要注意的“输出泄漏”问题。
另一个细节是ELSE分支。CASE语句加ELSE兜底,如果因为某种原因状态变量变成了一个不在枚举范围内的值(比如被通讯或者HMI强行写入了一个非法值),程序不会卡在真空区,而是自动回到S_IDLE。这对于设备安全非常重要。
5.4 状态跳转的“同周期二次跳转”陷阱
在仔细看上面的代码时,你可能已经注意到一个问题:在S_WAIT_NEXT状态里,如果条件满足,我直接跳转到了S_RUN_CONVEYOR。但是在同一个扫描周期里,跳转到S_RUN_CONVEYOR之后,这个CASE语句已经执行完了,S_RUN_CONVEYOR里的动作不会被处理,需要等到下一个扫描周期才执行。
这种跳转方式叫“状态末尾跳转”,通常没问题,因为PLC处理的速度很快,差一个扫描周期(1ms级别)对现场来说完全无感。但如果你有这样的需求:从状态A跳转到状态B的同一个周期内,B状态的处理逻辑也要立刻执行,这就需要在编程方法上做一些处理。
Sysmac Studio的ST语言里,CASE语句本身的特点就是一个周期只能执行一个分支。如果你需要立即执行跳转后的状态,可以引入“中间变量NextState”的方式:在每一个状态的末尾,不直接修改stateFeed,而是赋值给NextState,在整个CASE语句结束后,统一把NextState写入stateFeed,然后再做一次轻量级的二次分发(或者直接循环处理)。
这种做法的复杂度比较高,大多数设备根本不需要。我自己的经验是:除非你的设备有那种“传感器信号持续时间极短、必须在信号变化的同时完成两段状态的动作”这种苛刻要求,否则直接在每个周期末尾跳转就够了。PLC的扫描周期足够快,绝大多数动作差一个扫描周期,机械上根本感知不到。
但有一个情况需要特别注意:跳转条件如果是瞬时脉冲信号,比如光电传感器只遮挡了一下,那么你在这个扫描周期检测到信号、跳转到下一个状态,但是下一个扫描周期传感器信号已经消失,这会影响你在新状态里的逻辑判断吗?答案是:要看情况。如果新状态的开始动作不依赖这个瞬时信号,就没事;如果新状态的第一件事是检测“工件还在不在”,那传感器信号消失可能会导致逻辑误判。
解决办法很简单,就是给脉冲信号加保持。在Sysmac Studio里,可以用一个置位锁存的方式,把瞬时信号锁存为“本次流程需要处理的信号”,等状态机走到合适的位置再复位掉。这个手法在实际工程中非常常用,我称它为“事件锁存”。
5.5 用事件锁存处理瞬时信号
回到自动送料的例子里,启动按钮如果用的是普通按钮而不是带锁按钮,按下去的瞬间最多也就是几十毫秒,PLC扫描周期如果是1ms,几十个周期肯定能捕捉到。但如果设备比较大,扫描周期被延长到10ms甚至更长,按钮按下去的脉冲可能在两个扫描周期之间就消失了,程序可能会漏掉这次启动请求。
一个稳妥的做法是,在程序里对启动按钮做锁存:
// 在程序开头的地方,检测启动按钮上升沿 IF bStartButton AND NOT bStartButton_Latched THEN bStartButton_Latched := TRUE; END_IF;然后在状态机的S_IDLE状态里,判断条件从IF bStartButton改成IF bStartButton_Latched,进入S_RUN_CONVEYOR之后,在合适的时机复位bStartButton_Latched。
Sysmac Studio里也支持上升沿检测指令,用法是R_TRIG功能块。R_TRIG实例变量的使用方式和TON类似,需要声明一个实例,每次扫描周期调用一次,然后访问Q输出。在实际的工程代码里,用R_TRIG比手动锁存更易于阅读,但它要求“每一个扫描周期都要调用一次”,如果某个状态下忘记调用,上升沿就检测不到了。如果你觉得麻烦,用我上面那种手动锁存方式,反而更可控。
6. 状态机里的防呆设计与安全互锁
6.1 气缸动作的安全互锁
写状态机代码的时候,很多人容易陷入一个思维误区:觉得状态跳转正常了,设备就能正常运行。但现场设备不是这么回事,机械设备有惯性、有卡滞、有传感器失灵,这些意外情况都需要在程序里考虑。
最典型的是气缸互锁。还拿自动送料机构举例,气缸A夹紧和气缸B推出这两个动作,在物理上不允许同时进行,否则会撞坏工件或者损坏机械结构。在状态机里,S_CLAMP_CYL_A状态下B输出是FALSE,S_PUSH_CYL_B状态下A输出是TRUE,看起来正常流程中不会出现“B伸出时A没有夹紧”的情况。
但代码逻辑是代码逻辑,物理世界是物理世界。万一出现程序跑飞、状态变量被HMI写入异常值、或者中间继电器卡死,电磁阀可能会出问题。所以我在实际项目里,除了状态机逻辑,还会在输出控制的最末端加一道“硬互锁”逻辑。在Sysmac Studio的梯形图里加一个互锁网络,或者在ST程序最后加一行:
// 最终输出控制:状态机逻辑之外的安全互锁 bCylinderA_Out_Final := bCylinderA_Out AND NOT bCylinderB_Out; bCylinderB_Out_Final := bCylinderB_Out AND NOT bCylinderA_Out;然后把物理输出映射到bCylinderA_Out_Final而不是直接映射bCylinderA_Out。这算是一道非常可靠的软件防线,即使状态机内部逻辑出现任何异常,两个气缸的电磁阀也不可能同时得电。
更进一步,我建议在电气回路里也做硬互锁,通过中间继电器常闭触点串联。软件互锁和电气互锁双保险,才是设备防护的正解。软件永远只是最后一道防线而不是唯一防线。
6.2 传感器异常与超时保护
状态机里经常出现的情况是,跳转条件一直不满足。比如气缸A已经伸出了,但伸出到位传感器一直没有信号。这时候如果程序只是傻傻地等,设备就会停在S_CLAMP_CYL_A状态不走了,如果操作员没有盯着屏幕,可能半天都发现不了。
所以,每一个需要等待传感器信号的状态,都应该加上超时保护。超时保护的做法不复杂,就是在进入状态的第一个扫描周期启动一个定时器,或者开始计数,如果超过设定时间(比如5秒),说明该动作没有在预期时间内完成,设备应该进入报警或停止状态。
在Sysmac Studio里实现超时保护,我的做法是在每个需要等待的状态里,用类似下面的逻辑:
S_CLAMP_CYL_A: bCylinderA_Out := TRUE; bCylinderB_Out := FALSE; // 进入本状态后开始计时 IF stateFeedPrev <> S_CLAMP_CYL_A THEN iTimeCount := 0; ELSE iTimeCount := iTimeCount + 1; END_IF; // 正常跳转条件 IF bCylinderA_OutDetect THEN stateFeed := S_PUSH_CYL_B; // 超时保护:默认任务周期1ms,5000个周期=5秒 ELSIF iTimeCount > 5000 THEN stateFeed := S_ALARM; bAlarmCode := 16#A001; // 气缸A伸出超时 END_IF;这种写法用到了stateFeedPrev(上一个周期的状态值)来判断“是否刚刚进入本状态”,如果刚进入,计数清零,否则累加。Sysmac Studio的ST里没有内置的这个变量,需要自己在程序开头保存一下:
stateFeedPrev := stateFeed;需要说明的是,这里的iTimeCount的累加依赖于状态机每次扫描周期都执行到S_CLAMP_CYL_A分支。如果有一个更高优先级的任务把主程序抢占了,或者主任务本身发生了延迟,那么iTimeCount累加的实际时间会大于理论时间。对于气缸动作超时这种秒级判断,这个误差可以接受。如果你需要非常精确的超时判断,建议用操作系统的时间函数或者硬件定时器来做。
6.3 急停与报警状态的处理
很多工程师写状态机,会把“急停”当成一个普通的输入信号来处理,这在逻辑上是行不通的。急停不是输入,它是最优先级的“停机命令”,它的优先级高于一切状态,包括自动、手动、回原点。任何状态下按下急停,所有输出必须立即失电,设备必须马上停止运动。这不是状态机逻辑能解决的问题,它需要的是硬件安全回路。
在实际项目中,急停按钮应该接在安全继电器回路里,物理断开设备的主电源或伺服使能,同时在PLC里也接入急停信号的常闭触点,用于程序逻辑感知急停状态。在状态机程序里,急停信号作为最高优先级条件,必须在CASE语句之前做判断:
// 急停处理,优先级高于状态机 IF NOT bEStopOK THEN // 急停触发:所有输出立即复位 bConveyorRun := FALSE; bCylinderA_Out := FALSE; bCylinderB_Out := FALSE; // 状态机回到一个特殊的急停状态,等待复位 stateFeed := S_ESTOP; END_IF;注意这里的bEStopOK是急停未触发时的一个软信号,可以取反急停输入。在状态机外部先做急停判断,如果急停触发,直接把所有输出复位并进入S_ESTOP状态。在S_ESTOP状态下,除了急停复位信号之外,不接受任何其他跳转条件。这个设计的思路是:急停触发后,即使操作员按了启动按钮,设备也不会自动重新运行,必须先完成急停复位和故障清除,然后回到待机状态,再重新启动。
这样做的好处是防止“急停复位后设备立即自行启动”这种致命危险。很多设备的安全生产规范里明确要求:急停复位后,必须人工重新按下启动按钮才能恢复自动运行。这个逻辑用状态机来实现非常自然,但是用梯形图的M继电器方式实现,就很容易出现漏考虑的情况。
7. 调试工具与常见问题排查
7.1 Sysmac Studio的数据监视与在线修改
程序写完之后,接下来是调试阶段。Sysmac Studio提供了一套比较完整的调试工具。最常用的是“数据监视窗口”,可以在线监控所有全局变量、局部变量的实时值。调试状态机的时候,可以直接把stateFeed这个枚举变量拖到监视窗口,在线看到它当前的值。在Sysmac Studio里,枚举类型在监视窗口会直接显示符号名(比如S_RUN_CONVEYOR),这对于调试来说非常友好,不用自己对着编号猜状态。
除了普通的监视,Sysmac Studio还支持“程序在线修改”和“变更程序运行”。也就是说,程序下载到PLC运行之后,你可以随时在开发环境里改ST代码,修改完成后选择“变更程序运行”而不是“传送并运行”,PLC不会停机,新的代码会立即生效。这个功能在调状态机时太有用了,每次改完跳转条件,不需要重新下载程序、不需要复位PLC,点一下“变更程序运行”就好了。不过要注意,“变更程序运行”不是所有的改动都支持,比如新增变量、修改任务设置这些还是需要完整传送。另外,频繁在线修改代码会把CPU的存储区重新分配,如果项目很大改得很频繁,可能会遇到内存碎片的情况,这时候做一次全量下载就能解决。
7.2 常见问题速查表
下面的表格是我在状态机调试中遇到过的典型问题、原因和解决办法,整理出来供参考:
| 问题现象 | 最可能的原因 | 排查与解决办法 |
|---|---|---|
| 某个状态一直不跳转 | 跳转条件中的传感器信号没有到位,或信号极性反了 | 用数据监视窗口查看对应的BOOL变量,确认物理信号已进入PLC |
| 状态机跳转,但输出没有变化 | 输出可能在状态机之外被其他程序段覆盖了 | 检查输出变量是否有多个程序段在赋值,这是最常见的冲突原因 |
| 状态从正常值变成非法值 | 有通讯写入或HMI写入覆盖了状态变量 | 在HMI/上位机程序里禁止对状态变量写入,或者加密保护 |
| 动作执行一两步就停了,且状态很正常 | 跳转条件中某一步的传感器信号是瞬时脉冲,在下一个扫描周期消失 | 用信号锁存或沿检测来处理瞬时信号 |
| 看门狗超时报警 | 状态机里写了等待循环(如WHILE/等待),占用了太长的扫描时间 | 不要用等待循环,改用状态停留+定时器/计数的模式 |
| TON定时器在状态跳走后还在计时 | 离开状态之前没有把TON的In信号置FALSE | 检查所有使用TON的状态,确保离开前切断In信号 |
| 气缸在状态跳跃时出现瞬间误动作 | 状态中未显式给所有输出赋默认值,导致输出保持上一个状态的值 | 每个状态都要对每个输出统一赋值,不要只赋值变化的量 |
| 枚举变量在监视窗口不显示符号名 | 监视窗口变量类型可能被强制转换成了整数 | 删除监视项重新添加,或检查变量类型是否正确引用了该枚举类型 |
7.3 利用Sysmac Studio的“程序联锁”与仿真调试
Sysmac Studio还有一个非常有用的功能:模拟器。在没有实物PLC的情况下,可以在软件里启动模拟运行,PC会模拟PLC的指令执行,你可以用模拟器完成大部分状态机的逻辑验证。我自己经常在写状态机的时候先在模拟器上把跳转条件用变量强制(Force)的方式模拟一遍,比如强制bWorkpieceDetect为TRUE,看状态机是否正确从S_RUN_CONVEYOR跳到S_CLAMP_CYL_A。这样在去现场之前就能消灭大部分逻辑Bug,到了现场只需要调试传感器信号和机械动作的配合,效率提升非常明显。
模拟器有一点要注意:它不能模拟物理I/O的响应时间,也不能模拟通讯总线(比如EtherCAT)的真实刷新周期。所以涉及运动控制的程序,在模拟器上运行的结果只能作为逻辑参考,不能作为最终性能依据。但是单就状态机逻辑来说,模拟器已经足够强大。
调试状态机时还有一个实用技巧:把状态跳转的历史记录下来。Sysmac Studio本身没有内置的“状态历史记录”功能,但你可以用一个数组变量作为环形缓冲区,每次状态变化时,把当前状态值和变化时刻存入数组。这样设备出现故障的时候,打开数据监视窗口看看这个数组,就能复盘出设备在故障前经历了哪些状态、每一步停留了多久。这是排查偶发故障的神器。
再分享一个小细节。Sysmac Studio的“变更程序运行”在状态机调试中非常快,但是每次修改之后,最好做一次“程序检查”,看有没有改了跳转条件导致变量未使用的警告。我遇到过好几次,删掉了一个传感器变量的引用,但是忘了删除全局变量表里的定义,结果程序下载到现场PLC之后,那个变量一直显示为TRUE,导致状态机逻辑异常。这种低级错误浪费了很多时间,现在每次修改完都会先检查一遍变量引用。
8. 从Demo到工程化:状态机写法的演进心法
写到这里,自动送料的状态机Demo已经完整了。但我想再多说一点自己的心得体会,因为光会写一个Demo和能在工程中灵活使用状态机,之间还是有一段距离的。
第一个体会是:状态机的难点不在写代码,而在分状态。什么是分状态?就是你能不能把设备的整个动作流程,拆成一个一个互斥的、边界清晰的稳定状态。拆得好,代码自然顺滑;拆得不好,就容易出现两个状态之间的边界模糊,或者一个状态需要等两个几乎同时到达的信号,逻辑就开始别扭。拆状态有一个行之有效的技巧:不要按“动作”去分状态,要按“稳定条件”去分状态。比如“气缸A正在伸出”不是一个状态,因为气缸伸出的过程是不稳定的,你应该拆成“气缸A缩回在原位,等待伸出指令”和“气缸A已伸出到位,保持夹紧”这两个状态。以稳定条件为状态,设备的每个阶段都有明确的物理含义,跳转条件也会变得很清晰。
第二个体会是:状态机的输出赋值,要追求“完全确定”。每一个状态,对每一个输出变量都要有确定的值,不能有“这个输出在这个状态下保持原样”的模糊地带。你可能会问,有些状态不需要改变某个输出,那直接不赋值不就行了吗?是可以不赋值,但我不建议这么做。原因前面也提到了,状态机在运行过程中状态是不断切换的,如果某个输出只在部分状态里赋值,在另一些状态里不赋值,那它的值就取决于“上一个赋了这个输出的状态”,这种隐式依赖是Bug的温床。所以我的习惯是,每个状态的CASE分支第一件事,就是把所有输出先按该状态的默认值全部赋值一遍,再写需要变动的那些。代码会变得冗长一些,但可读性和健壮性提升不止一个档次。
第三个体会是:状态机的扩展性远好于梯形图。传统梯形图程序,如果要增加一个新的工作模式,往往需要在原来的置位复位逻辑里小心翼翼地加触点。而状态机天然支持“加状态”的扩展模式,你只需要在枚举里加一个新的状态,在CASE里加一个分支,再把跳转条件接上,原来的代码完全不用动。这一点对我们做非标设备的来说非常重要,因为客户改需求是家常便饭,状态机结构让改需求不再是一场噩梦。
第四个体会也是最后一个:不要为了用状态机而用状态机。状态机适合逻辑复杂、状态多的场景,但如果你就是一个水泵启停,启保停三句话的事,硬套状态机反而是脱裤子放屁。工具是为人服务的,不要被方法论绑架。我的判断标准很简单:如果设备有超过两三个连续动作需要按顺序执行,或者有自动/手动/报警多种模式需要切换,那状态机就是值得的;如果只是简单的泵阀控制,按常规方式写就好。
Sysmac Studio本身提供了非常强大的IEC结构化编程环境,用状态机这种思想来组织的程序,无论是可读性、可维护性还是可扩展性,都远胜于传统的梯形图堆继电器。如果你以前没用过这种方式,建议从一个小的Demo开始练手,把状态划分、CASE结构、超时保护、信号锁存这些基本功练扎实,再慢慢过渡到多状态、多模式、嵌套状态机的复杂项目。
我在第一次用Sysmac Studio写状态机的时候,其实写了改、改了删,折腾了不少时间。但现在回头看,那些折腾都很值得,因为用状态机写出来的程序,一次调试通过率真的高很多,现场排查问题也快很多。这大概就是结构化思维带来的红利吧。