PLC结构化文本位操作:WAND/WOR/WXOR高效联锁逻辑设计 ST位操作进阶WAND/WOR/WXOR联锁应用做设备类项目做到一定阶段你会发现程序里最让人头疼的不是PID调参也不是通讯解析反而是那些零散分布的联锁条件。今天想跟你聊聊ST结构化文本里三个经常被低估的指令——WAND、WOR、WXOR以及它们怎么在联锁逻辑里发挥真正价值。这套组合拳我用在实际项目里解决过不少BOOL变量满天飞的混乱局面把几十行重复判断压缩成几条字操作逻辑反而更清晰、更好维护。这篇文章适合两类人一类是刚从梯形图转ST、对联锁逻辑还在用老办法硬写的编程人员另一类是已经在用ST、但位操作指令用得比较浅想看看别人怎么组织联锁状态字的同行。不堆理论直接讲设计思路、代码写法和踩过的坑。1. 位操作与联锁为什么这一套组合值得深挖1.1 从BOOL满天飞到字操作的思路转变先说说我见过最多的联锁写法。设备启动条件通常是这个套路门关好了、急停没拍、润滑油压力正常、变频器无故障、上级允许启动……于是代码里全部写成单个BOOL变量IF bDoorClosed AND bEStopOK AND bLubeOK AND bVFDReady AND bPermit THEN条件一多一行写到屏幕外面去加一个条件还要改一大片。这种写法人人都懂但工程上有个隐藏问题你的联锁条件经常不止判断这一个动作还要做状态记录、故障定位、首出报警。比如急停信号掉了你不但要停设备还要知道是不是第一个触发的原因门开了要提示门未关油压低要提示油压不足。如果全是散装的BOOL变量这些附加功能需要写一堆分支非常容易漏。字操作指令解决的是整组状态一次性处理的问题。把若干个BOOL状态打包到一个16位的WORD里每一位代表一个独立的联锁条件然后用WAND/WOR/WXOR去整字判断、整字修改。程序里不需要出现几十个BOOL变量只需要维护几个状态字逻辑走向一眼就能看明白。1.2 联锁逻辑的本质需求联锁Interlock说到底就是条件不满足则禁止动作或动作发生后必须特定条件才允许解除。工程上对它的要求非常明确第一可靠性。读取的状态必须是同一个时刻的一致性快照不能前面一个条件读到新值、后面一个条件读到旧值。字操作天然吃这个优势把状态打包在一个变量里整个字在一次扫描内刷新判断的时候不会出现半个状态。第二可诊断性。联锁停了机操作员和维修工最关心的是到底哪个条件把它卡住了。如果用字做掩码直接把状态字和掩码做运算再配合位测试可以把触发原因精确定位到某一位比一串AND出来的BOOL结果直观得多。第三可扩展性。现场改工艺、加保护点是常事。加了传感器只改掩码和注释联锁逻辑主体不用动删了某个条件把掩码对应位清零就行。要是散装BOOL的写法每增删一次条件都要重新审视整段逻辑。所以我的结论是位操作本身并不神秘但它和联锁场景天然匹配。不是所有项目都值得上这套方案但凡是条件超过五六个、状态需要记录、故障需要定位的联锁逻辑字操作绝对比散写变量省心得多。2. WAND/WOR/WXOR指令详解从电气回路到PLC代码2.1 指令语义与梯形图对照先快速对齐一下这三个指令的语义。WAND是字与Word ANDWOR是字或Word ORWXOR是字异或Word XOR。你从梯形图转过来就会觉得非常眼熟梯形图里两个常开触点串联是AND、并联是OR交换一个常开一个常闭的并联是XOR。但WAND/WOR/WXOR的区别在于它们操作的对象不是一个BOOL位而是一个16位的WORD整体两个WORD对应位逐位做逻辑运算输出另一个WORD。举个例子。iResult : WAND(iA, iB)iA是16#00FF、iB是16#0F0F结果就是16#000F。每一位都独立运算第0位iA是1、iB是1结果位是1第4位iA是1、iB是0结果位是0。这个合并结果的行为在联锁里的用处非常大因为你可以把结果字的每一位当成一个分项联锁结果来用。WOR的典型用途是状态聚合。多个报警信号可以单独用一个BOOL触发但你希望有一个总报警标志。与其写IF bAlarm1 OR bAlarm2 OR bAlarm3 THEN不如把三个报警分别放在状态字的第0、1、2位然后这个状态字本身就代表着当前有哪些报警处于激活状态wor这个指令可以用来把新的报警位合并进状态字。WXOR最有意思它的典型语义是差异检测。用一个期望状态字和目标状态字做异或结果里为1的那些位就是两者不一致的位置。这在联锁里直接对应信号反馈与指令信号不一致检测——你发了启动指令反馈却对不上异或一算一目了然。2.2 16位数据的位坐标思想用好字操作的前提是脑子里建立一张位坐标表。一个WORD有16个位位编号从0到15分别对应二进制的每一位。你在代码里定义这些位的含义就等于在一张地图上标坐标。我一般会直接在程序注释里画位分布表(* 联锁状态字 iLockStatus 位定义 bit 0 : 急停回路OK1正常 bit 1 : 设备门关闭1关到位 bit 2 : 润滑油压力OK1正常 bit 3 : 变频器无故障1正常 bit 4 : 手动/自动模式选择1自动 bit 5-7: 预留 *)有了这张表后面所有掩码计算都有了依据。16#0007表示前三位的状态都OK16#0018表示第3和第4位被同时检查。这种按位坐标思考的习惯比记住指令语法重要得多。另一个容易被忽略的点是位编号和PLC的字节序、显示习惯的关系。某些HMI或者监控软件看WORD变量时显示的是十六进制调试时把16#0005读成第0位和第2位为1并不困难但如果是字节数组方式传输你要清楚高位字节和低位字节的顺序不然后面做通讯联锁很容易把位对应错。提示建议把状态字的位定义固定成惯例。比如bit 0到bit 7给安全类条件bit 8到bit 15给运行状态类条件这样所有设备程序之间可以复用同一套联锁工具函数新项目接手的人也不用从头猜位含义。3. 联锁应用场景与代码实现3.1 单点联锁状态字掩码检查最基础、也最常用的联锁场景是一组条件全部满足才允许动作。用状态字加掩码来做代码精简到一眼到底。假设设备启动允许条件有四个分别存在状态字iLockStatus的 bit0~bit3全部为1才允许启动。启动允许条件表达式就是IF WAND(iLockStatus, 16#000F) 16#000F THEN bStartPermit : TRUE; ELSE bStartPermit : FALSE; END_IF这里有门道。WAND(iLockStatus, 16#000F)的作用是把关注位保留出来把不关注位全部清零。掩码为1的位透传原状态掩码为0的位强制变0。然后再和16#000F比较检查这四个关注位是否全部为1。这叫掩码全等比较。另一种做法是只关心关注位里有没有某一个不满足用WAND(iLockStatus, 16#000F) 16#000F来判断联锁不满足。两种写法表达式不同但逻辑等价选一种贯穿整个项目就好。如果要精确定位哪个条件没满足把状态字和期望值做WXORiDiff : WXOR(WAND(iLockStatus, 16#000F), 16#000F); IF iDiff 0 THEN // iDiff里为1的位就是未满足的联锁条件 END_IFiDiff的bit0~bit3里面哪一位是1就代表哪一位的实际状态和期望值不一致。比如iDiff 16#0002就是bit1对应的设备门关闭没有满足。这个结果直接可以映射到HMI上的故障信息不需要再逐个IF去判断。3.2 互斥联锁条件组互锁设备控制里经常有正转和反转不允许同时给信号开阀和关阀不允许同时输出这类互斥要求。传统写法是IF bFwdCmd THEN bRevCmd : FALSE; END_IF IF bRevCmd THEN bFwdCmd : FALSE; END_IF这种写法能从逻辑上避免同时为1但代码量随互斥条件增多而膨胀。更重要的是如果输出是线圈保持型这种后写覆盖先写的方式在扫描周期上有隐患一个周期内先执行了正转又执行了反转覆盖外部输出的实际状态取决于网络执行顺序。用字操作可以设计一个输出控制字iOutputWordbit0正转输出、bit1反转输出。互斥联锁写成// 只允许bit0和bit1同时至多一位为1 IF WAND(iOutputWord, 16#0003) 16#0002 THEN // 互斥条件冲突优先切断动作侧 iOutputWord : WAND(iOutputWord, 16#FFFC); // 清零bit0和bit1 END_IFWAND(iOutputWord, 16#0003) 16#0002这个判断挺巧妙16#0003掩码取出bit0和bit1的值如果结果是2或3说明bit1已经为1不管bit0是否为1此时再强制把这两个输出位清掉。不过说实话在真正的安全要求高的场合仅仅靠程序互斥是不够的还需要外部继电器回路硬互锁。程序里的互斥更多是为了防止误操作、减少触点竞争。用字操作来写互斥的优势是多组互斥条件可以并行在一个字里管理比如16个输出位分成8组互斥每组两个位一个WAND判断就能覆盖全部组十几行代码搞定原本得写几十行IF的活。3.3 故障字聚合与联锁触发联锁不止作用于启动前更常用的是运行中来了故障要马上停。这个场景里状态的实时聚合和锁存是两个关键动作。假设设备有8个故障源分别对应iFaultWord的bit0~bit7。任何一位为1都要触发跳停。按BOOL写法就是IF bFault1 OR bFault2 OR ... THEN iStop : TRUE; END_IF8个故障写8个OR16个故障写16个OR非常难看。字操作可以直接判断故障字是否为0IF iFaultWord 0 THEN bTripCmd : TRUE; END_IF这还不够工程上通常要求记录第一个故障首出原因。你可以在故障字刚变为非零的瞬间用上升沿把当时的故障字捕获保存IF bReadyForTrip AND (iFaultWord 0) THEN iFirstFault : iFaultWord; // 锁存首个故障字 bReadyForTrip : FALSE; // 只记录一次 END_IFiFirstFault里为1的位就是触发跳停的第一批故障源不管后续又来了多少新故障第一个原因都保留着直到人工复位。这个故障字快照设计在设备诊断里价值非常高维修人员到现场一看HMI上的iFirstFault就能直奔故障点。还有一个实用技巧是故障字的分组聚合。把8个故障按类别分组一类是电气故障、一类是机械故障、一类是工艺异常每个组维护一个分类掩码。要判断电气类有没有故障只需要WAND(iFaultWord, 16#00F0) 0不用再写IF bFault4 OR bFault5 OR ...了。3.4 组合应用急停链路与状态校验把上面的思路组合起来可以做更复杂的联锁方案。这里分享一个我在某台设备上实际用过的框架。这个设备有个核心需求急停链路必须在一拍内完成所有安全条件检查 - 输出切断 - 记录原因三个动作。我用三个字变量互相配合iSafetyStatus实时安全状态字bit0急停未拍bit1门锁闭合bit2光幕OKbit3抱闸监视正常iSafetyMask当前需要检查的安全条件掩码工艺允许部分条件旁通时修改此掩码iTripWord跳停记录字每一位对应一种跳停原因核心循环每扫描周期执行// 1. 计算当前安全状态是否满足掩码需求 iSafetyFault : WXOR(WAND(iSafetyStatus, iSafetyMask), iSafetyMask); IF iSafetyFault 0 THEN // 2. 跳停并记录是哪一组安全条件失配 iTripWord : WAND(iTripWord, 16#FF00); // 保留历史分组标记 iTripWord : WOR(iTripWord, iSafetyFault); bRunEnable : FALSE; ELSE bRunEnable : TRUE; END_IF第一步WAND(iSafetyStatus, iSafetyMask)把不关心的位清零只保留当前必须满足的安全条件随后WXOR检查它和掩码是否完全一致一旦有任一位失配iSafetyFault对应位变成1。这个失配字直接当跳停原因记录。旁通功能做得很干净某天光幕长时间被货物遮挡但工艺允许继续运行操作员确认旁通后iSafetyMask里bit2清零程序不再检查光幕。以后要恢复把掩码位恢复即可主逻辑一个字都不用改。这套组合写下来整个急停链路的代码不超过40行但覆盖了状态采集、掩码检查、差异定位、原因锁存、旁通管理全部功能。如果是散装BOOL写法这个量级的逻辑至少要翻一倍代码量而且排查起来远没有这么直观。4. 实操过程与关键环节实现4.1 联锁状态字的设计组织状态字设计是整个方案的地基。我在新项目里通常按以下步骤来组织第一步梳理联锁需求清单。逐个列出所有需要参与联锁的条件标注类型安全条件、设备条件、工艺条件、外部许可条件。这一步必须在写代码之前完成宁可列多了也不要边写边加。第二步分配位号。把条件按类型分配到状态字的固定位置。原则是把同一类、同一故障域的条件放相邻位这样掩码比较集中也方便后面分组解析。比如bit0~bit3放安全条件bit4~bit7放设备状态bit8~bit11放工艺参数bit12~bit15做预留扩展。第三步用常量定义掩码而不是在表达式中写魔法数字。看代码遇到16#010F除非注释写得清楚否则根本不知道是针对什么条件。建议直接定义常量CONSTANT MASK_SAFETY : WORD : 16#000F; // 安全条件组掩码 MASK_DEVICE : WORD : 16#00F0; // 设备状态组掩码 MASK_PROCESS : WORD : 16#0F00; // 工艺条件组掩码 MASK_ALL : WORD : 16#0FFF; // 全部联锁条件掩码 END_CONSTANT第四步在程序注释里固定画位分布表。位定义一旦发布任何修改都要同步更新注释。我在实际项目里吃过亏原来定义bit3是液压压力OK后来设备改造把位含义换成了温度OK结果注释没改调试的时候对着代码看了半小时才反应过来。4.2 掩码的计算方法与技巧掩码计算不复杂但容易出错特别是位比较多的时候。这里分享几个实用技巧。技巧一用十六进制逐位展开来算。16位WORD的十六进制每位代表4个二进制位。16#000F 二进制0000 0000 0000 1111即bit0~bit3为1。16#00F0 0000 0000 1111 0000即bit4~bit7为1。16#0F0F 0000 1111 0000 1111即bit0~bit3和bit8~bit11为1。算掩码时先确定需要的位范围再用每四位一组、查十六进制表的方式组合。技巧二反掩码取反可以直接用WXOR实现。想清零某一位正常做法是WAND(iWord, 16#FFFB)16#FFFB的bit2为0。如果不习惯口算反掩码可以用WXOR(iWord, 16#0004)把bit2取反——注意这只适用在你明确知道该位当前状态的场景单纯清零还是用AND靠谱。技巧三掩码尽量用命名常量而不是裸数字。这不仅是可读性问题还牵扯到后期维护。我用过裸掩码的项目三个月后自己都会犹豫16#000D到底是哪三个位。常量的好处是改一个地方、全部生效不会出现掩码改了一半导致联锁判断矛盾的情况。4.3 与保持、复位逻辑的配合联锁逻辑通常是故障锁存、人工复位。字操作实现锁存复位要特别注意复位语义的位级粒度。复位操作最常见的错误写法是把整个状态字清零。比如iTripWord : 0这会把所有故障原因一次性清掉但此时如果故障源仍然存在下一拍故障字又会重新变成非零首出信息就会丢失。更合理的做法是分级复位// 复位等待信号有效 IF bResetCmd AND NOT bRunEnable THEN // 仅当当前无实际故障时才允许整体复位 IF iFaultWord 0 THEN iFirstFault : 0; bTripCmd : FALSE; END_IF END_IF这个先确认实时故障字已经为0再清除锁存记录的顺序很关键。否则现场维修人员按了复位设备立刻再次跳停首出原因还是同一个看起来就像复位没用。还有一类保有型状态比如运行中发生过门打开报警要求报警保持到人工确认。可以用状态字的某一位作为保持标志用WXOR来检测新发生的位变化iNewFault : WAND(iFaultWord, WXOR(iFaultWord, iOldFaultWord));这个表达式的语义挺有意思iFaultWord当前值、iOldFaultWord是上一拍的值WXOR找出变化的位再用WAND和当前故障字确认哪些变化是变为1的。结果iNewFault里为1的位就是这一拍新出现的故障。这个写法不是必须的但它展示了一个思路用字操作模拟上升沿的位级版本一次抓取多个新激活位省掉一长串FALSE_TO_TRUE写法。5. 常见问题与排查技巧实录5.1 位号偏移与掩码写错这是我见过最高频的问题。定义状态字时说bit5是冷却水流量正常写掩码时却拿16#0010bit4去判断联锁结果当然不对。这类问题隐蔽性很强逻辑运算本身没错参数错位而已。排查方法先打印状态字原始值和期望值再打印掩码运算后的结果对照位定义表逐个核对。别直接在脑子里推演除非你的二进制心算足够强。把WAND(iLockStatus, 16#0010)的实际结果读到监控里一看是0还是非0就清楚了。我自己的习惯是写完掩码之后用十六进制反推二进制逐位写一遍注释做完这一步再往下走。看着繁琐但能省下后面漫长的联调排故时间。5.2 数据类型长度不一致WAND/WOR/WXOR对操作数类型有要求两个操作数必须是同一类型而且通常要求16位整数。常见错误是把BYTE和WORD混用或者把INT带符号数和WORD无符号数混用。混用带来的问题不只是编译报错还有符号位扩展的隐患。INT类型的16位数据最高位bit15是符号位如果联锁状态字用INT声明bit15被当成负数标志WAND和WXOR虽然按位运算不看符号但之后做比较判断时符号位会产生干扰。比如IF iResult 0这种写法配合位操作结果极容易翻车。我的建议是状态字、掩码字、故障字统一声明为WORD不要用INT。这样所有位都按无符号处理掩码和比较逻辑完全干净。字节交换、类型转换之类的事留给通讯层去处理联锁内部就不碰带符号数。5.3 扫描周期与联锁抖动字操作在联锁里很方便但它依然遵循同一扫描周期内变量值不变的规则。如果外部IO映射和联锁判断放在同一个扫描周期的不同任务里或者状态字在多个任务中被修改就有可能出现判断结果滞后一拍或者抖动。排查这类问题先看任务的调度周期。设备联锁建议放在固定周期任务里执行不要挂在自由运行任务中否则时序没有保障。状态字的更新源IO映射、通讯接收最好在联锁逻辑的上一任务里提前刷新保证联锁判断拿到的是一致快照。另外要注意HMI或者上位机对状态字写入权限的管理。如果操作画面能直接写联锁状态字很可能一个误操作把掩码改掉联锁逻辑立刻失效。强烈建议状态字在PLC侧定义为只读数据上位机只能读取、不能写入。联锁旁通等操作通过独立的旁通指令字实现并在画面里加操作记录。5.4 联锁逻辑的仿真测试方法调试位操作逻辑最怕的是看着表达式以为对实际跑起来不对。我通常会用仿真器搭一小段独立测试逻辑把状态字用手动设置的方式反复切换第一轮测掩码提取。手动把状态字设为16#FFFF逐个验证不同掩码的WAND结果是否符合预期位。重点确认只关注位保留、非关注位清零。第二轮测差异检测。手动把状态字设为和期望值相差一位确认WXOR结果能精确找出该位。这一步把首出判断的准确性先验证掉。第三轮测锁存复位。在仿真里给故障字强制赋值观察bTripCmd和iFirstFault是否正确锁存然后清故障字观察是不是一定要等到故障字为0后才能复位。我踩过的一个印象最深的坑在这里仿真时用TRUE强制给BOOL位赋值状态字本身却来自IO映射区强制值在下一个周期就被IO刷新覆盖了。排查了半天发现状态字根本不是我强制出来的值。用仿真器测联锁逻辑时强制点要放在状态字源头或者直接用变量表监视修改而不是强制定位到IO输入。6. 最后的实操体会联锁逻辑是设备的底线代码怎么组织直接决定了调试和维保阶段的工作量。我个人的体会是WAND/WOR/WXOR这三个指令本身很简单但它们推动你先去思考状态如何组织、位怎么分配、故障怎么记录而不是急着写IF分支——这种思维方式的转变比记住指令语法值钱得多。建议你从一个小模块开始试。比如先拿一台单机设备把它启动许可条件和故障报警整理成两个状态字用掩码加比较的方式重写现有联锁逻辑然后对比测试老写法和新写法的行为差异。实测过一轮之后你会对这些指令的边界和性格有更实在的把握。再分享一个小技巧给状态字起名时用前缀区分用途比如iLockStatus是联锁状态、iFaultWord是故障字、iSafetyMask是安全掩码命名统一之后写代码时几乎不用想它在整个系统里扮演什么角色。这个习惯帮我在四五个项目里保持联锁代码的高度一致新项目接手的人也能快速进入状态。如果你也在这条路上踩到了什么隐蔽的坑欢迎拿出来聊。位操作这门功夫多交流几次边界就摸清楚了。