
接手汇川PLC项目这几年我总结出一条规律程序跑不起来八成不是伺服没调好而是设备状态没理清。尤其是带转盘、多工位、多模式的中型设备如果状态全靠一堆M继电器瞎置位最后一定是一边改一边乱出一大堆“说不清为什么会这样”的毛病。后来我把逻辑重构成状态机把“设备现在处于什么状态”和“叠加在设备上的额外状态”分开管理问题一下就收敛了。这篇文章就拿我在汇川PLC上写的设备状态机做例子把状态机的划分方式、实现代码、典型案例、常见坑一次讲清楚适合刚接触状态机编程的PLC工程师也适合正在为设备逻辑发愁的朋友。1. 设备状态机是什么为什么PLC程序越复杂越离不开它1.1 先搞清楚“设备状态”和“工艺步骤”不是一回事很多设备程序越写越乱第一个原因就是把“设备状态”和“工艺步骤”混在一个变量里管理。设备状态是设备的宏观处境一个状态可以包含十几个工艺步骤。比如“自动运行”是一个状态在这个状态下设备按顺序执行上料、定位、点胶、固化、下料这些步骤“暂停”是另一个状态在这个状态下不管刚才做到哪一步整个流程先挂起执行器进入安全保持。如果反过来把“正在上料”“正在点胶”“正在固化”“正在等待下料”都当成独立的状态来切换程序就会非常痛苦。因为中间插入一个“暂停”就要给十几个状态各写一个暂停分支中间插入一个“报警”又要给十几个状态各写一个报警分支。每加一个全局条件就要把所有状态翻一遍。正确的做法是宏观状态只分五个六个大区比如空闲、回原点、自动运行、暂停、报警。在这个大区内部再用一个“步骤指针”去驱动工艺步骤。这样全局条件只需要在状态机外壳上处理一次不再散落在几十个步骤里。这个认知是整个状态机设计的地基搞反了后面全乱。1.2 单独状态与叠加状态设备的两套坐标系设备的状态可以从两个维度去观察。第一个维度是“设备当前处在哪个互斥阶段”同一时刻只可能是其中一个空闲、启动、运行、暂停、报警。这一类状态叫单独状态也叫主状态。第二个维度是“哪些额外条件正叠加在设备上”这样理解更直观安全门被打开了、急停被按下了、加热温度到了、缺料报警了、上位机正在远程操作。这些条件可以同时成立互不排斥我把它叫叠加状态。用人的状态来类比就很容易消化。一个人现阶段是“工作中”还是“休息中”这是单独状态不可能同时既在工作又在休息。但这个人可以同时是“感冒中”“戴着眼镜”“心情不错”这些是叠加状态感冒了不代表不能戴眼镜戴眼镜也不代表心情不好。设备也一样一台点胶机可能正处于“自动运行”这个单独状态同时又叠加了“安全门被打开”“温度已到位”“气压正常”这些标志每个标志都独立存在独立影响相应的动作逻辑。单独状态负责“设备在干什么”叠加状态负责“设备处于什么条件、有哪些限制”。两套坐标系正交之后程序的表达能力一下子强了很多。之前那种“状态太多了搞不清”的混乱大多是因为把这两类东西揉进了同一个状态变量里。1.3 不用状态机时现场调试有多折磨说实话早期调设备我也用过土办法用十几个M中间继电器表示各种工况这里置位一个那里复位一个。结果就是设备动作经常出现“不可能出现的组合”比如运行中M10和停机中M11同时为ON报警了设备还在动。排查的时候只能一步一步在线监控看哪个M被谁偷偷置位了一个晚上就耗在这种毫无技术含量的事情上。还有一个痛点需求一变设备要增加一个“缺料暂停”就得把所有跟自动运行有关的M条件全部改一遍。改漏一个设备就可能在缺料的情况下继续跑下一个工位轻则产品报废重则撞坏治具。状态机的价值就是把所有状态转移收敛到一个地方每个跳转都有明确的触发条件不会出现“谁都能改状态”的失控局面。2. 汇川PLC实现状态机的核心思路主状态加叠加状态2.1 变量规划主状态用INT编号叠加状态用BOOL标志在汇川PLC的编程软件里我习惯把状态机独立放进一个程序段或功能块中变量规划是第一步。主状态用一个INT变量比如eMainState用数字编号代表不同的单独状态。叠加状态用一组BOOL变量每个BOOL代表一个独立条件。状态编号不是随便给的我强烈建议用0、10、20、30这样的间隔编号不要用0、1、2、3的连续编号。原因有两个一是中间留了空位以后在“空闲”和“运行”之间想插入一个“预启动”状态可以直接用5不用把后面所有状态号全改一遍二是配合HMI的枚举显示功能状态号可以映射成文本间隔编号不会影响映射但可读性好很多。变量声明大致长这样VAR eMainState : INT : 0; (* 主状态0空闲 10启动 20运行 30暂停 40报警 *) eMainStateOld : INT : 0; (* 上一周期主状态用于检测状态跳变 *) bStart : BOOL; (* 启动按钮 *) bPauseReq : BOOL; (* 暂停请求 *) bResetReq : BOOL; (* 故障复位请求 *) bHomeOK : BOOL; (* 回原点完成 *) bEStop : BOOL; (* 急停硬线信号 *) bSafeDoorOpen: BOOL; (* 安全门打开 *) bAlarmActive : BOOL; (* 叠加状态存在报警 *) bHeatingReady: BOOL; (* 叠加状态加热温度到 *) bAutoRunning : BOOL; (* 叠加状态自动运行中 *) END_VAR这里有个容易忽略的细节所有主状态和叠加状态变量尽量集中声明不要东一个西一个。后期维护的时候翻开变量表一眼能看到“设备现在的宏观状态有哪些”比在一个大型梯形图里翻半天找变量要省力得多。2.2 主状态转移用CASE语句别再用SET/RST一堆M汇川PLC支持IEC标准的结构化文本也就是ST语言。主状态的转移逻辑我用CASE语句来写一台中等复杂度的设备核心转移代码大概几十行CASE eMainState OF 0: (* 空闲 *) bAutoRunning : FALSE; IF bStart AND bHomeOK AND NOT bAlarmActive THEN eMainState : 20; END_IF 10: (* 启动/回原点 *) bAutoRunning : FALSE; IF bHomeOK THEN eMainState : 0; END_IF 20: (* 自动运行 *) bAutoRunning : TRUE; IF bEStop THEN eMainState : 40; ELSIF bPauseReq OR bSafeDoorOpen THEN eMainState : 30; ELSIF bCycleDone THEN eMainState : 0; END_IF 30: (* 暂停 *) bAutoRunning : FALSE; IF NOT bPauseReq AND NOT bSafeDoorOpen AND NOT bEStop THEN eMainState : 20; END_IF 40: (* 报警锁停 *) bAutoRunning : FALSE; IF bResetReq AND NOT bAlarmActive THEN eMainState : 0; END_IF END_CASE为什么用CASE而不是用梯形图的SET/RST因为CASE的转移对象只有一个变量eMainState不可能同时等于20又等于40天然不会出现老式M继电器程序那种“两个状态同时为ON”的情况。状态转移到哪里条件清清楚楚哪个状态进来、哪个状态出去维护的人不用从头到尾捋几十个网络。还有一点很重要CASE分支里我只处理状态转移和叠加状态的部分标志不直接给物理输出赋值。物理输出统一放在CASE之后由专门逻辑处理。这样做的原因后面第四部分详细说这里先记住原则。2.3 叠加状态用BOOL标志独立计算不与主状态“你死我活”叠加状态的赋值逻辑相对独立可以放在CASE同周期内、CASE之前或之后具体怎么放看习惯但一定要保证每个周期都会被刷新一遍。我的习惯是放在CASE之前因为主状态转移需要用到这些叠加状态作为条件。bSafeDoorOpen : NOT bDoorClosed; bAlarmActive : bAlarmTemp OR bAlarmEStop OR bAlarmMotor; bHeatingReady : (nHeatingTemp 45) AND (nHeatingTemp 60); bAutoRunning : (eMainState 20);叠加状态的好处是它可以同时存在。比如设备正在自动运行同时安全门被打开bAutoRunning和bSafeDoorOpen都为TRUE这没什么矛盾。在点胶逻辑里我要求bAutoRunning为TRUE才允许点胶动作在安全逻辑里只要bSafeDoorOpen为TRUE就禁止转台旋转。两条逻辑各管各的互不干扰。叠加状态的清除条件一定要写完整这是最常见的坑。比如bAlarmActive它的复位条件是“所有原始报警消失且操作员按下复位按钮”这个条件与主状态无关不能在CASE的某个分支里才清除。否则可能出现一种情况设备在报警状态里按复位因为主状态还在40清除叠加状态的逻辑只写在主状态0的分支里那报警永远清不掉设备永远原地踏步。这种问题是自己给自己挖坑。2.4 入口动作、出口动作与持续动作状态机真正的质感来自于区分三类动作入口动作、出口动作、持续动作。入口动作是进入某个状态的那一刻执行一次的动作。比如进入自动运行状态时把工位计数器和产量计数器清零进入报警状态时记录当前时间为故障发生时间。出口动作是离开某个状态之前执行一次的动作。比如离开自动运行状态时立刻禁止所有轴使能防止设备带着“惯性”进入暂停。持续动作是只要停留在这个状态里就每个周期执行的动作比如在运行状态中持续输出允许运行信号。在ST里入口动作和出口动作需要借助上一周期状态变量来捕捉跳变沿IF eMainState eMainStateOld THEN (* 状态发生了变化 *) eStateFrom : eMainStateOld; eStateTo : eMainState; END_IF eMainStateOld : eMainState;捕捉到跳变之后用两个小CASE分别处理出口动作和入口动作CASE eStateFrom OF 20: (* 离开运行状态立即停轴 *) bAxisEnable : FALSE; bDispenseEnable : FALSE; 40: (* 离开报警状态记录报警消失 *) dtAlarmClearTime : NOW(); END_CASE CASE eStateTo OF 20: (* 刚进入运行状态清计数器 *) nCycleCount : 0; dtRunStartTime : NOW(); END_CASE这套写法把动作收敛到了“状态切换点”上而不是像梯形图那样到处散落着置复位。维护的时候想查“进入运行状态时做了什么”直接找eStateTo等于20的分支就行。3. 实际案例拆解三工位点胶转台的状态机这样写3.1 设备工艺描述我拿之前调过的一台三工位点胶转台举例。设备有一个旋转台均布三个工位。工位1是人工上料把待点胶的工件放上去工位2是点胶工位由点胶阀执行点胶工位3是固化/下料工位点完胶的工件在这里经过短时间固化然后人工取走。转台每次旋转120度完成一个工位循环。这台设备有四个需要重点控制的执行器转台旋转机构、点胶阀、加热模块、安全门。加热模块负责把胶水维持在设定温度温度没到不允许点胶安全门打开时转台必须停止旋转防止夹伤操作员。在实际调试时我先不管点胶的工艺参数先把状态机的框架搭起来再往里面填工艺动作。这个顺序很重要状态框架不稳后面点胶参数调得再好也白搭。3.2 单独状态定义表按照3.1的设备和工艺我把单独状态定义成下面这张表写完直接贴到程序注释里维护的人不用猜状态号状态名含义进入条件退出条件0空闲设备待机执行器不带电上电完成全部循环复位完成按下启动按钮10回原点转台回到机械原点上电后首次运行前原点到位信号OK20自动运行按工艺步骤循环执行点胶流程启动按钮原点OK无报警暂停/报警/整机循环完成30暂停自动流程挂起执行器安全保持暂停按钮/安全门打开/缺料暂停条件解除40报警锁停故障锁定需人工确认复位任一报警产生故障排除后再复位这张表里特别要留意“进入条件”和“退出条件”的写法它们是状态转移的依据。比如从空闲进入自动运行必须同时满足启动按钮按下、原点已经回好、当前无报警三个条件缺一个都不准动。实际现场经常出现“按下启动没反应”的情况多半就是这三个条件里有一个不满足此时去监控这三个BOOL一眼就能定位。3.3 叠加状态定义表叠加状态定义如下标志名含义影响置位条件复位条件bSafeDoorOpen安全门打开禁止转台旋转、禁止点胶门打开信号门关闭信号bEStop急停被按下主状态强制跳报警输出总断硬线急停触点断开急停复位bAlarmActive存在报警禁止进入自动状态任一报警源触发所有报警源消失且按复位bHeatingReady加热温度OK允许点胶动作温度在设定范围内温度超范围bMaterialOK工位1有料允许自动流程往下走料位传感器检测到工件料位传感器无工件叠加状态和单独状态最大的不同就是它们“不抢占设备主导权”。bHeatingReady为FALSE不会把设备从自动运行拉到报警状态它只是让点胶阀暂时不动作bSafeDoorOpen为TRUE则是比较强的联锁会把自动运行状态直接拉到暂停但不会改变“设备存在报警”这个事实。这些状态叠加在主状态之上各管一摊逻辑上非常清爽。3.4 关键工位的转移逻辑与联锁自动运行状态内部我用一个工艺步骤变量nStep来驱动转盘动作IF eMainState 20 THEN CASE nStep OF 0: (* 等待启动转台 *) IF bMaterialOK AND bHeatingReady THEN nStep : 10; bRotateCmd : TRUE; END_IF 10: (* 转台旋转等待到位 *) IF bRotateDone THEN bRotateCmd : FALSE; nStep : 20; END_IF 20: (* 工位2点胶动作 *) bDispenseCmd : TRUE; IF bDispenseDone THEN bDispenseCmd : FALSE; nStep : 30; END_IF 30: (* 固化等待、提示取料 *) IF bCureTimeUp THEN nStep : 0; bCycleDone : TRUE; END_IF END_CASE END_IF安全门与急停的联锁优先于工艺步骤我在主状态转移那里已经写过了。这里额外强调一点安全门触发暂停之后操作员把门关上设备不能立刻自动恢复运行必须再按一次“继续”按钮防止关门瞬间设备突然旋转吓到人。暂停恢复逻辑我会这样写30: (* 暂停 *) IF bConfirmResume THEN IF NOT bPauseReq AND NOT bSafeDoorOpen AND NOT bEStop THEN eMainState : 20; END_IF END_IFbConfirmResume是一个按钮信号操作员确认安全后手动释放暂停。这种“恢复也要人工确认”的习惯能让现场少出很多事故。3.5 手动与自动切换时的状态安全处理自动运行状态和手动调试状态之间的切换是状态机设计里最容易出危险的地方。我的原则是不允许直接从自动运行切到手动更不允许从手动直接跳进自动运行。手动和自动之间的切换必须先回到空闲状态所有输出归零再从空闲进入目标模式。这样处理的原因很实际。自动运行时设备正在高速执行动作此时切到手动模式操作员以为设备停了伸手进去调试结果某个轴因为手动逻辑还带电瞬间动作就会造成伤害。先回空闲状态把所有执行器输出断开再允许手动操作安全性高很多。4. 状态机程序最容易踩的坑我全踩过一遍4.1 状态跳变/飞状态先查“多个变量表示状态”飞状态这个词是我在现场跟同事学的意思是设备状态莫名其妙变成了一个不可能的状态所有动作全部乱套。最常见的原因就是用了多个M继电器联合表示状态置位了一个忘了复位另一个。比如M10表示自动运行、M11表示暂停理想要么M10ON、要么M11ON实际可能因为逻辑疏忽两个同时ON设备就会一边执行自动流程一边执行暂停动作。用CASE加单一INT变量之后这类问题从根源上消失。但要注意另一个坑在线调试时如果用强制功能直接把eMainState强制成一个不在状态定义里的数比如99CASE没有对应分支设备会进入“无状态”状态所有输出保持上一次的值看起来像死机。遇到这种情况不要慌把eMainState强制回0或者重新初始化程序输出就能恢复正常。4.2 叠加状态清不掉多半是复位条件写错位置叠加状态清不掉的现象很典型报警明明已经消失了设备还是不让启动HMI上报警灯也不灭。查来查去发现bAlarmActive的复位代码被写在某个CASE分支内部而那个分支所在的单独状态从来没被进入过因此复位代码一个扫描周期都没执行过。调试这类问题的思路很简单叠加状态的置位和复位条件必须描述“客观事实”与主状态无关。bAlarmActive的复位条件应该是“所有报警源都消失并且操作员按下复位按钮”而不是“当eMainState0时把bAlarmActive复位”。把复位逻辑从CASE分支里挪出来放到CASE之前的独立赋值区问题瞬间解决。4.3 输出在状态机里乱给导致设备切换瞬间乱动有段时间我图省事直接在CASE的20分支里写Y0:TRUE控制电磁阀在0分支里写Y0:FALSE。看起来逻辑没错但现实往往很打脸。因为程序还有其他联动比如急停回路、手动调试按钮、安全联锁它们也会操作同一个输出。结果就是CASE这个周期里把Y0置TRUE后面另一个程序段又把Y0复位掉再下个周期CASE又置TRUE输出在几个毫秒内不停抖电磁阀嗡嗡响气缸动作跟抽风一样。解决方法是物理输出统一分配所有程序内部使用中间变量比如bDispenseCmd、bRotateCmd状态机和各联锁逻辑只操作这些中间变量程序最后专门用一个输出段把它们映射到物理输出Y点Y00 : bDispenseCmd AND bHeatingReady AND NOT bSafeDoorOpen; Y01 : bRotateCmd AND bAutoRunning AND NOT bSafeDoorOpen;这样做的好处是把“逻辑”和“执行”分开逻辑上设备的意图是点胶但最终要不要给电磁阀通电由最后这个输出段统一把关。安全联锁只需要在一个地方做总闸不需要在每个状态分支里反复排查。4.4 超时监视状态卡住不等于报警有一种很难发现的故障设备停在某个状态什么都不动但PLC没报警操作员看着像死机其实是某个到位信号一直没来。比如转台旋转指令已经发出了但旋转到位传感器坏了或者机械卡住反馈信号永远不亮程序就一直等在那里。没有超时监视的设备这种问题排查起来只能慢慢对点位非常费劲。我的习惯是对所有“指令-反馈”成对的动作加超时监视。转台旋转指令发出后如果在设定时间内没收到到位信号就置位一个超时报警并让主状态跳到报警锁停。这样设备卡住时HMI会提示“转台旋转超时”维修人员知道该去看机械和传感器而不是对着PLC发呆。超时监视的逻辑可以写一个通用FB也可以在每个工艺步骤里写TON核心是“每一对指令和反馈都要有超时”。这个习惯养成了现场调试省心非常多。4.5 在线监控时强制变量要讲武德在线调试时很多人喜欢用强制功能快速验证逻辑。强制输入信号没问题比如强制一个感应器信号为TRUE看看设备反应这是常规操作。但我不建议直接强制状态变量或者输出点来“绕过”状态机跳转。飞过几次就会明白强制eMainState从0跳到20设备虽然进入了自动运行状态但内部的工艺步骤变量nStep还在0原点状态、位置反馈都没准备好输出的结果完全不可控严重时设备直接乱动作。正确的调试姿势是强制模拟现场的传感器输入信号让状态机按正常路径自己去跳转。这样验证的是逻辑本身而不是欺骗逻辑。另外汇川PLC编程软件的在线监控表里可以同时监视eMainState和所有叠加状态配合二进制显示一眼就能看出哪些叠加状态同时为ON排查效率很高。5. 状态机程序调试与后续维护时我常用的一些野路子5.1 用状态迁移表做调试清单我每接一台新设备写完状态机之后都会做一张状态迁移表横向是当前状态纵向是触发条件对应交叉点填目标状态。调试的时候照着表一条条打钩空闲加启动按钮能不能进自动运行运行中急停能不能进报警报警复位能不能回空闲。这张表不进程序只在自己手里但每次联调时它比程序注释还好用。程序改过一次之后对着表逐条验证能快速发现“改崩了”的地方。5.2 HMI上不要只显示数字要让状态“说人话”汇川的触摸屏基本上都支持“数值映射文本”功能把状态变量关联过去HMI上显示的不再是0、20、30这些数字而是“空闲”“自动运行”“暂停”“报警锁停”这些中文名称。这样操作工和维修工一看就知道设备卡在哪个环节不需要拿着PLC在手提电脑上看监控表。我还会在HMI上做一个“状态转移历史记录”页面把最近几十次状态切换的时间和动作记录下来。设备出现间歇性故障时翻历史记录往往能直接看到“运行→报警→复位→运行”的切换路径比自己盯着监控表碰运气强多了。5.3 新加状态时尽量插空号而不是批量重排用户需求总是会变的设备跑着跑着可能就要加一个“顶料气缸待机”状态。如果当初状态编号用了0、1、2、3的连续编号新增状态就意味着把所有关联条件全部改一遍工程量巨大。如果用了0、10、20、30的间隔编号新增状态直接插在5或15这个空位上原有所有逻辑不需要改动只是CASE里多一个分支而已。这就是我坚持间隔编号的真实原因不是炫技是踩过坑后的妥协。5.4 状态机程序建议独立成块别跟主逻辑揉成一团汇川PLC支持多程序组织单元我建议把状态机独立建块输入输出通过全局变量或接口传递。状态机块里只负责主状态转移和叠加状态刷新不写具体气缸动作、不写伺服点动、不写报警文本。具体执行动作放到各自的子程序里。这样分层之后一个人负责调动作一个人负责调状态不会互相干扰。哪怕是单兵作战程序结构清晰了隔半年再出差改设备重新上手的速度也会快很多。状态机看起来是编程技巧本质上是把设备逻辑梳理清楚的方法论。我个人的体会是先花半小时把状态表和迁移条件想明白写代码只要几分钟反过来不做设计就埋头写梯形图后面排查问题要花好几天。这这笔账怎么算都是前期规划划算。如果你正准备在汇川PLC上写一套新设备程序强烈建议从状态变量规划开始而不是从第一段输出逻辑开始。