
做ST逻辑的老哥们多半都经历过这种场景程序里一个封装好的状态字明明只想让某一个联锁标志位翻个状态结果一条MOVE下去整个字被整块覆盖相邻的运行位、报警标志全部被打回原形。这种坑我踩过不止一次后来把逻辑想明白才发现ST语言里真正适合做联锁控制的不是常规的BOOL与或非而是WAND、WOR、WXOR这一族位操作指令。这篇文章就围绕这三个指令来拆。它们解决的核心问题很简单在一个16位或32位的字里精确修改某一位或某几位而不动其他位。应用的典型场景就是设备联锁——正反转互锁、泵阀组合安全联锁、自动/手动模式切换、报警复位。适合正在写设备控制逻辑的工程师尤其是做顺序控制、安全回路、模式切换的人。不管你是刚接触ST还是已经用它写过几个项目下面这些掩码写法和避坑经验都能直接用上。1. 位操作联锁的底层逻辑1.1 为什么普通AND/OR做不了安全联锁很多人一开始会犯迷糊ST里不是已经有AND、OR、XOR了吗为什么还要单独学WAND、WOR、WXOR区别在操作对象。普通AND、OR、XOR在IEC 61131-3里单次调用处理的是两个BOOL量或者作为表达式运算符处理两个BOOL结果还是一个BOOL。比如a : b AND c; (* 只有b和c都为TRUEa才为TRUE *)这种写法适合做单点条件判断急停按下、前门关好、油压到达这几个条件串起来输出一个运行许可BOOL。但实际设备控制根本不是单点游戏。一个控制字往往塞了一堆指令位bit0是正转命令、bit1是反转命令、bit2是点动命令、bit3是故障复位、bit4是急停复位。这些位彼此独立又存在互斥关系。你不可能用一条BOOL运算同时处理它们除非把一个字拆成十几个BOOL变量然后每个BOOL单独维护——程序会膨胀到没法看而且跨FB传参时接口多到怀疑人生。所以工程上的做法是压缩把互相关联的开关量打包成一个INT或DINT用字传递用位来区分。这个字就是联锁逻辑的主战场。问题随之而来——你怎么在不动其他位的前提下单独把bit1清零、把bit0置一MOVE做不到普通AND/OR也做不到因为它们面向的是整个操作数不是某一位。这就是WAND/WOR/WXOR存在的理由它们把“与或非”下沉到字里的每一个位让你用一条指令精确修改指定位置。1.2 从“整字搬运”到“按位修改”的转变先说MOVE为什么不能随便用在状态字上。假设你有一个设备状态字bit0表示运行中bit1表示故障bit2表示本地/远程bit3表示参数校验通过。某天你只想把bit3从0改成1于是写了StatusWord : 16#0008; (* 灾难现场 *)这一写bit0、bit1、bit2全部被清零。上位机那边看到设备突然“停机”“无故障”“手动模式”而实际上设备运行得好好的。这就是整字搬运的典型事故它把目标字里所有位都强制覆盖成了你给的数值哪怕你没提那些位。按位修改的思路完全不同先“屏蔽”再“置位”。屏蔽就是让无关位保持原样只开放你想改的那一位。放到位运算里就是用一个掩码去“保护”其他位想让某些位保持原值就把掩码对应位置1因为 1 AND x x想让某些位强制清零就把掩码对应位置0因为 0 AND x 0想让某些位强制置一就把掩码对应位置1再做OR因为 1 OR x 1。这套思路翻译成指令就是屏蔽用WAND置位用WOR翻转用WXOR。理解了模型后面看代码就不费劲了。2. WAND/WOR/WXOR指令语义精读2.1 三条指令的位级真值先给结论再解释。指令掩码位0时的效果掩码位1时的效果典型用途WAND按位与目标位强制清0目标位保持原值复位专用、屏蔽用于“关锁”WOR按位或目标位保持原值目标位强制置1置位专用用于“开锁”WXOR按位异或目标位保持原值目标位取反状态翻转、边沿模拟这里最容易绕晕的是WAND。大家受普通与逻辑影响总以为“WAND就是把位置1”实际恰恰相反——它擅长的是把位置0因为掩码为0的位会被清零。你拿WAND去置位大概率得到一个意想不到的0。我自己就干过这事想用WAND把bit2置一写了 WAND(StatusWord, 16#0004)结果bit2反而变成0其他位倒没事。后来才明白WAND的掩码设计逻辑是“0表示复位1表示放行”。WOR就直观多了掩码某位是1目标位就变成1掩码某位是0目标位不动。开锁、置位、启动命令都靠它。WXOR是三个里最有意思的掩码位1时目标位取反掩码位0时不动。它不需要知道目标位当前是什么状态直接翻。所以做“单按钮模式切换”特别合适按一次从自动切手动再按一次切回来不用读原状态。通用语法写出来大概是这个样子不同品牌略有差异但逻辑一致d : WAND(source, mask); (* 各位1保留0清零 *) d : WOR(source, mask); (* 各位1置一0保留 *) d : WXOR(source, mask); (* 各位1取反0保留 *)很多纯ST环境里没有独立的WAND函数直接写d : source AND mask;效果一样。维保程序的时候看到这种写法也别慌本质就是位与。2.2 掩码设计与十六进制换算掩码是这三个指令的灵魂。写错了不会报错但联锁逻辑会以非常隐蔽的方式失效。掩码设计说白了就是一道十六进制翻译题你先把想操作的位标出来然后翻译成16进制数。举例一个控制字Controlbit0是正转bit1是反转bit2是点动。现在要把反转和点动都复位只保留正转位保持那么掩码应该是bit0保持掩码位写1bit1清零掩码位写0bit2清零掩码位写0bit3~bit15全部保持写1所以掩码二进制是 1111 1111 1111 1000低三位是100bit1和bit2为0十六进制就是16#FFF8。Control : WAND(Control, 16#FFF8); (* 把反转、点动关掉 *) Control : WOR(Control, 16#0001); (* 然后只开放正转 *)这套组合拳执行完以后控制字的状态是正转1、反转0、点动0其他位一概没动。你说它香不香。如果要置位一个比较高的位比如bit8掩码就是16#0100。人手算容易错我的习惯是先在纸上把位从0到15排成一行标出要操作的位置再四位一组转十六进制。四位二进制对应一位十六进制这个映射关系熟练了以后基本一眼能出结果。实在没把握就用Windows计算器切换到程序员模式输入二进制看十六进制慢不了几秒。3. 联锁场景完整实操3.1 正反转互锁的ST实现先来个最经典的电机正反转互锁。这是所有联锁逻辑的基本盘。需求一句话正转和反转任何时候都不能同时为真否则主回路直接短路。我习惯用一个INT型的控制字 Motor_Ctrl 来管理bit0正转命令bit1反转命令bit2点动命令点动时也要和正反转互锁正转启动逻辑Motor_Ctrl : WAND(Motor_Ctrl, 16#FFF9); (* 清反向位和点动位 *) Motor_Ctrl : WOR(Motor_Ctrl, 16#0001); (* 置正转位 *)反转启动逻辑Motor_Ctrl : WAND(Motor_Ctrl, 16#FFFA); (* 清正转位和点动位 *) Motor_Ctrl : WOR(Motor_Ctrl, 16#0002); (* 置反转位 *)停止逻辑Motor_Ctrl : WAND(Motor_Ctrl, 16#FFF8); (* 三个运行相关位全清 *)这套写法的精髓在顺序先在同一个扫描周期内把冲突位全部清掉再置目标位。两条指令连续执行从PLC外部看就是原子操作不存在“正转位还亮着、反转位也亮了”的中间状态。你可能会问为什么不把WAND和WOR合成一条物理上做不到因为你要对不同的位做不同的操作WAND负责清WOR负责置各管一段。实际工程中启动正转前还要加条件判断IF Start_Fwd AND bNotFault AND bReady THEN Motor_Ctrl : WAND(Motor_Ctrl, 16#FFF9); Motor_Ctrl : WOR(Motor_Ctrl, 16#0001); END_IF;bNotFault、bReady这些就是BOOL级别的使能条件它们只决定要不要执行联锁操作一旦执行就是位操作上场。3.2 泵阀组合联锁多状态字一次更新第二个场景是泵阀组合比正反转稍微复杂一点但更能体现字操作的价值。某输送系统有三台泵和三个阀门工艺要求泵P1运行时阀V1必须开泵P2运行时阀V2必须开但P1和P2不允许同时运行因为下游管线压力会超限。阀门本身有“开到位”“关到位”反馈会写进另一个状态字。我把控制逻辑分成三个字Pump_Ctrlbit0P1启bit1P2启bit2P3启Valve_Ctrlbit0V1开bit1V2开bit2V3开Device_Statusbit0P1运行bit1P2运行bit2V1开到位bit3V2开到位bit4V3开到位启动P1的操作不是只置Pump_Ctrl的bit0而是要联动阀IF Start_P1 THEN Pump_Ctrl : WAND(Pump_Ctrl, 16#FFF9); (* 清P2、P3启动位 *) Pump_Ctrl : WOR(Pump_Ctrl, 16#0001); (* 置P1启动 *) Valve_Ctrl : WAND(Valve_Ctrl, 16#FFFE); (* 确保V1开先把bit0清零 *) Valve_Ctrl : WOR(Valve_Ctrl, 16#0001); (* 再置bit0 *) END_IF;这套动作把“泵互锁”和“阀联动”放在同一个条件分支里一次扫描周期完成。Placement上有个小心机先清泵指令再置泵指令再清阀位、置阀位。顺序不能乱因为如果先把阀开了P1还没起来会短暂出现“阀开但泵停”的空管状态反过来先停泵后关阀才是安全顺序。我们这套是先泵后阀对启动来说没问题因为启动过程中短暂开阀不进料比憋压安全。停泵的时候顺序要反着来先关泵、再关阀IF Stop_P1 THEN Pump_Ctrl : WAND(Pump_Ctrl, 16#FFFE); (* 清P1 *) Valve_Ctrl : WAND(Valve_Ctrl, 16#FFFE); (* 清V1 *) END_IF;要理解这里为什么要对 Valve_Ctrl 先 WAND 再 WOR如果直接用 MOVE 写 16#0001会把V2、V3的状态也改了。多阀门并联系统中阀门状态相互独立取消一台泵不能影响另外两台泵的阀门。这就是按位操作最典型的价值隔离修改范围把副作用降到零。3.3 自动/手动模式切换的WXOR应用第三个场景聊WXOR它做模式切换是一把好手。设备有自动和手动两种模式用一个 Mode_Word 的bit0表示0手动1自动。操作面板上只有一个“模式切换”按钮按一次切一次不需要两个按钮分开点。需求描述成逻辑就是每次按钮上升沿bit0取反。用WXOR写IF ModeBtn_Rising THEN Mode_Word : WXOR(Mode_Word, 16#0001); (* bit0翻转 *) END_IF;不用WXOR的话你得先读回当前模式再判断再置位或复位少说三五行逻辑还有可能因为上位机写入延迟读到旧值。WXOR一条搞定干净利落。这里有个常见的误用要提醒一下WXOR适合做“状态翻转”但不适合做“条件触发”。如果你写的逻辑是“故障时把输出位翻转”那故障消失后输出不会自动恢复。因为翻转不记忆因果它只做取反。真正做故障联锁停机必须用WAND把运行位清零而不是WXOR否则下次启动时输出状态是反的设备直接跳过安全逻辑动作极危险。另一个适合WXOR的场景是报警字复位。不少PLC的报警寄存器写入1表示锁存报警要清除某条报警传统做法是给对应位写0。但有的老设备协议里写“写1清除”这时候WXOR就能用来产生一个“变位脉冲”IF AlarmAck_Rising THEN Alarm_Word : WXOR(Alarm_Word, 16#0008); (* bit3翻转产生边沿 *) END_IF;具体是否适合得看现场协议但原理是同一个WXOR给目标位制造一个“状态变化”而很多复位逻辑认的就是这个变化。4. 踩坑实录与排查建议4.1 常见问题速查表WAND/WOR/WXOR看着简单真正上手以后坑不少。我整理了一份速查表全是现场真金白银换来的。现场表现可能原因排查方法某一位怎么都置不上掩码写反WAND掩码0位把目标位清了用在线监视看掩码与实际值确认自己用的是WOR不是WAND相邻位被莫名其妙清零掩码的“保留区”位写成了0把掩码展开成二进制逐个位核对同一位置一后立刻变回0后续逻辑里又有一条WAND覆盖了该位交叉引用该控制字看所有写入点模式切换后不动作WXOR用在了条件触发上状态不因果检查该位是否还受其他逻辑控制按钮按下瞬间有效扫描几轮后失效按钮信号未做边沿检测每个周期都在翻转改成上升沿触发的BOOL变量再执行WXOR停机时设备还在运行停止逻辑只清了运行位没清联锁位停止分支里把相关互锁位一并WAND清零最隐蔽的一个坑是“多个FB共同写入同一个控制字”。比如上位机写了一次控制字你本地逻辑又写了一回两边各自用WAND/WOR改自己的位看似不冲突但如果某一边先用MOVE整字写入另一边基于旧值算出来的WAND结果就会被整体覆盖。解决办法是明确控制字的所有权一个字只有一个FB能写其他FB要用只能通过接口读。4.2 排查手段与实用心得排查位操作问题时在线监视是首选。把控制字按二进制显示一条一条步进观察目标位变化是否符合预期。大多数IDE都支持把变量右键切换成二进制显示这一步不要省十六进制看不出哪一位在跳。第二个实用手段是写一个简单的“位解析”函数块把INT拆成16个BOOL输出在HMI上做个联锁状态页调试效率翻倍。不用写复杂逻辑每位移出来判断一下就行FOR i : 0 TO 15 DO BitArray[i] : (Value MOD 2) 1; Value : SHR(Value, 1); END_FOR;第三个经验是注释规范。位操作代码一行指令就改好几个位不写注释三个月后自己都看不懂。我现在的习惯是每条WAND/WOR后面跟一行注释写清楚“哪几位被操作、被操作成什么”Pump_Ctrl : WAND(Pump_Ctrl, 16#FFF9); (* bit1, bit2清0禁P2/P3 *) Pump_Ctrl : WOR(Pump_Ctrl, 16#0001); (* bit0置1启P1 *)不要嫌注释啰嗦。维护工程师拿到这种代码五分钟就能理解全套联锁策略比看图纸快得多。还有一个容易忽略的时序问题WAND/WOR执行后新值要等到本次扫描周期结束时才统一刷新到外部输出。如果你在同一个周期里先写完控制字又立刻读这个控制字去判断设备状态读到的可能是旧值。这种情况要把判断逻辑放到下一扫描周期或者用临时变量先存一份新值。最后再分享一个实实在在的小技巧很多人写联锁逻辑时会陷入“BOOL变量地狱”——正转一个BOOL、反转一个BOOL、点动一个BOOL互相加互锁条件IF嵌套五六层改一处牵全身。用WAND/WOR/WXOR把位操作吃透之后我强烈建议你试试“控制字按位处理”的写法。它并不会让逻辑变复杂只是把分散在各个BOOL之间的互锁关系收敛到一个字的几位里改联锁策略的时候只动掩码不用满屏找变量。我个人经验是第一次从BOOL写法迁移到字操作确实要适应几天因为调试时看着十六进制数不如看BOOL直观。但等你把位和十六进制映射练熟了再回头看以前的代码会明显感到那些多层IF又笨又难维护。趁手头的项目还没跑起来拿一个控制字练练手感受一次“一条指令改一个位、其他位纹丝不动”的爽感你会回来找我聊的。