☰
ST联锁编程进阶:用WAND/WOR/WXOR位运算精简多条件联锁逻辑
2026/10/11 3:03:27 网站建设 项目流程

写ST联锁写了五六年,我越来越觉得,能在关键时候把代码从一团乱麻里捞出来的,往往不是那些花哨的算法,而是最容易被忽略的字逻辑运算WAND、WOR、WXOR。大多数工程师遇到多条件联锁,第一反应就是A AND B AND C一路串下去,条件一旦超过十个,整面墙都是布尔表达式,看着头大,改起来更头大。而IEC 61131-3体系里,WAND、WOR、WXOR能对WORD、DWORD、LWORD按位做与、或、异或,十来个联锁条件一次就能折叠进一个32位联锁字。后面无论做总联锁判定、首出报警锁存,还是给HMI上报状态,都清爽得多。这篇文章不讲手册里已有的指令定义,只讲我在实际项目里怎么用这三个指令搭联锁,以及踩过的坑,适合已经会写ST、想往位级思维再走一步的同行。

1. 为什么联锁代码越写越“肿”:字级运算的价值所在

1.1 先看一个让代码膨胀的经典写法

假设要做一个设备启动允许判断,条件包括:急停未触发、门锁关到位、液压站压力正常、润滑油流量正常、温度不超限、变频器无故障、接触器无反馈异常、模式选择在自动位。教科书式写法是这样的:

IF b_EMO_Not AND b_DoorLocked AND b_HydPress_OK AND b_LubFlow_OK AND b_Temp_OK AND b_Drive_OK AND b_KMD_OK AND b_Mode_Auto THEN b_RunPermit := TRUE; ELSE b_RunPermit := FALSE; END_IF;

这段代码本身没有错,但在真实项目里,信号不会叫这么短的名字。它们可能散落在各个功能块里,写成 fb_SafeMon.bEMO_Closed、fb_Station2.Status.DoorLocked、fb_Hydraulic.bPressureOK 这种长名,一行轻轻松松超出屏幕。更要命的是调试阶段:设备起不来,想知道到底是哪个条件不满足,只能一条一条在监控表里看,或者临时加一堆输出变量,非常浪费时间。

条件数量翻一倍呢?16个条件就是16个AND串在一起,加上换行缩进,整个IF判断得滚两屏。而且每增加一个条件,都要小心翼翼地把新变量插到对应的位置,漏掉一个括号或者写错一个名字,编译通过了但逻辑却不对。这样的代码,说它是“联锁”,不如说它是一堵早晚要倒的墙。

1.2 联锁的本质:一组布尔条件的组合判定

把联锁想成“多个开关串联”,就很容易理解位级运算的价值。8个条件相当于8个开关串联,任何一个断开,灯就不亮。而8个开关的状态,本质上就是8个比特:1代表闭合,0代表断开。它们可以拼成一个8位二进制数。所以“所有条件都满足”这句话,翻译成位运算就是“这8个比特全都是1”。

反过来说,如果我们手里有一个“模板”,模板里想检查哪一位,哪一位就是1,不想管的位是0,这个模板就是掩码。把条件数和掩码一按位与,得到的结果如果等于掩码,说明模板里要求的位全都是1;结果不等于掩码,说明至少有一位不满足。这个逻辑干净利落,而且不管条件数是8个、16个还是32个,代码都只有一行。

很多同行第一次听到这个思路时,都会产生一个疑问:这不是把简单问题复杂化了吗?单个条件用布尔变量多直白,干嘛非要塞进一个整数里?我的回答是:当你只有两三个条件时,确实没必要。但现场设备的联锁条件动辄十几个、二十几个,还要做首出报警、历史记录、HMI显示,这时候把所有条件装进一个32位字,整套逻辑的复杂度反而降下来了。

1.3 WAND/WOR/WXOR到底在做什么

这三个指令我在联锁里分工挺明确,先列个表方便理解:

指令运算规则我在联锁里的用途
WAND对应位都为1结果才为1过滤出需要检查的位,判断总允许
WOR对应位只要有一个为1结果就为1多个状态字合并,或把派生条件合入
WXOR对应位相同为0、不同为1找出条件字与掩码的差异,做首出锁存

