EP.03嵌入式與系統

UART & I2C
嵌入式硬體溝通基礎原理

不需要深入硬體,只需理解軟體工程師該懂的通訊概念 —
Python pyserial / smbus 實作

Joseph Chen 2026 10 min read UART · I2C · Python · Raspberry Pi

「在 SCT 工作時,第一次用 Python 控制硬體設備,才發現程式碼和硬體之間需要一種「共同語言」——通訊協定。不需要懂電路,只要知道資料是如何被打包和傳送的,就能寫出穩定的通訊程式。」

UART 和 I2C 是嵌入式系統最基礎的兩種通訊協定。這篇從軟體工程師的視角切入, 不講電氣規格,而是聚焦在「如何用 Python 與硬體溝通」、「兩種協定的使用場景差異」、 以及「實際工作中最常遇到的排查問題」。

1. 為什麼軟體工程師要懂通訊協定

隨著 Raspberry Pi 和 Arduino 普及,軟體工程師越來越常需要寫程式控制硬體。 了解通訊協定的基礎概念,讓你能夠快速排查問題、選對工具。

Raspberry Pi 控制感測器

溫濕度、距離、加速度感測器大多透過 UART 或 I2C 介面連接, Python 幾行程式就能讀取數據。

測試儀器 UART Console

嵌入式設備的 debug console 幾乎都走 UART, 透過 pyserial 可以自動化擷取設備 log 與發送指令。

I2C 多感測器整合

一條 I2C bus 可同時連接多個感測器,用地址區分, 特別適合 IoT 設備中需要整合多種感測器的場景。

2. UART — 點對點非同步通訊

一句話理解 UART

「兩條線,Tx 發、Rx 收,點對點非同步通訊。雙方不需要共用時鐘,但必須事先約定好相同的 Baud rate。」

參數常用值說明
Baud rate9600 / 115200 bps每秒傳輸的符號數,雙方必須一致,否則資料亂碼
Data bits8 bits(幾乎固定)每次傳輸的資料位元數
Stop bits1 bit標記每個資料框的結束,常見值為 1 或 2
ParityNone(最常見)奇偶校驗,用於基礎錯誤檢測,多數場景設 None
Flow controlNone / RTS-CTS流量控制,簡單場景不需要

連接方式:交叉接線(Cross-connection)

Device A[Tx] ──────────────→ [Rx]Device B
Device A[Rx] ←────────────── [Tx]Device B
Device A[GND] ─────────────── [GND]Device B

⚠ Tx 接 Rx,不能 Tx 接 Tx(常見接錯)

Python 的 pyserial 套件封裝了 UART 操作, 安裝後幾行程式碼就能與設備通訊:

pyserial 基本使用
import serial
import time

# 開啟串口
# Windows: 'COM3'、Linux/Mac: '/dev/ttyUSB0' 或 '/dev/ttyAMA0'
ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=115200,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    timeout=1          # 讀取逾時(秒),None = 永久等待
)

# 傳送資料(bytes,記得加 \n 結尾)
ser.write(b'HELLO\n')

# 讀取一行回應
response = ser.readline().decode('utf-8').strip()
print(f'Device replied: {response}')

# 讀取固定長度的資料
raw_bytes = ser.read(10)

# 清空緩衝區
ser.flushInput()

# 完成後關閉
ser.close()

UART 最常見的問題

Baud rate 不一致:資料亂碼(出現奇怪字元)的第一個排查點,確認雙方設定完全相同。

Tx 接 Tx(接錯線):完全收不到資料,記住要交叉接——A 的 Tx 接 B 的 Rx。

權限問題(Linux)Permission denied: '/dev/ttyUSB0',執行 sudo usermod -aG dialout $USER 後重新登入。

找不到設備:執行 ls /dev/tty*,USB 轉 UART 通常是 /dev/ttyUSB0,板載是 /dev/ttyAMA0

3. I2C — 一主多從匯流排

一句話理解 I2C

「兩條線(SDA 資料 + SCL 時鐘),一個主設備(Master)可以連接最多 127 個從設備(Slave),用 7-bit 地址區分每個設備。」

I2C 匯流排拓撲

Master (Raspberry Pi)
SDA ────────┬────────────┬────────────┐
SCL ────────┼────────────┼────────────┤
Slave
0x3C
OLED
Slave
0x48
ADC
Slave
0x68
MPU6050
I2C 地址設備用途
0x3C / 0x3DSSD1306 OLED 顯示器小型顯示模組,0x3C 最常見
0x48ADS1115 ADC類比轉數位,讀取類比感測器
0x68 / 0x69MPU-6050 / MPU-6500六軸陀螺儀 + 加速度計
0x76 / 0x77BME280溫度 + 濕度 + 氣壓三合一感測器
0x27 / 0x3FPCF8574 LCD 背包I2C 轉 LCD 1602 介面

Python 的 smbus2 套件封裝了 I2C 操作, 透過地址與暫存器編號讀寫設備:

smbus2 基本使用
import smbus2

# 開啟 I2C bus(Raspberry Pi 通常是 bus 1)
# /dev/i2c-1 對應 bus=1
bus = smbus2.SMBus(1)

