先分清楚欄位一致與數值正確
快速任務發布一組狀態,慢速任務負責讀取。如果發布者先改序號,再改數值與品質,讀取者可能看到新序號配舊數值。這不是CRC算錯,而是多欄位更新被任務切換拆開。本文用合成資料組示範如何拒絕混合快照,並說明seqlock、雙緩衝和平台原子性各自能保證什麼。
| 欄位 | 意義 | 一次發布內容 |
|---|---|---|
| Seq | 資料世代 | 完整發布遞增 |
| Value | 工程值 | 與Seq同世代 |
| Quality | 品質狀態 | GOOD或BAD |
| Owner | 唯一寫入者 | 快速任務 |
| Reader | 使用者 | 慢速任務只讀 |
一個資料組只能有一個發布者寫入,其他任務只能讀取或送出請求。若兩個任務都能改Value,版本前後檢查只能發現衝突,不能決定哪個版本勝出。一致性快照也不等於安全互鎖,控制命令還要另外檢查權限、模式和有效時間。
錯開寫入就會讀到新舊混合
假設舊資料是Seq=10、Value=200、Quality=GOOD。發布第11世代時,程式依序寫Seq、Value、Quality。讀取任務若在三次寫入中間被排程,就會取得不能代表任何完整世代的組合。Quality即使仍是GOOD,也不能掩蓋Value尚未更新。
| 時刻 | 發布動作 | 讀取觀察 | 判定 |
|---|---|---|---|
| t0 | 10/200/GOOD | 10/200/GOOD | 接受 |
| t1 | 先寫Seq=11 | 11/200/GOOD | 拒絕 |
| t2 | 再寫Value=250 | 11/250/GOOD | 拒絕 |
| t3 | 完成Quality | 11/250/GOOD | 可驗證 |
| t4 | 下一次先改Seq=12 | 12/250/GOOD | 拒絕 |
上表的拒絕是設計上應有的結果,原本只先寫世代的錯誤程式並沒有足夠資訊辨認更新是否完成,不能真的靠這三欄直接判定。t1與t2的資料都可能通過範圍檢查,卻不屬於同一個已發布世代。CRC總和只能作為內容錯誤的輔助訊號,不能取代提交協定;CRC可能碰撞,也可能在計算或讀取期間被分段更新。
Seqlock的完整條件
本節說明序列計數器概念,常與seqlock一起討論,並非任何PLC都可直接用普通變數自行實作的保證。序號奇偶表示更新狀態。單一寫入者開始更新時,先把Seq改為奇數;完成所有資料欄位後,再把Seq改為下一個偶數。讀取者先讀v1,若v1為奇數就拒絕;複製Value與Quality後再讀v2,只有v1等於v2且為偶數才接受。奇數標記必須在資料寫入前,偶數發布必須在資料寫入後。
| 階段 | Seq | 資料狀態 | 讀取行為 |
|---|---|---|---|
| 穩定舊版 | 20偶數 | 完整 | 可嘗試 |
| 開始更新 | 21奇數 | 可能混合 | 拒絕 |
| 寫入中 | 21奇數 | 不可用 | 不採用 |
| 發布完成 | 22偶數 | 完整新版 | 前後相等才接受 |
| 回繞風險 | 接近0 | 世代混淆 | 讀取期間不可繞一整圈 |
三個前提不能省略。第一,Seq的單次讀寫必須在目標平台具備足夠原子性;多位元值若可被拆讀,前後比較本身不可靠。第二,資料寫入與Seq發布要有可證明的記憶體可見性順序,不能假設編譯器或快取一定依原始碼排列。第三,必須只有一個寫入者,否則兩次更新可能交錯,讀者看到相等偶數仍未必代表同一交易。
讀取重試也要有上限
教學偽碼(不可直接編譯):v1:=ReadSeq;若v1為奇數則RETRY;Copy(Value,Quality);v2:=ReadSeq;若v1=v2且v2為偶數則ACCEPT,否則RETRY。本篇每次讀取任務只嘗試一次,失敗退出並留待下次呼叫,連續失敗達設定上限才回報不可用。尤其不能讓高優先讀者搶占低優先寫者後原地等待奇數變偶數,因為寫者無法恢復執行,可能形成活鎖。達到上限後回報SNAPSHOT_UNAVAILABLE;能否保留上一筆要由需求決定,不能默默混用。
| 測試 | 插入事件 | 預期 |
|---|---|---|
| R1 | 讀取前無更新 | 接受同一偶數世代 |
| R2 | v1後開始更新 | v2不同或奇數,拒絕 |
| R3 | 複製期間更新兩次 | v1=20 v2=24 拒絕 |
| R4 | 超過重試上限 | SNAPSHOT_UNAVAILABLE |
| R5 | Seq接近回繞 | 同一啟動世代且不可完整回繞 |
R3特別重要:若只切換A/B緩衝索引,讀者先讀到A,發布者切到B,再切回A並改了第二次內容,索引可能看似未變。雙緩衝降低重疊機會,但不單獨保證兩次更新之間的交易邊界;應保存版本、索引與複製時刻。
版本相等還需排除ABA回繞。若版本寬度為w,每次完整發布加2,一次讀取期間必須少於2的w減1次方次發布,且不能重啟重設世代;否則新版本可能再次等於舊版本。查不到原子讀寫與記憶體可見性保證時,改用平台提供的同步介面,不套用此偽碼。此範例只複製純量,不讀取可能失效的指標。
雙緩衝 鎖定與可見性
雙緩衝流程是寫入非工作緩衝、完成檢查後發布索引;鎖定則在平台支援時讓讀寫區段互斥。兩者都要確認索引或鎖旗標的原子性、記憶體可見性、持有時間和任務優先級。若平台只保證基本型別單次寫入,不能推論整個STRUCT能一次複製。
| 方案 | 適合解決 | 必須查證 |
|---|---|---|
| Seqlock | 讀者多、寫者快 | 版本原子性與可見性 |
| 雙緩衝+版本 | 避免同區讀寫 | 索引發布與兩次更新 |
| 平台鎖定 | 明確互斥 | 支援、等待與搶占 |
| 整組複製API | 平台交易複製 | 大小與版本限制 |
測試可在複製Value後刻意延遲,再讓發布者完成兩次更新;也要測讀取者低優先、發布者高頻、序號回繞、寫入者重啟和Quality變BAD。這些案例驗證的是協定邊界,不是任何平台已通過實機測試。
排查 FAQ與官方來源
排查順序是先確認單一寫入者,再確認Seq奇偶與資料寫入先後,接著查任務搶占和記憶體可見性,最後才看CRC或數值範圍。監看畫面應同時顯示v1、v2、Seq奇偶、資料世代、重試次數和最後拒絕原因。本文未指定CPU或工程軟體版本。若錯誤只在高頻更新出現,先把讀取與發布事件記成時間線,不要以增加延遲或降低頻率掩蓋協定缺口。監看時保留發布者最後一次寫入的奇數與偶數序號,並記錄寫入開始、欄位完成和發布時刻;若這些時刻無法觀察,就至少保留讀取者的v1、v2和重試次數。這樣才能證明拒絕發生在更新中,而不是把所有異常都歸咎於計算。
讀取失敗也要區分奇數更新中、前後版本不同、重試耗盡和資料品質不良;四種原因的復歸方式可能不同,不能全部映射成同一個零值。上一筆資料若被保留,必須附上Stale旗標、最後成功世代和資料年齡,讓上層能決定等待、重試或顯示過期,也能避免把舊資料送入下一個控制步驟,尤其是需要新鮮值的命令。
問:CRC相同就能接受嗎?不能,CRC不是提交訊號。問:雙緩衝自動防兩次更新嗎?不會,索引、版本與可見性仍要定義。問:兩個寫入者能共用Seq嗎?不應假定,應先建立唯一所有者或平台同步協定。問:讀不到一致快照可用上一筆嗎?只有需求明確允許,且要標示資料過期。
適用限制:需依目標PLC文件確認任務搶占、基本型別原子性、結構複製、記憶體可見性與同步API。案例為合成資料。
參考:CODESYS Task configuration and scheduling
參考:CODESYS structure data type