iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰系列 第 18

Day 18:邊緣設備的高頻同步實戰—從 Race Condition 到二補數對齊

  • 分享至 

  • xImage
  •  

在邊緣運算與物聯網 (IoT) 開發中,資料同步往往是最具挑戰性的一環。特別是當我們的設備(DataDock)必須同時處理低頻(20Hz 生命體徵)與高頻(50Hz PPG 綠光與三軸)的混合資料流時,韌體的時序差異與通訊協定的邊界處理,往往會引發意想不到的坑。

今天,我們將深入探討在 DataDock 專案中,如何解決序列埠同步的 「競速危害 (Race Condition)」,以及如何精準處理硬體傳感器的二進位格式與開機雜訊。


1. 幽靈封包:跨頻率下載的 Race Condition

問題發生場景

在我們的架構中,手環會先將積累的歷史資料切換至下載模式。根據通訊協定,我們會分批拉取資料:

  1. 第一階段:拉取 20Hz 生命體徵資料。
  2. 第二階段:拉取 50Hz 高頻 PPG 與三軸資料。

每當一個階段的檔案傳輸完畢時,手環端會發送一個 0x04 0xFF 0x55 0xFF 0x55FinishTransferPacket 作為該階段結束的簽章。

然而,我們遇到了一個棘手的問題:PPG 檔案總是下載失敗,長度異常中斷!

追蹤與診斷

透過排查 Log 我們發現了經典的競速危害(Race Condition):
當我們在第一階段 (20Hz) 算準了預期位元組數 (Expected Bytes),在收到最後一個位元組的瞬間,我們的 SyncEngine 就「提早」宣布第一階段成功並進入第二階段 (50Hz) 下載。

但此時,手環的韌體因為硬體處理延遲,慢了半拍才把第一階段的 FinishTransferPacket 吐出來
這導致這個「遲到的結束簽章」,直接撞上了我們已經開始的第二階段 50Hz 下載任務。SyncEngine 誤以為這是 50Hz 任務結束的訊號,於是強制中斷了 PPG 下載,導致高頻資料流失。

解決方案:狀態感知的防禦機制

為了解決這個時序落差,我們在 SyncEngine 中引入了 _ignore_next_finish 旗標 (Flag)。

[!TIP]
攔截幽靈封包
當我們因為「算準 Bytes」而提早結束下載任務時,我們主動豎起防護罩(設定 _ignore_next_finish = True)。當接收到下一個 FinishTransferPacket 時,系統會主動丟棄並印出:
[SyncEngine] Ignoring leftover FinishTransferPacket from previous fast-finish.

這個機制成功隔離了前一個任務的殘留訊號,保護了 50Hz 巨量 PPG 資料 (100KB+) 不被無辜腰斬。


2. 硬體開機雜訊與「二補數 (Two's Complement)」的陷阱

解決了下載中斷的問題後,我們成功取得了 50Hz 的原始資料。但在解碼為 CSV 檢查時,卻發現了另一個靈異現象:檔案的第一筆資料,出現了 -1

9 Bytes 封包解析

50Hz 的 PPG 封包非常緊湊,每一筆採樣 (Sample) 只佔用 9 個 Bytes:
Time Diff (1) + Green PPG (2) + Acc X (2) + Acc Y (2) + Acc Z (2)

在 Python 中,我們最初使用 struct.unpack('>4h', ...) 來解碼這後面的 8 個 Bytes。
這裡的 h 代表的是 Signed 16-bit Integer (帶符號整數)

0xFFFF 與啟動雜訊

我們發現這個 -1 其實是感測器的 硬體啟動雜訊。在晶片剛通電、FIFO 暫存器尚未準備好第一筆真實採樣的最初幾毫秒,硬體會直接吐出記憶體的預設空值:0xFFFF

0xFFFF 遇到我們定義的帶符號解碼器 h 時:
根據 二補數 (Two's Complement) 原理,0xFFFF (二進位的 1111111111111111) 會被無情地轉換為整數 -1
這就是為什麼綠光 PPG 和三軸一開始都是 -1,但到了第四行就恢復成 30816 這種正常生理讀數的原因。

[!NOTE]
在處理 IoT 邊緣設備的 Raw Data 時,開頭幾筆出現極值或預設值 (0x0000 或是 0xFFFF) 是極為正常的硬體特性,通常在後端資料清洗時 Drop 掉即可。


3. 無縫接軌:全面轉換為無符號 (Unsigned) 對齊

雖然了解了原理,但對於後端分析系統來說,綠光 PPG 屬於「絕對光強度」,本質上不應該有負數
同時,為了與團隊舊有的歷史資料處理管線 (Pipeline) 完全相容,客戶要求我們將所有的欄位(包含具有方向性的三軸)全部強制設定為無符號正數。

我們將 Python 的 struct 解碼字元做了一點魔法替換:
從原本的 >4h (4 個帶符號整數)
改成了 >4H (4 個無符號整數,大寫 H 代表 Unsigned Short)

- vals = struct.unpack('>4h', bytes(chunk[1:9]))
+ vals = struct.unpack('>4H', bytes(chunk[1:9]))
  green, acc_x, acc_y, acc_z = vals[0], vals[1], vals[2], vals[3]

結果與影響

改動推送到 Raspberry Pi 產線後:

  1. 綠光 PPG:原本的 -1 雜訊,現在正確顯示為無符號的最大值 65535,完全消除了光強度變負的邏輯謬誤。
  2. 三軸資料:維持了與舊系統一致的「極大正數」輸出。後端在運算 G 力時,只要手動扣除 65536,就能還原出物理世界的真實負向加速度。

結語

在這場高頻資料保衛戰中,我們不僅見證了分散式非同步通訊中 Race Condition 的狡猾,也深刻體驗了二進位協定中位元組解碼 (Signed vs Unsigned) 對於最終資料呈現的巨大影響力。

在開發邊緣運算節點時,我們不僅是在寫程式,更是在擔任硬體與軟體世界之間的最佳翻譯官

明天,我們將繼續深入 DataDock 的系統穩定性工程,敬請期待!


上一篇
Day 17 - 奪回控制權:實作 USB 狀態監聽,動態卸載本機 FAT32 (Lazy Unmount)
下一篇
Day 19 - 軟體重插拔 (Software Re-plug):不碰硬體,用指令強制 Windows 重新掛載
系列文
從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言