ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ST编程进阶:用WAND/WOR/WXOR位操作实现高效联锁与首出记录

ST编程进阶:用WAND/WOR/WXOR位操作实现高效联锁与首出记录 在STStructured Text程序里做联锁很多人习惯一个BOOL一个BOOL地排而 WAND、WOR、WXOR 这三个位操作函数能把十几路联锁条件压缩进一个字里用一两行完成投切、锁存、变化检测。这篇内容写给已经会ST基础语法、但还没吃透位操作的工程师重点是讲清楚位操作和联锁逻辑怎么结合以及现场调试时那些文档里不会写的坑。1. 传统联锁写法的问题不只是“代码长”那么简单1.1 一段典型的BOOL联锁改起来到底有多费劲先说一个很常见的场景。设备有急停、安全门、光栅、油压、温度等七八个联锁条件最初的程序通常长这样bTrip : FALSE; IF NOT bEStop OR NOT bDoorClosed OR bLightCurtain OR bOilLow THEN bTrip : TRUE; ELSE IF bReset THEN bTrip : FALSE; END_IF; END_IF;这段代码的问题不在于逻辑本身而在于当联锁条件越来越多时你很难一眼看清哪些条件参与了联锁哪个条件被旁路复位按钮到底清的是什么新加一路联锁是不是每个IF都要碰一遍实际现场改这种程序最痛苦的是改完以后你不知道动了哪里。比如工艺员说“光栅先临时旁路一天”你找了个IF把那行注释掉结果发现还有另一个分支里也判断了光栅漏改一个设备动不动就误停。这是典型的“单个BOOL信息量不足”造成的维护灾难。1.2 把16路信号压进一个WORD联锁的思考方式就变了这是我后来在多个项目里验证过的方向联锁输入的每一个信号本质上就是一个二值状态。既然ST里原生就有WORD类型的位串运算为什么不用一个WORD来表示一整组联锁状态举个例子用一个全局变量fault_word作为“故障字”bit0 急停触发bit1 安全门打开bit2 光栅遮挡bit3 油压低bit4 主轴过载bit5 温度超高当fault_word : 16#0032时从十六进制反推二进制是0000 0000 0011 0010也就是 bit1、bit4、bit5 同时为1代表安全门开、主轴过载、温度超高同时存在。一个变量的内容就能看出设备当前的整体联锁状态。更关键的是后面聊的 WAND/WOR/WXOR就是对这种“状态字”做整体运算一次处理16路。这才是位操作在联锁里的真正价值。2. WAND/WOR/WXOR的位级语义先把底层的“乘法表”吃透2.1 三个位操作的逻辑本质先复习一下位运算的底层规则。WAND就是按位“与”WOR是按位“或”WXOR是按位“异或”。以一位为例真值表如下位A位BWANDWORWXOR00000010111001111110如果用继电器回路类比WAND相当于两个常开触点串联要求“全部闭合才导通”WOR相当于并联任意一个闭合就导通WXOR则更像数字电路里的异或门最直观的理解是“忽略相同的只看不同的”。在ST里它们的本质就是两个WORD或DWORD的每一位独立做布尔运算每一位都互不干扰。你可以把这两个字想象成两列并排的开关状态运算结果就是新的一列开关状态。2.2 最常用的四种“标准姿势”位操作在联锁中使用得像吃饭一样频繁的其实是下面四个固定搭配目的写法作用保留某些位a AND mask只让mask中为1的位通过强制置位a OR setword让setword中为1的位变成1强制清位a AND (NOT clrword)让clrword中为1的位变成0翻转位a XOR flpword让flpword中为1的位取反为0的位不动很多PLC的ST环境里AND、OR、XOR可以直接作用于WORD类型也有部分平台仍然保留WAND()、WOR()、WXOR()这样的库函数写法。语义完全一样原理相通。2.3 WXOR最容易被低估的地方它是天然的“变化探测器”很多人觉得WXOR除了偶尔翻转位之外没什么用这是误区。异或作用于两个相同的值会得0作用于不同的值会得1。利用这个特点让同一个状态字分别保存“当前值”和“上一周期值”再做一次异或得到的字里每个为1的位就代表这一位在两个周期之间发生了变化。changed_word : state_word XOR last_state_word;这行代码之后changed_word里为1的位是“刚发生变化的位”为0的位是“没变化的位”。不需要写16个 EDGE 检测指令一行就完成了16路信号的边沿信息提取。后面聊首出记录就是建立在这条思路上的。3. 第一组实战用WAND/WOR把联锁投切、锁存复位变成两个表达式3.1 联锁投切一张旁路掩码表代替“到处改IF”联锁逻辑里最常遇到的需求是“某一路联锁暂时不参与动作”。比如油压传感器检修时工艺上允许把油压低这一路旁路掉但其他路必须继续保护。传统写法是到每个判断里改条件用位操作之后可以这样设计trip_word : fault_word AND (NOT bypass_word);fault_word当前实时故障字某位为1代表该路原始故障存在。bypass_word旁路掩码字某位为1代表该路被旁路不参与联锁。trip_word最终生效的联锁触发字。为什么NOT bypass_word因为只有当某一位在旁路字里是0取反后才变成1才能与fault_word中的故障位做“与”。一旦某一位被置1旁路取反后变成0无论故障字里该位是0还是1结果都是0这路联锁自然就被屏蔽了。实际操作中旁路掩码的定义表看起来像这样Bit信号联锁含义默认旁路0急停动作即停机不可旁路1安全门打开即停机不可旁路2光栅遮挡即停机调试时可旁路3油压低压力低即停机可旁路4温度高超温即停机可旁路5振动大超限即停机可旁路每次要旁路灯路联锁不是去翻程序注释而是直接更新bypass_word对应位。触摸屏上做一个“旁路开关”画面理由、时间、操作人都在HMI里登记比改代码安全得多。3.2 故障锁存与批量复位WOR不能省掉的道理联锁触发往往是一瞬间的事。比如油压突然闪一下又恢复如果只判断当前状态设备可能已经停机了但HMI上什么记录都没有。这时候必须做锁存——故障位一旦为1就保持住直到人工复位。锁存的写法非常简单latch_word : latch_word OR trip_word;trip_word里只要有任何一个故障位为1这个位就会通过WOR“刻”进latch_word。哪怕故障源下一秒恢复latch里对应位依然保持为1。这就是联锁自保持的批量版本。复位则反过来latch_word : latch_word AND (NOT reset_word);reset_word对应位为1表示操作员确认要复位这一路。NOT之后该位变成0跟latch_word做与对应锁存位被清零而其他未复位的位不受影响。这里有一个现场经验复位和锁存的顺序一定要讲究。如果先复位再锁存而当前trip_word里故障位还是1那么这个周期内锁存马上又会被OR进去看起来“复位无效”。正确做法是先锁存再复位这样即使故障源还存在复位命令也只能把这个周期之前锁存的故障清掉当前仍然存在的故障位下一周期还会再次锁存设备依旧停在保护状态。只有当故障源真正消失复位命令才会真正把锁存解除。这个顺序是很多初写联锁的人最容易踩的坑之一。3.3 启动允许条件一串AND也能压缩成一个字比较除了停机联锁设备启动前的“允许条件”也可以用同样的思路。假设启动前需要同时满足液压就绪、润滑就绪、门关好、参数下载完成。传统写法是IF bHydReady AND bLubeReady AND bDoorClosed AND bParamLoaded THEN bPermitStart : TRUE; END_IF;用位操作后可以这样IF (ready_word AND need_word) need_word THEN bPermitStart : TRUE; END_IF;need_word里需要的位为1其他位不管。这个写法的好处是新增一个启动条件只需要分配新的bit位根本不用改IF判断的骨架。而且将来万一启动条件不满足还能配合XOR快速看出缺了哪一位。4. 第二组实战用WXOR批量抓“哪一路刚触发”和首出记录4.1 一次处理16路跳变的边沿检测公式联锁里面还有一个高频需求不只是要知道哪个故障存在还要知道哪个故障是“刚刚发生”的。尤其是在停机之后工程师首先想知道的就是“第一引起停机的到底是谁”。传统的边沿检测只针对单个BOOL变量R_TRIG(CLK : bFault, Q bFaultRise);如果要做16路就要写16个R_TRIG实例很啰嗦。用WXOR可以一次搞定changed_word : state_word XOR last_state_word; rising_word : state_word AND changed_word; falling_word : (NOT state_word) AND changed_word; last_state_word : state_word;拆开看changed_word当前周期和上一周期不同的位。rising_word当前为1、且发生变化说明是0→1的上升沿。falling_word当前为0、且发生变化说明是1→0的下降沿。last_state_word及时更新下一周期继续用。用这个概念做联锁首出好处非常明显一次扫描周期里哪一路联锁条件先变成故障rising_word的对应位就会置1其他没变化的位保持0。4.2 首出记录为什么“先触发的那一路”能被准确锁定首出first fault的价值在于定位根因。设备是多路联锁同时动作的传统程序里你只能看到一堆故障同时存在根本分不清谁是因、谁是果。用WXOR的上升沿做首出就能把第一次变成1的那一位锁存下来。核心逻辑是IF rising_word 0 THEN FOR i : 0 TO 15 BY 1 DO IF (rising_word AND (16#0001 SHL i)) 0 THEN first_bit : i; first_word : 16#0001 SHL i; EXIT; END_IF; END_FOR; END_IF;解释一下思路rising_word是当前周期刚发生的故障位集合。从bit0开始逐位检查找到第一个1就是“首出位”。first_bit保存位号比如3就代表是“温度超高”先触发。first_word保存对应掩码方便后续画面上直接按位显示。这里有一个取舍要说明如果同一周期内真的有多路同时变成1这个FOR循环会记录最小编号的那一路。在大多数工艺场景里把最小位号当作首出是可接受的。如果要求精确到毫秒级别的“绝对第一路”那得借助带时标功能的输入模块或者系统级记录纯PLC扫描逻辑很难做到这一点心里有数就好。这个首出逻辑之后程序里仍然会把所有故障位锁存所以设备停机后的HMI画面可以同时显示两部分左侧是“全部已锁存故障”右上角单独高亮显示“首出位”故障定性会快很多。4.3 WXOR边沿检测的局限先把抖动问题解决再说别的WXOR虽然好用但不是万能。最典型的坑是信号抖动。如果某一路传感器在临界状态来回振荡state_word会在0和1之间反复跳那么rising_word每个周期都可能出现新的上升沿首出记录就会被刷新成最新一次抖动失去意义。所以联锁输入在做WXOR之前必须经过滤波、去抖、硬件滤波或软件确认。软件上可以先做一段去抖连续N个扫描周期都稳定为1才认为这个位真正变成1。去抖完成后才打包进state_word。顺序不能反。另外还要注意如果状态字里的位来自不同通信模块刷新周期不一致WXOR会在某个模块刷新瞬间产生“假变化”。这种情况下建议对通信来的字加一个“有效时间戳”判断或者统一采样周期避免把通信噪声当成联锁触发。5. 组装一个联锁函数块代码结构、调用与监控参考5.1 函数块接口设计的思路前面拆了原理现在把它们组合成一个可以反复调用的函数块。我习惯把接口这样设计输入总投切开关、实时故障字、旁路掩码字、复位掩码字、总复位。输出联锁动作结果、有效触发字、锁存故障字、首出位号、首出掩码。bEnable很关键。现场调试时如果总投切开关断开安全策略应当是把全部联锁强制生效而不是把全部联锁都解除。这跟旁路掩码正好相反是“安全兜底”思想。5.2 完整ST代码与注释下面是一份可供参考的FB实现语法以IEC 61131-3为主不同PLC平台的细节差异我在注释里标注。FUNCTION_BLOCK FB_Interlock VAR_INPUT bEnable : BOOL; // 总联锁投切开关FALSE时全部生效 raw_fault : WORD; // 实时故障字某位为1表示该路联锁条件成立 bypass_word : WORD; // 旁路掩码某位为1表示该路被旁路 reset_word : WORD; // 复位掩码某位为1表示复位对应锁存位 bResetAll : BOOL; // 总复位脉冲按下时清空全部锁存 END_VAR VAR_OUTPUT bTrip : BOOL; // 联锁动作输出TRUE表示需要停机/保持停机 trip_word : WORD; // 当前参与联锁的有效故障位 latch_word : WORD; // 已经锁存的故障位 first_bit : INT; // 首出位号-1表示当前无首出记录 first_word : WORD; // 首出掩码只有首个触发位的对应位为1 END_VAR VAR last_trip : WORD; // 上一周期有效故障字用于WXOR变化检测 changed_word : WORD; // 本周期发生变化的位置 rising_word : WORD; // 本周期0-1变化的位 i : INT; // FOR循环索引 END_VAR实现部分// 1. 计算有效故障字 IF bEnable THEN trip_word : raw_fault AND (NOT bypass_word); ELSE trip_word : raw_fault; // 总投切断开时全部联锁强制生效 END_IF; // 2. 先锁存再复位顺序不能反 latch_word : latch_word OR trip_word; IF bResetAll THEN latch_word : 16#0000; ELSE latch_word : latch_word AND (NOT reset_word); END_IF; // 3. 联锁动作锁存存在或当前有效故障仍然存在 bTrip : (latch_word 16#0000) OR (trip_word 16#0000); // 4. 首出清空只有锁存全部解除首出记录才清掉 IF latch_word 16#0000 THEN first_bit : -1; first_word : 16#0000; END_IF; // 5. 首出检测基于有效故障字的0-1变化 changed_word : trip_word XOR last_trip; rising_word : trip_word AND changed_word; last_trip : trip_word; IF rising_word 16#0000 THEN FOR i : 0 TO 15 BY 1 DO IF (rising_word AND (16#0001 SHL i)) 16#0000 THEN first_bit : i; first_word : 16#0001 SHL i; EXIT; END_IF; END_FOR; END_IF;几个细节再点一下bTrip里加OR (trip_word 0)是防止复位瞬间输出抖动。如果只判断latch_word复位命令发出那一周期锁存被清空而故障源还在bTrip可能出现一个扫描周期的假恢复。加上当前有效故障字能保证故障没排除之前联锁输出始终为TRUE。首出清空的判断放在首出检测之前这样如果复位和新故障发生在同一周期首出逻辑还能正确记录新故障。多处用16#0000而不是0是为了在严格类型检查的PLC上减少WORD与整型比较的隐式转换报错。如果你的平台允许写0也行。5.3 调用示例与在线监控建议主程序调用这个FB非常直接fb_ilock( bEnable : bInterlockEnable, raw_fault : wFaultWord, bypass_word : wBypassWord, reset_word : wResetWord, bResetAll : bResetAllPulse, bTrip bMachineStop, trip_word wActiveTrip, latch_word wLatchedFault, first_bit iFirstBitNo, first_word wFirstFaultWord );在线监控时我最常用的做法是把wLatchedFault、wActiveTrip、wFirstFaultWord三个变量都设置成十六进制显示。十六进制一眼就能对照出状态字的位分布。比如wLatchedFault显示为16#0018立刻知道是bit3和bit4锁存了。而iFirstBitNo显示4就说明bit4那一联锁是第一个触发的查故障效率比从前翻BOOL列表快得多。6. 现场最容易翻车的细节位号、时序、安全边界和调试习惯6.1 程序、组态、报文三层里的“位编号”必须统一一个WORD在ST程序里是bit0~bit15。但到了触摸屏组态有的组态软件从“位0”显示有的从“位1”显示到了通信报文里还有可能涉及高低字节顺序问题。所以我的习惯是位定义以程序注释和代码里的掩码常量为唯一标准HMI、通讯协议表、维护手册都共用同一张位定义表。文档里明确写“bit0对应16#0001”不给模糊空间。位号掩码语义bit016#0001急停触发bit116#0002安全门开bit216#0004光栅遮挡bit316#0008油压低bit416#0010主轴过载bit516#0020温度超限这张表一旦发布所有开发、调试、维护的人都按它来对照能省掉大量沟通成本。6.2 WXOR的“变化”扛不了抖动联锁前必须滤波特别提醒一句安全联锁输入通常来自机械触点、继电器输出、安全光栅等多少都带抖动。直接用WXOR去判断首出可能一个抖动就被记录下来。正确做法是在输入阶段加去抖逻辑连续若干周期达到同一状态才认定有效。联锁保护宁可晚几十毫秒也不能乱报首出。6.3 在线“强置清零”故障字是调试大忌调试阶段有人图方便直接在线修改latch_word : 0设备确实能动了但故障源没消除下个周期又重新锁存而且首出记录被清掉后面想查问题就难了。正确做法永远是找到原始故障源修好或旁路。让raw_fault对应位变回0。按下真正的复位命令让锁存按正常逻辑清除。这三点缺一个复位都不算干净。6.4 安全级联锁不要放进同一个旁路字前面代码里bypass_word能屏蔽任意一路这是个很强的权限。如果把急停、安全门这些安全级信号也放进同一个旁路机制一旦操作员误把全字写成16#FFFF整台设备所有联锁都失效这是绝对不允许的。常见的边界做法有两种第一种安全级信号走独立的硬接线或安全PLC回路不进入普通逻辑程序。第二种如果程序必须处理也要把安全位从旁路逻辑里拆出来单独处理// bit0、bit1 是急停和安全门不受旁路字影响 safe_fault : raw_fault AND 16#0003; proc_fault : raw_fault AND (NOT 16#0003); trip_word : (proc_fault AND (NOT bypass_word)) OR safe_fault;这样即使bypass_word被误写成全1安全位仍然能触发联锁。6.5last_state的更新位置错了边沿检测会出现假信号WXOR做边沿检测的前提是“当前状态”和“上一周期状态”来自同一个采样节拍。如果state_word在A程序组织单元里更新last_state_word在B程序组织单元里保存而这两个块正好不在同一个扫描周期执行检测出来的rising_word就会偶尔多出或者漏掉一些边沿。我的建议是把“采集状态字→计算变化→更新last_state”全部放在同一个程序组织单元、同一个调用链、同一个扫描周期的同一段代码里不要在中断任务里穿插修改。排序稳定边沿结果才稳定。用位操作做联锁本质上是把一大片BOOL逻辑变成“状态字掩码”的运算。从联锁投切、故障锁存到批量边沿、首出记录WAND/WOR/WXOR每一样都有不可替代的位置。我自己在仿真环境里反复测过很多次最有价值的地方不是代码少写几行而是出故障时你能用十六进制加一个首出位号几分钟内就锁定问题根源。真正动手改自己的联锁程序之前强烈建议先在仿真器里把bypass_word、reset_word、latch_word这三个字各强制几组数据亲眼看看位与位之间的干扰在哪里哪些顺序不能调换体会会比只看文章深得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进