設(shè)備聯(lián)鎖控制)
做ST邏輯的老哥們多半都經(jīng)歷過這種場景程序里一個封裝好的狀態(tài)字明明只想讓某一個聯(lián)鎖標(biāo)志位翻個狀態(tài)結(jié)果一條MOVE下去整個字被整塊覆蓋相鄰的運行位、報警標(biāo)志全部被打回原形。這種坑我踩過不止一次后來把邏輯想明白才發(fā)現(xiàn)ST語言里真正適合做聯(lián)鎖控制的不是常規(guī)的BOOL與或非而是WAND、WOR、WXOR這一族位操作指令。這篇文章就圍繞這三個指令來拆。它們解決的核心問題很簡單在一個16位或32位的字里精確修改某一位或某幾位而不動其他位。應(yīng)用的典型場景就是設(shè)備聯(lián)鎖——正反轉(zhuǎn)互鎖、泵閥組合安全聯(lián)鎖、自動/手動模式切換、報警復(fù)位。適合正在寫設(shè)備控制邏輯的工程師尤其是做順序控制、安全回路、模式切換的人。不管你是剛接觸ST還是已經(jīng)用它寫過幾個項目下面這些掩碼寫法和避坑經(jīng)驗都能直接用上。1. 位操作聯(lián)鎖的底層邏輯1.1 為什么普通AND/OR做不了安全聯(lián)鎖很多人一開始會犯迷糊ST里不是已經(jīng)有AND、OR、XOR了嗎為什么還要單獨學(xué)WAND、WOR、WXOR區(qū)別在操作對象。普通AND、OR、XOR在IEC 61131-3里單次調(diào)用處理的是兩個BOOL量或者作為表達(dá)式運算符處理兩個BOOL結(jié)果還是一個BOOL。比如a : b AND c; (* 只有b和c都為TRUEa才為TRUE *)這種寫法適合做單點條件判斷急停按下、前門關(guān)好、油壓到達(dá)這幾個條件串起來輸出一個運行許可BOOL。但實際設(shè)備控制根本不是單點游戲。一個控制字往往塞了一堆指令位bit0是正轉(zhuǎn)命令、bit1是反轉(zhuǎn)命令、bit2是點動命令、bit3是故障復(fù)位、bit4是急停復(fù)位。這些位彼此獨立又存在互斥關(guān)系。你不可能用一條BOOL運算同時處理它們除非把一個字拆成十幾個BOOL變量然后每個BOOL單獨維護(hù)——程序會膨脹到?jīng)]法看而且跨FB傳參時接口多到懷疑人生。所以工程上的做法是壓縮把互相關(guān)聯(lián)的開關(guān)量打包成一個INT或DINT用字傳遞用位來區(qū)分。這個字就是聯(lián)鎖邏輯的主戰(zhàn)場。問題隨之而來——你怎么在不動其他位的前提下單獨把bit1清零、把bit0置一MOVE做不到普通AND/OR也做不到因為它們面向的是整個操作數(shù)不是某一位。這就是WAND/WOR/WXOR存在的理由它們把“與或非”下沉到字里的每一個位讓你用一條指令精確修改指定位置。1.2 從“整字搬運”到“按位修改”的轉(zhuǎn)變先說MOVE為什么不能隨便用在狀態(tài)字上。假設(shè)你有一個設(shè)備狀態(tài)字bit0表示運行中bit1表示故障bit2表示本地/遠(yuǎn)程bit3表示參數(shù)校驗通過。某天你只想把bit3從0改成1于是寫了StatusWord : 16#0008; (* 災(zāi)難現(xiàn)場 *)這一寫bit0、bit1、bit2全部被清零。上位機(jī)那邊看到設(shè)備突然“停機(jī)”“無故障”“手動模式”而實際上設(shè)備運行得好好的。這就是整字搬運的典型事故它把目標(biāo)字里所有位都強(qiáng)制覆蓋成了你給的數(shù)值哪怕你沒提那些位。按位修改的思路完全不同先“屏蔽”再“置位”。屏蔽就是讓無關(guān)位保持原樣只開放你想改的那一位。放到位運算里就是用一個掩碼去“保護(hù)”其他位想讓某些位保持原值就把掩碼對應(yīng)位置1因為 1 AND x x想讓某些位強(qiáng)制清零就把掩碼對應(yīng)位置0因為 0 AND x 0想讓某些位強(qiáng)制置一就把掩碼對應(yīng)位置1再做OR因為 1 OR x 1。這套思路翻譯成指令就是屏蔽用WAND置位用WOR翻轉(zhuǎn)用WXOR。理解了模型后面看代碼就不費勁了。2. WAND/WOR/WXOR指令語義精讀2.1 三條指令的位級真值先給結(jié)論再解釋。指令掩碼位0時的效果掩碼位1時的效果典型用途W(wǎng)AND按位與目標(biāo)位強(qiáng)制清0目標(biāo)位保持原值復(fù)位專用、屏蔽用于“關(guān)鎖”WOR按位或目標(biāo)位保持原值目標(biāo)位強(qiáng)制置1置位專用用于“開鎖”WXOR按位異或目標(biāo)位保持原值目標(biāo)位取反狀態(tài)翻轉(zhuǎn)、邊沿模擬這里最容易繞暈的是WAND。大家受普通與邏輯影響總以為“WAND就是把位置1”實際恰恰相反——它擅長的是把位置0因為掩碼為0的位會被清零。你拿WAND去置位大概率得到一個意想不到的0。我自己就干過這事想用WAND把bit2置一寫了 WAND(StatusWord, 16#0004)結(jié)果bit2反而變成0其他位倒沒事。后來才明白WAND的掩碼設(shè)計邏輯是“0表示復(fù)位1表示放行”。WOR就直觀多了掩碼某位是1目標(biāo)位就變成1掩碼某位是0目標(biāo)位不動。開鎖、置位、啟動命令都靠它。WXOR是三個里最有意思的掩碼位1時目標(biāo)位取反掩碼位0時不動。它不需要知道目標(biāo)位當(dāng)前是什么狀態(tài)直接翻。所以做“單按鈕模式切換”特別合適按一次從自動切手動再按一次切回來不用讀原狀態(tài)。通用語法寫出來大概是這個樣子不同品牌略有差異但邏輯一致d : WAND(source, mask); (* 各位1保留0清零 *) d : WOR(source, mask); (* 各位1置一0保留 *) d : WXOR(source, mask); (* 各位1取反0保留 *)很多純ST環(huán)境里沒有獨立的WAND函數(shù)直接寫d : source AND mask;效果一樣。維保程序的時候看到這種寫法也別慌本質(zhì)就是位與。2.2 掩碼設(shè)計與十六進(jìn)制換算掩碼是這三個指令的靈魂。寫錯了不會報錯但聯(lián)鎖邏輯會以非常隱蔽的方式失效。掩碼設(shè)計說白了就是一道十六進(jìn)制翻譯題你先把想操作的位標(biāo)出來然后翻譯成16進(jìn)制數(shù)。舉例一個控制字Controlbit0是正轉(zhuǎn)bit1是反轉(zhuǎn)bit2是點動?,F(xiàn)在要把反轉(zhuǎn)和點動都復(fù)位只保留正轉(zhuǎn)位保持那么掩碼應(yīng)該是bit0保持掩碼位寫1bit1清零掩碼位寫0bit2清零掩碼位寫0bit3~bit15全部保持寫1所以掩碼二進(jìn)制是 1111 1111 1111 1000低三位是100bit1和bit2為0十六進(jìn)制就是16#FFF8。Control : WAND(Control, 16#FFF8); (* 把反轉(zhuǎn)、點動關(guān)掉 *) Control : WOR(Control, 16#0001); (* 然后只開放正轉(zhuǎn) *)這套組合拳執(zhí)行完以后控制字的狀態(tài)是正轉(zhuǎn)1、反轉(zhuǎn)0、點動0其他位一概沒動。你說它香不香。如果要置位一個比較高的位比如bit8掩碼就是16#0100。人手算容易錯我的習(xí)慣是先在紙上把位從0到15排成一行標(biāo)出要操作的位置再四位一組轉(zhuǎn)十六進(jìn)制。四位二進(jìn)制對應(yīng)一位十六進(jìn)制這個映射關(guān)系熟練了以后基本一眼能出結(jié)果。實在沒把握就用Windows計算器切換到程序員模式輸入二進(jìn)制看十六進(jìn)制慢不了幾秒。3. 聯(lián)鎖場景完整實操3.1 正反轉(zhuǎn)互鎖的ST實現(xiàn)先來個最經(jīng)典的電機(jī)正反轉(zhuǎn)互鎖。這是所有聯(lián)鎖邏輯的基本盤。需求一句話正轉(zhuǎn)和反轉(zhuǎn)任何時候都不能同時為真否則主回路直接短路。我習(xí)慣用一個INT型的控制字 Motor_Ctrl 來管理bit0正轉(zhuǎn)命令bit1反轉(zhuǎn)命令bit2點動命令點動時也要和正反轉(zhuǎn)互鎖正轉(zhuǎn)啟動邏輯Motor_Ctrl : WAND(Motor_Ctrl, 16#FFF9); (* 清反向位和點動位 *) Motor_Ctrl : WOR(Motor_Ctrl, 16#0001); (* 置正轉(zhuǎn)位 *)反轉(zhuǎn)啟動邏輯Motor_Ctrl : WAND(Motor_Ctrl, 16#FFFA); (* 清正轉(zhuǎn)位和點動位 *) Motor_Ctrl : WOR(Motor_Ctrl, 16#0002); (* 置反轉(zhuǎn)位 *)停止邏輯Motor_Ctrl : WAND(Motor_Ctrl, 16#FFF8); (* 三個運行相關(guān)位全清 *)這套寫法的精髓在順序先在同一個掃描周期內(nèi)把沖突位全部清掉再置目標(biāo)位。兩條指令連續(xù)執(zhí)行從PLC外部看就是原子操作不存在“正轉(zhuǎn)位還亮著、反轉(zhuǎn)位也亮了”的中間狀態(tài)。你可能會問為什么不把WAND和WOR合成一條物理上做不到因為你要對不同的位做不同的操作WAND負(fù)責(zé)清WOR負(fù)責(zé)置各管一段。實際工程中啟動正轉(zhuǎn)前還要加條件判斷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級別的使能條件它們只決定要不要執(zhí)行聯(lián)鎖操作一旦執(zhí)行就是位操作上場。3.2 泵閥組合聯(lián)鎖多狀態(tài)字一次更新第二個場景是泵閥組合比正反轉(zhuǎn)稍微復(fù)雜一點但更能體現(xiàn)字操作的價值。某輸送系統(tǒng)有三臺泵和三個閥門工藝要求泵P1運行時閥V1必須開泵P2運行時閥V2必須開但P1和P2不允許同時運行因為下游管線壓力會超限。閥門本身有“開到位”“關(guān)到位”反饋會寫進(jìn)另一個狀態(tài)字。我把控制邏輯分成三個字Pump_Ctrlbit0P1啟bit1P2啟bit2P3啟Valve_Ctrlbit0V1開bit1V2開bit2V3開Device_Statusbit0P1運行bit1P2運行bit2V1開到位bit3V2開到位bit4V3開到位啟動P1的操作不是只置Pump_Ctrl的bit0而是要聯(lián)動閥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;這套動作把“泵互鎖”和“閥聯(lián)動”放在同一個條件分支里一次掃描周期完成。Placement上有個小心機(jī)先清泵指令再置泵指令再清閥位、置閥位。順序不能亂因為如果先把閥開了P1還沒起來會短暫出現(xiàn)“閥開但泵?!钡目展軤顟B(tài)反過來先停泵后關(guān)閥才是安全順序。我們這套是先泵后閥對啟動來說沒問題因為啟動過程中短暫開閥不進(jìn)料比憋壓安全。停泵的時候順序要反著來先關(guān)泵、再關(guān)閥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的狀態(tài)也改了。多閥門并聯(lián)系統(tǒng)中閥門狀態(tài)相互獨立取消一臺泵不能影響另外兩臺泵的閥門。這就是按位操作最典型的價值隔離修改范圍把副作用降到零。3.3 自動/手動模式切換的WXOR應(yīng)用第三個場景聊WXOR它做模式切換是一把好手。設(shè)備有自動和手動兩種模式用一個 Mode_Word 的bit0表示0手動1自動。操作面板上只有一個“模式切換”按鈕按一次切一次不需要兩個按鈕分開點。需求描述成邏輯就是每次按鈕上升沿bit0取反。用WXOR寫IF ModeBtn_Rising THEN Mode_Word : WXOR(Mode_Word, 16#0001); (* bit0翻轉(zhuǎn) *) END_IF;不用WXOR的話你得先讀回當(dāng)前模式再判斷再置位或復(fù)位少說三五行邏輯還有可能因為上位機(jī)寫入延遲讀到舊值。WXOR一條搞定干凈利落。這里有個常見的誤用要提醒一下WXOR適合做“狀態(tài)翻轉(zhuǎn)”但不適合做“條件觸發(fā)”。如果你寫的邏輯是“故障時把輸出位翻轉(zhuǎn)”那故障消失后輸出不會自動恢復(fù)。因為翻轉(zhuǎn)不記憶因果它只做取反。真正做故障聯(lián)鎖停機(jī)必須用WAND把運行位清零而不是WXOR否則下次啟動時輸出狀態(tài)是反的設(shè)備直接跳過安全邏輯動作極危險。另一個適合WXOR的場景是報警字復(fù)位。不少PLC的報警寄存器寫入1表示鎖存報警要清除某條報警傳統(tǒng)做法是給對應(yīng)位寫0。但有的老設(shè)備協(xié)議里寫“寫1清除”這時候WXOR就能用來產(chǎn)生一個“變位脈沖”IF AlarmAck_Rising THEN Alarm_Word : WXOR(Alarm_Word, 16#0008); (* bit3翻轉(zhuǎn)產(chǎn)生邊沿 *) END_IF;具體是否適合得看現(xiàn)場協(xié)議但原理是同一個WXOR給目標(biāo)位制造一個“狀態(tài)變化”而很多復(fù)位邏輯認(rèn)的就是這個變化。4. 踩坑實錄與排查建議4.1 常見問題速查表WAND/WOR/WXOR看著簡單真正上手以后坑不少。我整理了一份速查表全是現(xiàn)場真金白銀換來的?,F(xiàn)場表現(xiàn)可能原因排查方法某一位怎么都置不上掩碼寫反WAND掩碼0位把目標(biāo)位清了用在線監(jiān)視看掩碼與實際值確認(rèn)自己用的是WOR不是WAND相鄰位被莫名其妙清零掩碼的“保留區(qū)”位寫成了0把掩碼展開成二進(jìn)制逐個位核對同一位置一后立刻變回0后續(xù)邏輯里又有一條WAND覆蓋了該位交叉引用該控制字看所有寫入點模式切換后不動作WXOR用在了條件觸發(fā)上狀態(tài)不因果檢查該位是否還受其他邏輯控制按鈕按下瞬間有效掃描幾輪后失效按鈕信號未做邊沿檢測每個周期都在翻轉(zhuǎn)改成上升沿觸發(fā)的BOOL變量再執(zhí)行WXOR停機(jī)時設(shè)備還在運行停止邏輯只清了運行位沒清聯(lián)鎖位停止分支里把相關(guān)互鎖位一并WAND清零最隱蔽的一個坑是“多個FB共同寫入同一個控制字”。比如上位機(jī)寫了一次控制字你本地邏輯又寫了一回兩邊各自用WAND/WOR改自己的位看似不沖突但如果某一邊先用MOVE整字寫入另一邊基于舊值算出來的WAND結(jié)果就會被整體覆蓋。解決辦法是明確控制字的所有權(quán)一個字只有一個FB能寫其他FB要用只能通過接口讀。4.2 排查手段與實用心得排查位操作問題時在線監(jiān)視是首選。把控制字按二進(jìn)制顯示一條一條步進(jìn)觀察目標(biāo)位變化是否符合預(yù)期。大多數(shù)IDE都支持把變量右鍵切換成二進(jìn)制顯示這一步不要省十六進(jìn)制看不出哪一位在跳。第二個實用手段是寫一個簡單的“位解析”函數(shù)塊把INT拆成16個BOOL輸出在HMI上做個聯(lián)鎖狀態(tài)頁調(diào)試效率翻倍。不用寫復(fù)雜邏輯每位移出來判斷一下就行FOR i : 0 TO 15 DO BitArray[i] : (Value MOD 2) 1; Value : SHR(Value, 1); END_FOR;第三個經(jīng)驗是注釋規(guī)范。位操作代碼一行指令就改好幾個位不寫注釋三個月后自己都看不懂。我現(xiàn)在的習(xí)慣是每條WAND/WOR后面跟一行注釋寫清楚“哪幾位被操作、被操作成什么”Pump_Ctrl : WAND(Pump_Ctrl, 16#FFF9); (* bit1, bit2清0禁P2/P3 *) Pump_Ctrl : WOR(Pump_Ctrl, 16#0001); (* bit0置1啟P1 *)不要嫌注釋啰嗦。維護(hù)工程師拿到這種代碼五分鐘就能理解全套聯(lián)鎖策略比看圖紙快得多。還有一個容易忽略的時序問題WAND/WOR執(zhí)行后新值要等到本次掃描周期結(jié)束時才統(tǒng)一刷新到外部輸出。如果你在同一個周期里先寫完控制字又立刻讀這個控制字去判斷設(shè)備狀態(tài)讀到的可能是舊值。這種情況要把判斷邏輯放到下一掃描周期或者用臨時變量先存一份新值。最后再分享一個實實在在的小技巧很多人寫聯(lián)鎖邏輯時會陷入“BOOL變量地獄”——正轉(zhuǎn)一個BOOL、反轉(zhuǎn)一個BOOL、點動一個BOOL互相加互鎖條件IF嵌套五六層改一處牽全身。用WAND/WOR/WXOR把位操作吃透之后我強(qiáng)烈建議你試試“控制字按位處理”的寫法。它并不會讓邏輯變復(fù)雜只是把分散在各個BOOL之間的互鎖關(guān)系收斂到一個字的幾位里改聯(lián)鎖策略的時候只動掩碼不用滿屏找變量。我個人經(jīng)驗是第一次從BOOL寫法遷移到字操作確實要適應(yīng)幾天因為調(diào)試時看著十六進(jìn)制數(shù)不如看BOOL直觀。但等你把位和十六進(jìn)制映射練熟了再回頭看以前的代碼會明顯感到那些多層IF又笨又難維護(hù)。趁手頭的項目還沒跑起來拿一個控制字練練手感受一次“一條指令改一個位、其他位紋絲不動”的爽感你會回來找我聊的。