举个例子。条件字是二进制 0011 0101,掩码是 0000 1111,那么 WAND 结果是 0000 0101,不等于掩码 0000 1111,说明低四位里至少有一位不满足。再用 WXOR 算一下,0011 0101 XOR 0000 1111 = 0011 1010,非零位出现在bit0、bit2、bit3,这三个就是当前不满足的条件。你看,一条WAND找出“有没有问题”,一条WXOR找出“问题在哪几个位”,这就是位级联锁的骨架。

2. 联锁字的数据模型:每一位代表一个条件

2.1 位分配:先把“字典”定下来

用位级联锁之前,第一件事不是写代码,而是画一张位分配表。这个环节偷懒,后面维护就是灾难。我在模拟项目X里用的联锁字是32位的UDINT,位分配大致是这个样子:

位号变量名含义允许条件(1)
bit0EMO_OK急停未触发急停回路常闭点闭合
bit1DOOR_OK门锁关到位门锁开关闭合
bit2HYD_OK液压站压力正常压力开关闭合
bit3LUB_OK润滑油流量正常流量开关闭合
bit4TEMP_OK温度不超限温度检测正常
bit5DRV_OK变频器无故障变频器故障输出为0
bit6KMD_OK接触器无反馈异常反馈与指令一致
bit7MODE_OK模式处于自动模式选择在自动位
bit8-bit30RSV备用0
bit31IO_OKIO通讯正常从站在线且数据有效

这张表既是程序注释的一部分,也是后面HMI“位状态表”页面的字段来源。我习惯把位表存在代码文件头部,每位都用带名字的常量或者直接在程序里写注释引用,坚决不用魔法数字。位分配还有一个原则:把最关键的、必须最后兜底的条件放在最高位,比如IO_OK,这样即使某个位分配错了,最高位的总闸还在。

2.2 置位约定:把“允许”统一映射为1

这里有一个很多新手容易翻车的地方:IO信号本身的极性,和联锁条件里的“允许”含义,不一定同相。急停开关现场接的是常闭点,正常时输入为TRUE,代表急停没有按下,所以允许启动。但如果安全栅信号是反逻辑,同一个条件可能就要取反才能放进联锁字。

我的做法是专门写一个联锁信号采集段,把所有条件统一预处理成“1表示允许、0表示禁止”的BOOL,再放进联锁字。代码长这样:

// 联锁字清零后逐位写入,确保每个位都有确定值 stILock := UDINT#0; IF b_EMO_Closed THEN stILock.0 := TRUE; // 急停未触发 END_IF; IF b_DoorLocked THEN stILock.1 := TRUE; // 门锁关到位 END_IF; IF b_HydPress_OK THEN stILock.2 := TRUE; // 液压正常 END_IF; // 其余位同理

这里有一个很关键的习惯:每次先整体清零,再逐位置位。只置位不复位的写法容易留旧值,比如某一位在故障时被置了0,故障恢复后忘了把它置回1,联锁字就一直带着错误的0,设备明明条件都满足了却不允许启动。清零后再写,每个位每一轮扫描都有确定值,不会残留上一周期状态。

2.3 用SHL生成掩码,少写一点十六进制

掩码是“我想检查哪些位”的模板。最容易出错的地方就是手写十六进制:0xFF是低8位,0xFFFF是低16位,写错一位数字就全乱了。更稳妥的办法是用移位来生成:

// 掩码:需要检查bit0-bit7,即低8位 MASK_ILOCK := SHL(UDINT#1, 8) - UDINT#1;

