PLC 梯形圖程式設計審查報告
單車道雙向通行控制(車道感測器 A/B 狀態機)
| 審查對象: | 梯形圖程式(Rung 1–25,疑為 CCW / Micro800 平台) |
| 審查範圍: | 邏輯正確性、強健性、安全性、可維護性 |
| 報告日期: | 2026 年 7 月 5 日 |
| 結論摘要: | 發現 5 項嚴重問題、6 項重要問題、4 項改善建議 |
本報告依據五張程式截圖重建邏輯後進行分析;建議修改前先與原始專案檔核對。
依截圖重建之程式行為如下。系統為單車道雙向通行控制:車道兩端各有一支光電感測器 (INPUT1 = A 端、INPUT2 = B 端),以整數 STATE 實作狀態機:
| 值 | 狀態 | 意義 | 備註 |
|---|---|---|---|
| 0 | INIT |
上電初始 | 無任何 rung 處理此狀態 |
| 1 | IDLE |
車道淨空待機 | |
| 2 | FWD |
車輛由 A 往 B 通行中 | 驅動 OUTPUT_1、60 秒超時 |
| 3 | HOLD |
車輛已過 B,保持 10 秒 | 驅動 OUTPUT_1 |
| 4 | REV |
車輛由 B 往 A 通行中 | 不驅動任何輸出、無超時 |
| 5 | FAULT |
感測器卡住故障 | FaultLatch 驅動 OUTPUT_2 |
Rung 對照表
| Rung | 功能 | 邏輯 |
|---|---|---|
| 1–3 | INPUT1 邊緣偵測 | 手寫上升緣 M_INPUT1_Rise、下降緣 M_INPUT1_Fall、上週期值 M_INPUT1_Last |
| 4–6 | INPUT2 邊緣偵測 | 同上,產生 M_INPUT2_* |
| 8 | IDLE \(\to\) FWD | STATE=IDLE 且 A 上升緣 且 B 未遮斷 |
| 9 | IDLE \(\to\) REV | STATE=IDLE 且 B 上升緣 且 A 未遮斷 |
| 10 | FWD \(\to\) HOLD | STATE=FWD 且 B 上升緣 |
| 11 | FWD 超時計時 | STATE=FWD 啟動 TON_3(60 s) |
| 12 | 超時 \(\to\) IDLE | TON_3.Q 直接 MOV IDLE(未限定狀態) |
| 13 | HOLD 計時 | STATE=HOLD 啟動 TON_1(10 s) |
| 14 | HOLD \(\to\) IDLE | TON_1.Q 直接 MOV IDLE(未限定狀態) |
| 15 | HOLD \(\to\) FWD | STATE=HOLD 且 A 上升緣(後車跟進) |
| 16 | REV \(\to\) IDLE | STATE=REV 且 B 下降緣(條件錯誤) |
| 17 | 車道占用指示 | STATE=FWD 或 HOLD \(\to\) OUTPUT_1 |
| 19 | 卡住偵測計時 | INPUT1 或 INPUT2 遮斷即累計 TON_2(5 min) |
| 20–21 | 故障閂鎖 | TON_2.Q \(\to\) SensorStuck \(\to\) FaultLatch(L) |
| 22 | 進入 FAULT | FaultLatch 每掃描週期 MOV FAULT |
| 23 | 故障復歸 | PB_RESET 且 SensorStuck 已消失 \(\to\) 解閂(U) |
| 24 | 回到 IDLE | PB_RESET 直接 MOV IDLE(未限定狀態) |
| 25 | 故障指示 | FaultLatch \(\to\) OUTPUT_2 |
現況狀態轉移圖
問題分析
嚴重問題(會造成錯誤動作或死鎖)
critical STATE 上電為 0(INIT),但全程式沒有任何 rung 以 INIT 為條件或將其轉移到 IDLE。除非變數為保持型(retentive)且曾被手動改寫,否則每次上電後狀態機完全不動作, 唯一能離開 INIT 的路徑竟是按下 PB_RESET(Rung 24 的副作用)。
改善:加入首次掃描(first-scan / _SYSVA_FIRST_SCAN)rung: 上電時檢查兩支感測器皆淨空後 MOV IDLE \(\to\) STATE;若感測器有遮斷則進入 FAULT 或等待淨空。
critical Rung 24 只要按下 PB_RESET 就執行 MOV IDLE \(\to\) STATE,完全不檢查目前狀態:
車輛正在通行(
FWD/REV/HOLD)時誤按復歸,車道占用指示OUTPUT_1立即熄滅,對向車輛可能進入 —— 安全隱患。故障未排除時(
SensorStuck仍為真)按住PB_RESET:Rung 22 每週期寫入FAULT,Rung 24 又寫入IDLE,STATE在兩值之間每週期震盪; 而 Rung 8–17 位於 Rung 22 之前,看到的是上一週期殘留值,行為不可預測。
改善:Rung 24 的條件應為 PB_RESET 上升緣 且 STATE = FAULT 且 NOT FaultLatch(即 Rung 23 已成功解閂)。
critical 車輛由 B 端進入往 A 行駛:遮斷 B \(\to\) REV;車尾離開 B 感測器時 INPUT2 下降緣成立, 狀態立刻回 IDLE —— 但此時車輛還在車道中間!接著車輛抵達 A 端遮斷 INPUT1, Rung 8 條件(IDLE 且 A\(\uparrow\) 且 B 淨空)全部成立,系統誤判為一台新車要從 A 進入, 錯誤進入 FWD 並啟動 60 秒計時。單一車輛正常通過就會觸發整條錯誤流程。
改善:REV 應在車輛通過 A 感測器(M_INPUT1_Rise 或 M_INPUT1_Fall)時才結束,並比照 FWD 增加 HOLD 緩衝與 60 秒超時。
critical TON_3.Q \(\to\) MOV IDLE 與 TON_1.Q \(\to\) MOV IDLE 都沒有串接 EQU(STATE, FWD/HOLD)。目前僅靠「離開狀態後 TON 輸入變假、Q 自動復歸」的副作用維持正確, 這依賴掃描順序而且非常脆弱:任何人日後搬動 rung 順序、把計時器改為保持型(RTO)、或在 FAULT 進入的同一週期 Q 尚未復歸,都會讓 IDLE 覆寫掉不該覆寫的狀態 (例如把 FAULT 蓋回 IDLE)。
改善:所有寫入 STATE 的 rung 一律以「目前狀態」為第一個條件, 例如 EQU(STATE,FWD) AND TON_3.Q \(\to\) MOV IDLE。
critical Rung 1–2 的接點顯示為 INPUT1_Last,但 Rung 3 的線圈寫入的是 M_INPUT1_Last (INPUT2 同樣情形)。若這是兩個不同的變數,邊緣偵測永遠不會正確動作 (_Last 永遠為 0,上升緣變成電平觸發、下降緣永遠不觸發),整個狀態機行為全部走樣。 請開啟專案檔確認是否只是畫面截斷;若真為兩個變數,此為第一優先修復項目。
重要問題(功能缺陷與安全疑慮)
major Rung 12 的設計意圖是處理「車輛迴轉(U-turn)沒走完全程」,但車輛拋錨卡在車道中間 超過 60 秒時同樣成立:狀態回 IDLE、OUTPUT_1 熄滅,對向車輛會被放行進入 仍被占用的車道。超時屬於異常,應轉入 FAULT(或至少獨立的 TIMEOUT 警告狀態)並保持占用指示, 由人員確認車道淨空後復歸。
major Rung 17 只有 FWD/HOLD 會點亮 OUTPUT_1。車輛由 B 往 A 通行期間 (REV)車道明明被占用,占用指示卻是熄滅的。若 OUTPUT_1 是「車道占用/禁止進入」 燈號,這是雙向控制的核心目的,屬於明顯遺漏。改善:Rung 17 增加 EQU(STATE,REV) 分支 (以及 REV 側若有對應的 HOLD 狀態)。
major IDLE 時 A、B 幾乎同時遮斷:Rung 8 因 INPUT2 已遮斷不成立,Rung 9 因 INPUT1 已遮斷不成立,兩邊都不轉移。且上升緣旗標只存在一個掃描週期,之後除非車輛倒退 重新觸發,否則兩台車會永遠停在原地等待。改善:改用電平(車輛仍遮斷感測器)搭配 先到優先或固定優先權裁決;或增加「兩側同時佔用」的仲裁狀態。
major INPUT1 OR INPUT2 並聯餵入同一顆 TON_2:若 A 端遮斷 4 分鐘後車輛離開、 B 端隨即有車遮斷,OR 結果從未斷開,計時跨感測器連續累積,1 分鐘後就誤報 SensorStuck。此外,合法的長時間停留(如車輛在感測器前排隊等待)也會被判為故障。 改善:每支感測器各用一顆 TON 獨立計時;並考慮只在「與目前狀態矛盾」時 (例如 IDLE 卻長時間遮斷)才計時。
major Rung 22 位於 Rung 8–17 之後:FaultLatch 成立的那個週期,前面的狀態機與輸出 仍以舊狀態運作一整個掃描週期。另外復歸流程分散在 Rung 22/23/24 三處且互相依賴掃描順序 (先解閂、再回 IDLE、但 Rung 22 又持續覆寫),維護時極易改壞。改善:故障偵測 rung 移到狀態機之前;復歸整合為單一 rung:PB_RESET\(\uparrow\) AND STATE=FAULT AND NOT SensorStuck 時同時解閂並 MOV IDLE。
major FWD 進行中第二台車在 A 端觸發上升緣會被忽略(沒有任何 rung 消化它),之後首車過 B 進入 HOLD,第二台車若已壓在 A 感測器上不再產生新的上升緣,Rung 15 不會成立。 改善:若場域允許多車,應加入車輛計數(A 進 +1、B 出 \(-1\),REV 反向), 以「車道內車輛數 = 0」作為回 IDLE 的條件,遠比純計時可靠。
可維護性建議
minor Rung 1–6 共六個 rung 手寫上升/下降緣,等效於平台內建的 R_TRIG/F_TRIG (或 OSR/ONS 指令)。手寫版本多兩個中間變數且依賴 rung 順序(_Last 必須最後更新), 是問題 5 這類錯誤的溫床。
minor STATE 目前有 8 個分散的寫入點(Rung 8/9/10/12/14/15/16/22/24), 任兩個 rung 在同一週期同時成立時,「最後掃描到的贏」,正確性完全綁死在 rung 排列順序上。 建議改用結構化文字(ST)的 CASE STATE OF 集中管理(見第 3.2 節範例), 或至少保證每個狀態的轉移條件互斥。
minor TON_1/TON_2/TON_3 無法從名稱看出用途,建議改為 tHoldDwell、tSensorStuck、tFwdTimeout; OUTPUT_1/OUTPUT_2 亦建議改為 LaneOccupiedLamp、FaultLamp。 另建議把 60 s/10 s/5 min 等時間常數集中為具名常數,便於現場調整。
minor Rung 23/24 直接使用按鈕電平。按住不放時 Rung 24 每個週期都執行 MOV; 搭配問題 2 的無條件寫入,行為更難預測。所有手動按鈕一律建議取上升緣(one-shot)觸發。
建議的改善方案
修正後的狀態機
新增 REV_HOLD(B\(\to\)A 方向的緩衝)與把超時導向 FAULT, 所有轉移一律以「目前狀態」為前提條件:
| 目前狀態 | 事件 | 次狀態 | 動作 |
|---|---|---|---|
| INIT | 首次掃描且 A、B 淨空 | IDLE | — |
| IDLE | A\(\uparrow\) 且 B 淨空 | FWD | 占用燈 ON、車數 \(\gets\) 1、啟動 60 s |
| IDLE | B\(\uparrow\) 且 A 淨空 | REV | 占用燈 ON、車數 \(\gets\) 1、啟動 60 s |
| IDLE | A、B 同時佔用 | FAULT/仲裁 | 依現場規則裁決 |
| FWD | B\(\uparrow\) | HOLD | 車數 \(-1\)、啟動 10 s |
| FWD | A\(\uparrow\)(後車) | FWD | 車數 \(+1\)、重啟 60 s |
| FWD | 60 s 逾時 | FAULT | 占用燈保持、故障燈 ON |
| HOLD | 10 s 到且車數 \(=0\) | IDLE | 占用燈 OFF |
| HOLD | A\(\uparrow\) | FWD | 車數 \(+1\) |
| REV / REV_HOLD | (與 FWD/HOLD 鏡像對稱) | — | — |
| FAULT | Reset\(\uparrow\) 且感測器已淨空 | IDLE | 解閂、燈復歸 |
結構化文字(ST)重構範例
以 CASE 集中所有狀態轉移,STATE 只有一個寫入區塊, 掃描順序不再影響正確性:
(* Edge detection: use built-in blocks instead of hand-written rungs *)
trigA_R(CLK := INPUT1); trigA_F(CLK := INPUT1); // R_TRIG / F_TRIG
trigB_R(CLK := INPUT2); trigB_F(CLK := INPUT2);
(* Per-sensor stuck detection, only when it contradicts the state *)
tStuckA(IN := INPUT1 AND (STATE = ST_IDLE), PT := T#5m);
tStuckB(IN := INPUT2 AND (STATE = ST_IDLE), PT := T#5m);
IF tStuckA.Q OR tStuckB.Q THEN FaultLatch := TRUE; END_IF;
(* Fault has absolute priority, evaluated BEFORE the state machine *)
IF FaultLatch THEN
STATE := ST_FAULT;
END_IF;
CASE STATE OF
ST_INIT:
IF NOT INPUT1 AND NOT INPUT2 THEN STATE := ST_IDLE; END_IF;
ST_IDLE:
IF trigA_R.Q AND NOT INPUT2 THEN
STATE := ST_FWD; VehCount := 1;
ELSIF trigB_R.Q AND NOT INPUT1 THEN
STATE := ST_REV; VehCount := 1;
END_IF;
ST_FWD:
IF trigA_R.Q THEN VehCount := VehCount + 1; END_IF;
IF trigB_R.Q THEN
VehCount := VehCount - 1;
IF VehCount <= 0 THEN STATE := ST_HOLD; END_IF;
END_IF;
IF tTransit.Q THEN // 60 s timeout -> FAULT, not IDLE
FaultLatch := TRUE; STATE := ST_FAULT;
END_IF;
ST_HOLD:
IF trigA_R.Q THEN STATE := ST_FWD; VehCount := 1; END_IF;
IF tDwell.Q THEN STATE := ST_IDLE; END_IF;
ST_REV: (* mirror of ST_FWD, exit on sensor A, own 60 s timeout *)
// ...
ST_FAULT:
IF trigReset.Q AND NOT INPUT1 AND NOT INPUT2 THEN
FaultLatch := FALSE; STATE := ST_IDLE;
END_IF;
END_CASE;
(* Timers driven by state *)
tTransit(IN := (STATE = ST_FWD) OR (STATE = ST_REV), PT := T#60s);
tDwell (IN := (STATE = ST_HOLD) OR (STATE = ST_REV_HOLD), PT := T#10s);
(* Outputs: pure functions of state -- REV now indicates occupancy too *)
LaneOccupiedLamp := (STATE = ST_FWD) OR (STATE = ST_HOLD)
OR (STATE = ST_REV) OR (STATE = ST_REV_HOLD)
OR (STATE = ST_FAULT); // keep lane blocked on fault
FaultLamp := FaultLatch;
若必須維持梯形圖,同樣原則可落地為:(1) 故障 rung 移到最前面;(2) 每個 MOV \(\to\) STATE 一律先串 EQU(STATE, 現態);(3) 相同現態的多個轉移條件彼此互斥。
優先順序總表
| 優先 | 項目 | 一句話摘要 |
|---|---|---|
| P0 | 問題 5 | 確認 _Last 標籤是否斷裂,否則邊緣偵測全失效 |
| P0 | 問題 1 | 加入首次掃描 INIT\(\to\)IDLE,否則上電死鎖 |
| P0 | 問題 3 | REV 改以 A 感測器結束,否則單車正常通過即誤判 |
| P0 | 問題 2 | Reset 僅在 FAULT 且故障排除後有效 |
| P1 | 問題 4 | 所有 TON.Q 轉移補上狀態限定 |
| P1 | 問題 6、7 | 超時進 FAULT;REV 也要點占用燈 |
| P2 | 問題 8、9、11 | 同時來車仲裁、獨立卡住計時、車輛計數 |
| P3 | 建議 12–15 | R_TRIG/F_TRIG、CASE 集中化、語義化命名、按鈕取邊緣 |
結語
此程式的狀態機骨架(具名狀態常數、邊緣偵測先行、故障閂鎖與獨立復歸條件)方向正確, 但目前有四項會在現場直接出事的缺陷:上電死鎖(INIT 無出口)、REV 過早結束導致單車誤判、 Reset 無條件覆寫狀態、以及疑似邊緣偵測標籤斷裂。建議先以第 3.3 節的 P0 項目止血, 再依 P1–P3 逐步把狀態機集中化,讓 STATE 只有單一寫入點、輸出成為狀態的純函數, 即可大幅降低此類掃描順序相依的隱性錯誤。