address = 0x48   # ADS1115 的 I2C 地址

# 讀取單一 byte(從指定暫存器)
data = bus.read_byte_data(address, 0x00)
print(f'Register 0x00: {data:#04x}')

# 讀取 2 bytes(word,大多數感測器用 16-bit 資料)
word = bus.read_word_data(address, 0x00)

# 讀取多個 bytes(block read)
block = bus.read_i2c_block_data(address, 0x00, 6)  # 讀 6 bytes

# 寫入 byte 到暫存器
bus.write_byte_data(address, 0x01, 0xFF)

# 寫入 word(2 bytes)
bus.write_word_data(address, 0x01, 0x8385)

# 完成後關閉
bus.close()

掃描所有 I2C 設備:i2cdetect

在 Raspberry Pi 上可以用指令掃描 bus 上所有設備的地址:

$ i2cdetect -y 1

輸出會顯示一個地址表,有設備的格子會顯示 hex 地址(如 3c),空的格子顯示 --

如果沒有安裝:sudo apt install i2c-tools

4. UART vs I2C 比較

兩種協定都是兩條線,但設計目的完全不同:UART 是簡單的點對點串列通訊,I2C 是可以連接多個設備的匯流排通訊。

面向UARTI2C
線數2 條(Tx / Rx)2 條(SDA / SCL)
通訊模式非同步(Asynchronous)同步(Synchronous,有時鐘線)
設備數量點對點(1 對 1)1 Master 最多 127 個 Slave
速度最高 ~1 Mbps(常用 115200 bps)100 Kbps(Standard)/ 400 Kbps(Fast)/ 1 Mbps(Fast+)
定址不需要(直接通訊)7-bit 地址區分設備
適用場景GPS 模組、藍牙模組、系統 debug console溫濕度感測器、EEPROM、顯示器、IMU
Python 套件pyserialsmbus2

5. 軟體工程師最常遇到的問題

以下是實際工作中最常遇到的坑,每一個都可能讓你卡一個小時,但解法其實很簡單。

找不到設備 / 無法開啟串口

UART:執行 ls /dev/tty* 確認設備檔案是否存在。USB 轉 UART 晶片通常是 /dev/ttyUSB0,Raspberry Pi 板載 UART 是 /dev/ttyAMA0(或 /dev/serial0)。

I2C:執行 i2cdetect -y 1 掃描 bus,確認設備地址顯示在輸出表格中。如果什麼都沒有,先檢查接線是否正確(特別是 SDA / SCL 有沒有接反)。

• 也可以用 dmesg | grep ttydmesg | grep i2c 查看 kernel 識別設備的日誌。

Linux 權限問題

UART 權限Permission denied: '/dev/ttyUSB0',需要把用戶加入 dialout 群組:

$ sudo usermod -aG dialout $USER

執行後需要登出再重新登入才生效。如果急著測試,可以用 sudo 暫時執行。

I2C 權限:同樣的方式,加入 i2c 群組:sudo usermod -aG i2c $USER

資料亂碼(UART 特有)

• 症狀:readline() 回傳奇怪的字元或亂碼。

• 第一步:確認雙方的 Baud rate 設定完全相同。115200 和 9600 都是常見值,兩邊任何一邊設錯都會亂碼。

• 第二步:確認 Data bits(8)、Stop bits(1)、Parity(None)設定一致。

• 第三步:確認接線是交叉接(A 的 Tx 接 B 的 Rx),不是平行接。

6. 面試常考題 Q&A

嵌入式或系統相關職位常問的通訊協定問題,附上軟體工程師視角的完整答案。

Q1

UART 和 I2C 的主要差異?

UART 是點對點的非同步通訊,不需要時鐘線,雙方約定相同的 Baud rate 後各自用內部時鐘計時,適合一對一的簡單通訊(GPS、藍牙模組)。I2C 是同步的多從設備匯流排,有一條共用時鐘線(SCL)讓所有設備同步,用 7-bit 地址區分設備,一條 bus 最多可連 127 個從設備,適合連接多種感測器(溫濕度、IMU、OLED)的場景。

Q2

I2C 如何區分連接在同一條 bus 上的多個設備?

每個 I2C 設備出廠時都有一個固定的 7-bit 地址(0x00–0x7F),Master 發起通訊時會先傳送目標 Slave 的地址,只有地址匹配的 Slave 才會回應,其他設備忽略。部分設備(如 BME280 可以是 0x76 或 0x77)提供地址選擇引腳,讓同型號的兩個設備可以共存在同一條 bus 上。

Q3

如何用 Python 讀取 UART 設備的資料?

用 pyserial 套件:先 import serial,然後用 serial.Serial() 建立連線,傳入 port(如 "/dev/ttyUSB0")、baudrate(如 115200)、timeout 等參數;用 ser.write(b"command\n") 發送指令;用 ser.readline() 讀取一行回應並 decode("utf-8") 轉成字串;完成後呼叫 ser.close()。要注意發送的資料必須是 bytes(用 b"" 前綴),讀回來的也是 bytes,需要 decode 才能當字串處理。

UART
I2C
Python
pyserial
smbus
嵌入式
Raspberry Pi
EP.03