SHL(UDINT#1, 8) 是把1左移8位变成0x100,减1后得到0xFF,正好是低8位全1。需要检查bit0、bit2、bit7这种间隔位时,可以分别生成再或起来:

MASK_PART := SHL(UDINT#1, 0) OR SHL(UDINT#1, 2) OR SHL(UDINT#1, 7);

这样掩码的含义一眼就能看出来,比0x1285这种数字清楚得多。掩码一旦定义好,我在整个程序里就不会再手改它,要改条件就回去改位表,再重新生成掩码。这个规矩看着琐碎,但在排查“为什么某个条件没生效”的时候,能帮你省掉大量猜谜时间。

3. 三段核心逻辑:总联锁判定、首出锁存、状态合并

3.1 总联锁判定:一行WAND替代一整排AND

条件字和掩码都准备好了,总允许的判断就变成了一行比较:

// 总允许:需要检查的联锁位全部为1 IF (stILock AND MASK_ILOCK) = MASK_ILOCK THEN b_RunPermit := TRUE; ELSE b_RunPermit := FALSE; END_IF;

有些IDE里AND默认就是位运算,有些IDE里需要写成 WAND(stILock, MASK_ILOCK) 这种函数式调用,语法有差异,查一下帮助文档再写。关键是逻辑:结果等于掩码,说明全部满足;结果不等于掩码,说明至少有一位不满足。如果只想在真正需要时才计算,还可以先判断是否使能:

IF b_Enable AND ((stILock AND MASK_ILOCK) = MASK_ILOCK) THEN b_RunPermit := TRUE; ELSE b_RunPermit := FALSE; END_IF;

这比一长串AND表达式好在哪里?一是好读,一眼看出判断对象是“这个联锁字”;二是好扩展,新增条件无非改MASK和置位段,不碰这一行判定逻辑;三是好调试,打开监控表看 stILock 这一行二进制,所有条件状态一览无余。

3.2 首出锁存:WXOR找出“第一个翻车的位”

首出报警,就是在多个联锁条件同时不满足时,准确报出最先触发的那一个。很多设备安全规范里是明确要求这个功能的。位级做法非常优雅:当总允许从TRUE变成FALSE的那个扫描周期,用WXOR比较条件字和掩码,凡不满足的位异或结果会是1,锁存下来再去定位最低非零位即可。

真正要做得稳,还得解决两件事:一是“不满足的位可能同时有好几个”,但首出只需要业务上优先级最高的那一个,我一般取最低非零位;二是要用沿检测捕获“总允许下降沿”这个时刻点。实现代码如下:

// 下降沿:总允许从TRUE翻到FALSE的那一拍 b_FallEdge := b_RunPermit_Prev AND NOT b_RunPermit; IF b_FallEdge THEN // 不满足的位,异或后为1 nErrMask := MASK_ILOCK XOR stILock; // 找最低非零位作为首出位 nFirstBit := -1; FOR i := 0 TO 31 DO IF (nErrMask AND SHL(UDINT#1, i)) <> 0 THEN nFirstBit := i; EXIT; END_IF; END_FOR; bLockOut := TRUE; END_IF; b_RunPermit_Prev := b_RunPermit;

代码里 nErrMask := MASK_ILOCK XOR stILock; 就是在做WXOR,有些IDE直接写成 WXOR(MASK_ILOCK, stILock) 也一样。注意我用 b_RunPermit_Prev 记录了上一周期的总允许状态,b_RunPermit_Prev AND NOT b_RunPermit 就是下降沿。沿检测本身是时序逻辑,靠的是变量自己记住上一拍,不是靠某条指令算出来的。

还有一个容易踩的坑:如果你在一个程序周期里同时更新 stILock 和做总允许判定,那么下降沿检测触发时,stILock 已经是本周期的新值,异或算出的“不满足位”可能包含刚发生变化的多个位。我习惯先把上一周期的 stILock 保存成 stILock_D1,等到检测到下降沿时,用 stILock_D1 和掩码做异或,这样锁存的是“导致允许消失的那一拍”的真实条件字。

3.3 用WOR合并多个状态字上报HMI

设备通常有很多子系统,每个子系统都有自己的状态字,HMI如果挨个读几十个BOOL会很费通讯资源。用WOR可以把状态字按位或成一个汇总字,一次上报:

stSysStatus := WOR(fbDriveUnit.stDiag, fbPumpUnit.stDiag); stSysStatus := WOR(stSysStatus, fbValveUnit.stDiag);

注意位分配不要重叠。子系统A的状态位用了bit0~bit7,子系统B就必须从bit8开始定义,否则或在一起会互相干扰。这跟前面联锁字的位表是一套管理方法,本质上都是“位号总表”。

3.4 一个完整的联锁单元示例

把上面几段串成一个完整的模拟项目X联锁单元,放在一个程序任务里:

// 联锁信号采集 stILock := UDINT#0; IF b_EMO_Closed THEN stILock.0 := TRUE; END_IF; IF b_DoorLocked THEN stILock.1 := TRUE; END_IF; IF b_HydPress_OK THEN stILock.2 := TRUE; END_IF; IF b_LubFlow_OK THEN stILock.3 := TRUE; END_IF; IF b_Temp_OK THEN stILock.4 := TRUE; END_IF; IF b_Drive_OK THEN stILock.5 := TRUE; END_IF; IF b_KMD_OK THEN stILock.6 := TRUE; END_IF; IF b_ModeAuto THEN stILock.7 := TRUE; END_IF; // 总允许 MASK_ILOCK := SHL(UDINT#1, 8) - UDINT#1; // 低8位 b_RunPermit := (stILock AND MASK_ILOCK) = MASK_ILOCK; // 首出锁存 b_FallEdge := b_RunPermit_Prev AND NOT b_RunPermit; IF b_FallEdge THEN nErrMask := MASK_ILOCK XOR stILock; FOR i := 0 TO 7 DO IF (nErrMask AND SHL(UDINT#1, i)) <> 0 THEN nFirstBit := i; EXIT; END_IF; END_FOR; END_IF; b_RunPermit_Prev := b_RunPermit;

输入输出对应关系如下表,联锁解锁后直接看 nFirstBit 就能定位到具体原因,不需要逐个点监控:

首出位对应条件处置方向
0急停触发或回路断线检查急停回路
1门锁未到位检查门锁与气动机构
2液压压力不足检查油泵与压力开关
3润滑油流量不足检查油位与流量开关
4温度超限检查散热与测温回路
5变频器故障查看变频器故障码
6接触器反馈异常检查接触器辅点与线圈
7非自动模式切换模式选择开关

这个表直接贴在HMI画面旁边,操作人员查找原因的时候非常方便,还能避免“现场只会按复位,不知道故障根源”的情况。

4. 踩过的坑:位宽、扫描时序、掩码和断线

4.1 位宽混用:DWORD与WORD的“隐形截断”

联锁字用UDINT,掩码也用UDINT,但如果操作数里混进一个WORD,麻烦就来了。有一次我把从IO模块读来的WORD直接传给WAND,那个WORD的状态字最高位恰好是1,结果高位被解释成符号位,按位与之后整个结果都不对,现场设备偶尔启动不了,查了半天才发现是类型转换的锅。

经验就一条:所有参与位运算的变量,统一声明为UDINT或者DWORD,禁止WORD和UDINT混用。从IO读来的WORD,先显式转换成UDINT,再参与位运算。如果选了64位的LINT,要确认IDE支持的是LAND而不是WAND,别硬套32位的写法。这类问题编译阶段往往不报错,报错也只是一句“类型不匹配”,真跑起来才暴露,所以一开始就要把类型管住。

4.2 扫描周期中间态:联锁字为什么会闪跳

联锁字在一个扫描周期里被多次写入时,如果IO信号之间没有同步,可能在某次采样时读到“又没门锁、又有急停”的中间状态,总允许瞬间翻转一下。这个问题在仿真器里很难复现,真机却会偶尔出现,表现为设备明明没故障却闪停一下。

我的对策是先把IO统一做一次快照,把所有输入考入中间变量,联锁程序只读中间变量,不直接引用IO地址。这样同一个扫描周期内所有联锁位看到的是同一份快照,不会因为IO刷新顺序不同而分裂。对需要抗抖动的信号,再加一个“连续N个周期保持才生效”的滤波器,避免电磁干扰引起误触发。这套做法会让程序多一层变量,但换来的是联锁的确定性,值得。

4.3 掩码写错与逐位强制测试

掩码0xFF写成0xF,或者bit号错位一位,这类错误最隐蔽,因为总允许照样输出TRUE,设备也能正常启动,只是某个条件从来没参与过联锁。等到真正危险发生,才发现那一脚刹车是空的。我后来定了一条死规矩:任何新联锁逻辑上线前,必须做一轮逐位强制测试。

方法不复杂,从bit0开始,把条件字里的某一个位在程序里强制置0,确认总允许下拉、首出位显示正确,然后恢复,接着测下一位。这样一轮下来,掩码写没写错、位号对没对上,当场就能暴露。测试过程最好做成一个临时测试段,用HMI或调试面板上的开关来触发,别用在线强制的硬办法,不然一个不小心就把整个CPU的内存改乱了。

4.4 从站通讯断线,联锁字怎么处理

如果联锁条件有很大一部分来自远程IO从站,那从站断线时输入字会清零、保持旧值,甚至出现全1,具体行为取决于从站配置。无论哪种,都可能导致联锁误判。我的做法很简单:在联锁字最高位专门放一个“IO通讯正常”位,代码在联锁采集段里加一句:

IF bIO_CommOK THEN stILock.31 := TRUE; END_IF;

MASK里也把bit31加进去。这样只要从站一断线,总允许必然变成FALSE,设备不会在“数据已经不可信”的情况下继续动作。等通讯恢复,条件字重新建立后再由其他条件决定是否允许启动。这套办法虽然朴实,但拦住过好几次莫名其妙的启动事故。

5. 维护心得:把联锁逻辑封装成一个可复用FB

5.1 FB接口设计:让外部只连“条件”和“复位”

前面这一套逻辑,每次项目都重写一遍太不划算了。我把它封装成一个功能块FB_ILock,接口大致是这样:

FUNCTION_BLOCK FB_ILock VAR_INPUT bEnable : BOOL; arCond : ARRAY[0..31] OF BOOL; // 每个元素对应一个联锁条件 bReset : BOOL; // 复位首出锁存 END_VAR VAR_OUTPUT bPermit : BOOL; // 总允许 bFailActive : BOOL; // 已锁存首出 nFailBit : INT; // 首出位号 0..31 END_VAR VAR stCondWord : UDINT; stMaskWord : UDINT; stErrMask : UDINT; bPrevPermit : BOOL; END_VAR

FB内部做的事情很固定:数组转联锁字、掩码判定总允许、WXOR锁存首出、复位处理。调用方只需要把32个条件BOOL数组连过来,剩下的细节都藏在FB里。打开实例监控,里面 stCondWord、stErrMask、nFailBit 一目了然,维护起来比散落在主程序里的几十行IF舒服得多。

用FB还有一个额外的好处:以后想加“首出优先级排序”,只需要在VAR_INPUT里加一个优先级数组,内部排序逻辑改一次,所有实例跟着升级。比起让每个设备程序复制粘贴同一段联锁代码,这个方式的长期维护成本低太多了。

5.2 位级方案的边界:哪些场景不适合硬套

不是所有联锁都适合位级硬套。条件很少,比如只有两三个,直接写AND组合反而更直白,没必要为了用位运算而位运算。条件里如果包含大量模拟量比较,也建议先把比较结果转成BOOL,再置位到联锁字,不要在FB里塞模拟量计算,否则FB的通用性和可读性都会被拖累。

还有一种情况最需要警惕:条件之间有复杂的或逻辑,比如“两台泵任意一台可用”“A联锁与B联锁互为备用”。这种不能用简单的一个位来表示,得先用传统布尔表达式把派生条件算好,再作为一位输入FB。换句话说,FB接收的一定是最终化简后的“允许/禁止”布尔值,不是还没规整的原始信号。这一点我在给团队新人培训时反复强调过,因为它决定了FB的适用范围。

5.3 在线调试技巧:联锁字可视化与故障注入

在线监控一个UDINT联锁字,直接看数字实在太累,尤其32位全亮的时候,一串十六进制根本看不出谁是谁。我通常把联锁字复制到一个BOOL数组,在HMI或上位机做一个位状态表,每一行显示位号、条件名、当前状态。对模拟量类条件,显示的是“比较结果:满足/不满足”,不是原始数值,这样排障时一眼就能找到是哪一位拉了后腿。

另一个很实用的套路是故障注入测试。我在联锁采集段前面预留了一个测试开关,某个测试变量为TRUE时强制把指定位置0,效果等同于现场把对应信号断开,专门用来验证首出锁存逻辑是否正确。这个功能在项目验收、改造验证时特别香,比拿着螺丝刀去短接端子安全得多。

最后再分享一个小经验:首出位不一定是数值最小的位,HMI显示时最好用条件名映射表,不要只显示一个数字位号,否则操作工根本不知道“bit3”是哪个条件。我现在的习惯是,每个项目的联锁字位表都放在程序区开头的大段注释里,谁接手都能在五分钟内看懂。这几个指令本身并不复杂,真正值钱的,是把它放进什么样的数据结构里。

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

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

立即咨询