Learning Objectives
- 說明 I2C 為什麼兩條線就能接好幾個裝置,以及位址的作用
- 用 i2c.scan() 找出匯流排上的裝置,並用十六進位表示位址
- 用 readfrom_mem 讀取感測器暫存器,把收到的位元組拼成數值
- 用二補數的規則把讀值轉成負數,並換算成溫度、加速度等實際單位
兩條線接很多個裝置
上一課的 OLED 只用了 SCL、SDA 兩條訊號線。如果還要接溫度感測器、加速度計,需要再多幾支腳位嗎?不用。 I2C(Inter-Integrated Circuit)是一種匯流排:好幾個裝置並聯在同一組兩條線上,各自有一個位址, 開發板(控制端,controller)先喊位址,被叫到的裝置(目標裝置,target)才回應。
這一課要回答:
- I2C 怎麼接線?怎麼知道匯流排上有哪些裝置?
- 什麼是感測器的「暫存器」?怎麼讀?
- 感測器回傳的是一串位元組,怎麼變成「25.5°C」這樣的數字?
接線
| 線 | 全名 | 作用 |
|---|---|---|
| SCL | Serial Clock | 時脈:由控制端產生,決定每個位元什麼時候讀取 |
| SDA | Serial Data | 資料:雙向,控制端與目標裝置輪流使用 |
| 3.3V、GND | 電源,所有裝置共地 |
ESP32 GPIO18 (SCL) ──┬────────┬────────┬───
ESP32 GPIO19 (SDA) ──┼──┬─────┼──┬─────┼──┬
OLED TMP102 MPU-6050
0x3C 0x48 0x68
兩條線都需要上拉電阻接到 3.3V(MicroPython 文件建議約 1~10 kΩ)。常見的感測器模組板多半已經焊好上拉電阻,
如果接上去之後 scan() 什麼都找不到,上拉電阻是要檢查的項目之一。
I2C 規格(NXP UM10204)定義了幾種速度,最常見的是標準模式 100 kbit/s 與快速模式 400 kbit/s;
MicroPython 建立 I2C 時用 freq= 指定,預設是 400 kHz。
逐步看:掃描匯流排上的裝置
scan() 會對 0x08~0x77 每個位址都喊一次,把有回應的位址放進串列。
位址是 7 位元的數字,習慣用十六進位寫:0x3C 和十進位的 60 是同一個數。
暫存器:感測器裡的小表格
每個 I2C 感測器內部都有一組暫存器(register),可以想成一張編了號碼的小表格,每一格存一個位元組: 有的格子放量測結果,有的格子放設定(量測範圍、省電模式…)。哪一格是什麼,要查感測器的規格書(datasheet)。
讀暫存器的步驟是:喊裝置位址 → 告訴它要從第幾號暫存器開始 → 讀回幾個位元組。MicroPython 把這三步包成一個函式:
data = i2c.readfrom_mem(裝置位址, 暫存器編號, 位元組數) # 回傳 bytes
i2c.writeto_mem(裝置位址, 暫存器編號, bytes([值])) # 寫入設定
裝置沒有回應時(位址寫錯、線沒接好),會發生 OSError,常見的訊息是 ENODEV。
位元組怎麼變成數字
一個位元組只有 0~255,感測器的讀值常常比這個大,所以會分成兩個位元組傳回來,程式要自己拼起來。 以 TMP102 溫度感測器為例(依 TI 的規格書):溫度暫存器是 0x00,讀 2 個位元組,前 12 個位元是溫度, 每 1 單位代表 0.0625°C:
第 1 個位元組 第 2 個位元組
0001 1001 1000 0000
└──────┬──────────┘└─┬─┘
12 位元:0001 1001 1000 = 408 後 4 位元不用
408 × 0.0625 = 25.5°C
用位元運算寫:(data[0] << 4) | (data[1] >> 4)。<< 往左移(乘 2 的次方),>> 往右移,| 把兩段合起來。
逐步看:讀 TMP102 溫度暫存器
模擬中第二次讀取時溫度已經降到零下(像把感測器放進冰箱)。注意第二次收到的位元組, 以及第 12~13 行怎麼把它變成負數。
二補數:位元組裡的負數
位元組本身沒有正負號,感測器用二補數(two's complement)表示負數:最高位元是 1 就代表負數, 把讀到的值減掉 2 的「位元數」次方,就得到真正的值。
| 位元數 | 最高位元是 1 的條件 | 減掉 |
|---|---|---|
| 8 位元 | 值 ≥ 128 | 256 |
| 12 位元(TMP102) | 值 ≥ 2048(raw & 0x800) |
4096 |
| 16 位元(MPU-6050) | 值 ≥ 32768 | 65536 |
上一個範例第二次讀到 [252, 192],拼成 4044,大於 2048,所以是 4044 - 4096 = -52,乘 0.0625 是 -3.25°C。
忘了這一步的話,程式會印出 252.75°C,這是讀感測器時很常見的錯誤。
先寫入設定,再讀資料
有些感測器開機時處於睡眠狀態,要先寫暫存器叫醒它。下一個範例的 MPU-6050(三軸加速度計+陀螺儀) 位址是 0x68;依規格書,要先在電源管理暫存器 0x6B 寫入 0,再從 0x3B 開始連讀 6 個位元組, 依序是 X、Y、Z 軸各 2 個位元組(高位在前)。在 ±2g 的量程下,16384 代表 1g(一個重力加速度)。
逐步看:讀 MPU-6050 三軸加速度
模擬的感測器是傾斜放置的,所以重力分到三個軸上。看 to_int16 怎麼把兩個位元組拼成有正負號的 16 位元整數。
小測驗:匯流排與位址
i2c.scan() 回傳 [60, 104],下面哪一個說法正確?
I2C 匯流排上的每一個裝置,SDA 和 SCL 怎麼接?
下面的程式會印出什麼?
for a in [60, 72, 104]:
print(hex(a), end=" ")
小測驗:位元組換算
下面的程式會印出什麼?
data = bytes([0x01, 0x2C])
v = (data[0] << 8) | data[1]
print(v)
下面的程式會印出什麼?(16 位元的二補數)
v = (0xFF << 8) | 0x38
if v >= 32768:
v -= 65536
print(v)
MPU-6050 在 ±2g 量程下,Z 軸讀到 8192,代表多少 g?
重點整理
- I2C 用 SCL(時脈)、SDA(資料)兩條線,所有裝置並聯,兩條線都要有上拉電阻;常見速度 100 kHz、400 kHz。
- 每個裝置有 7 位元位址,
i2c.scan()找出有回應的位址;用hex()對照規格書。 - 感測器的資料與設定放在暫存器:
readfrom_mem(位址, 暫存器, 位元組數)讀、writeto_mem(位址, 暫存器, bytes)寫。 - 多個位元組用
<<、|拼成一個數;最高位元是 1 時減掉 2 的位元數次方,就是二補數表示的負數。 - 依規格書換算單位:TMP102 每單位 0.0625°C;MPU-6050 在 ±2g 時 16384 代表 1g。
下一課介紹另一種常見的序列通訊:UART,GPS、藍牙序列模組都用它。