☰
ST语言位操作指令WAND/WOR/WXOR实现设备联锁控制
2026/10/11 1:31:59 网站建设 项目流程

做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都为TRUE,a才为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进制数。

举例:一个控制字Control,bit0是正转,bit1是反转,bit2是点动。现在要把反转和点动都复位,只保留正转位保持,那么掩码应该是:

  • bit0:保持,掩码位写1
  • bit1:清零,掩码位写0
  • bit2:清零,掩码位写0
  • bit3~bit15:全部保持,写1

所以掩码二进制是 1111 1111 1111 1000(低三位是100,bit1和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_Ctrl:bit0=P1启,bit1=P2启,bit2=P3启
  • Valve_Ctrl:bit0=V1开,bit1=V2开,bit2=V3开
  • Device_Status:bit0=P1运行,bit1=P2运行,bit2=V1开到位,bit3=V2开到位,bit4=V3开到位

启动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又笨又难维护。趁手头的项目还没跑起来,拿一个控制字练练手,感受一次“一条指令改一个位、其他位纹丝不动”的爽感,你会回来找我聊的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询