iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

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

Day 04 - 最古老也最可靠:使用 UART 打造邊緣設備的 Daisy Chain 通訊

  • 分享至 

  • xImage
  •  

在前面的文章中,我們已經確立了「Linux 閘道器 (Gateway) + RTOS 感測器 (MCU Node)」的異質雙核架構。現在面臨的第一個實務問題是:這兩台設備之間,該怎麼說話?

在物聯網與邊緣運算的領域中,我們有太多酷炫的選擇:BLE (藍牙低功耗)、Wi-Fi、MQTT、甚至是 USB。但在醫療照護或工業高穩定度的場域裡,越「古老」的技術,往往越可靠。

今天,我們來聊聊為什麼我們的 EdgeNode 選擇了最傳統的 UART (Universal Asynchronous Receiver-Transmitter),以及如何用 Python 寫出堅若磐石的通訊解析器。

  1. 為什麼是 UART?
    一開始,團隊曾考慮過直接走 USB 或是 BLE 通訊。但這兩者在長期的 24/7 運作下都有潛在的痛點:

BLE (藍牙):雖然無線很方便,但在多台設備同時運作的場域 (例如病房),頻段干擾與偶發性的斷線重連機制,會讓軟體架構變得極度複雜。
USB:USB 協定非常嚴謹且快速,但它高度依賴 Host 端的作業系統 (Linux) 來分配資源。只要拔插瞬間發生電氣雜訊,或是 Linux 核心的 USB 驅動稍有打嗝,整個掛載樹 (Device Tree) 就可能崩潰。
I2C / SPI:這兩者通常用於同一塊 PCBA 上的晶片級通訊。若要拉一條 1~2 公尺長的實體線路,訊號衰減與雜訊會讓資料慘不忍睹。
最後勝出的是 UART。它只需要三根線 (TX, RX, GND),完全非同步 (Asynchronous),且硬體成本極低。即使線路瞬間干擾掉了一個 Byte,軟體層的 Checksum 也能輕易攔截,下一秒立刻重新同步,系統容錯率極高。

  1. 邊緣設備的 Daisy Chain (菊鏈) 架構
    在我們的設計中,EdgeNode (Raspberry Pi) 是作為主控端 (Host),而底下的 MCU Node 是被動接收端 (Client)。我們採用類似 Daisy Chain 的一對一專線架構。

💡 什麼是 Daisy Chain 通訊? 在這裡指的是資料流的線性傳遞:感測器 (光學/陀螺儀) -> MCU (RTOS 演算) -> UART 實體線 -> Linux Gateway (Python 收集整理) -> Cloud (最終終點)。每一層都只對上一層負責,大幅降低了非同步通訊的耦合度。

  1. 實作痛點:如何對付「沒有邊界」的連續位元流?
    UART 是純粹的「位元流 (Byte Stream)」,它不會告訴你「這是一個封包的開頭」或「這是一個封包的結尾」。因此,通訊協定的設計 (Protocol Design) 成了系統穩定的關鍵。

[錯誤嘗試]:依賴固定長度讀取
早期開發時,我們可能會寫出這樣的程式碼:

python
import serial
ser = serial.Serial('/dev/ttyS0', 115200)
while True:
# 假設我們知道每筆資料都是 18 Bytes
packet = ser.read(18)  
process_data(packet)

災難場景:只要硬體因為任何原因漏送了 1 個 Byte,ser.read(18) 就會卡住,或者讀到「上一筆的結尾 + 下一筆的開頭」,從此資料全面錯位 (Shift),再也解不回來。

[底層原理]:狀態機解析 (State Machine Parser)
為了克服這個問題,我們必須在 MCU 與 Linux 之間約定好強健的封包結構,通常包含: [Start Byte] + [Command] + [Payload] + [Checksum]

並且在 Python 端實作一個「狀態機 (State Machine)」,逐 Byte 檢查:

python
class ProtocolParser:
    def __init__(self):
        self.buffer = bytearray()
        
    def process_stream(self, data: bytes):
        """處理源源不絕的 UART 串流"""
        self.buffer.extend(data)
        packets = []
        
        while len(self.buffer) > 0:
            start_byte = self.buffer[0]
            
            # 1. 尋找合法的標頭 (例如 0x04)
            if start_byte not in [0x02, 0x03, 0x04, 0xF0]:
                self.buffer.pop(0) # 丟棄雜訊
                continue
                
            # 2. 決定動態長度
            frame_size = self._get_frame_size(start_byte)
            if len(self.buffer) < frame_size:
                break # 封包還沒收完,等待下一次 Serial Read
                
            # 3. 取出完整封包並驗證 Checksum
            frame = self.buffer[:frame_size]
            if self._verify_checksum(frame):
                packets.append(self._decode(frame))
                
            # 4. 推進 Buffer
            self.buffer = self.buffer[frame_size:]
            
        return packets
  1. 終極解決方案:分離「通訊層」與「應用層」
    寫到這邊,你也許會發現,如果讓負責存檔、運算的 Python 主執行緒去等待 UART,那整個系統會變得很卡。

因此,在 DataGateway 專案中,我們實作了 SerialWorker 繼承自 threading.Thread。這個背景執行緒唯一的工作,就是死命地讀取 UART /dev/ttyS0,將收到的 Bytes 丟給 ProtocolParser。 一旦解碼出完整的 VitalSignPacket (生命徵象封包),才透過 Queue (或 Thread-safe Signal) 拋給主系統。

這種**「讓髒活留在背景,讓純淨資料進入核心」**的設計,正是我們能讓邊緣設備 24 小時不間斷運作的第一個秘密。

明日預告
現在我們已經打通了任督二脈,能透過 UART 拿到資料了。但是,我們的 DataGateway 是一個沒有螢幕、沒有鍵盤的「無頭設備 (Headless)」。當系統發生錯誤,或是 UART 斷線時,硬體該如何告訴我們? 明天,我們將來探討:沒有螢幕,硬體該如何說話?(LED 狀態機的軟即時控制)。


上一篇
[Day 03] 最古老也最可靠:使用 UART 打造邊緣設備的 Daisy Chain 通訊協定
系列文
從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言