Learning Objectives
- 說明 I2C 用兩條線接多個裝置的方式,以及位址的用途
- 啟用 I2C,看懂 i2cdetect 掃描出來的位址表
- 用 smbus2 從暫存器讀出位元組,以位元移位組成 TMP102 的溫度
- 用二補數處理負溫度,並在位址錯誤時接住 OSError
兩條線,接一整排感測器
上一課的 MCP3008 用 SPI,要四條訊號線,每多一顆晶片還要多一條片選線。 感測器一多,樹莓派的腳位很快就不夠用。
I2C(Inter-Integrated Circuit,常唸作 I-squared-C 或 I-two-C)只要兩條線,就能在同一條匯流排上接很多個裝置: 溫度感測器、顯示器、加速度計可以全部並聯在同樣的兩條線上。這一課要回答:
- 大家都接在同樣的兩條線上,樹莓派怎麼知道在跟誰說話?
- 感測器回傳的是一串位元組,怎麼變成「25.125°C」?
- 位址寫錯、感測器沒接好時,程式會遇到什麼?
SDA、SCL 與位址
I2C 的兩條線:
| 線 | 意思 | 樹莓派腳位(BCM) |
|---|---|---|
| SDA | 資料(雙向) | GPIO2(實體第 3 腳) |
| SCL | 時脈 | GPIO3(實體第 5 腳) |
再加上 3.3V 與 GND,感測器模組就接好了。I2C 的線平常要靠上拉電阻拉到高電位,樹莓派在 GPIO2、GPIO3 上已經內建 1.8 kΩ 的上拉電阻,所以這兩支腳不適合拿來當一般按鈕輸入。
匯流排上由樹莓派發起每一次通訊,稱為控制端;感測器等著被點名,稱為目標裝置。每個目標裝置有一個 7 位元的位址, 通訊一開始先送出位址,只有位址相符的裝置會回應。常見的例子:
| 裝置 | 常見位址 | 說明 |
|---|---|---|
| TMP102 溫度感測器 | 0x48 | ADD0 腳接 GND;接 3.3V、SDA、SCL 分別變成 0x49、0x4A、0x4B |
| LCD 1602 的 I2C 轉接板(PCF8574) | 0x27 | 使用 PCF8574A 晶片的版本是 0x3F |
| SSD1306 OLED 顯示器 | 0x3C | 有些模組是 0x3D |
同一條匯流排上位址不能重複;兩顆一樣的感測器要接在一起時,用 ADD0 這類腳位把其中一顆換成別的位址。
位址習慣用十六進位寫:0x48 就是十進位的 72。Python 裡寫
0x48或72都一樣。
啟用 I2C 與掃描位址
和 SPI 一樣,樹莓派的 I2C 預設關閉。執行 sudo raspi-config,選 3 Interface Options → I5 I2C,回答要啟用
(或在桌面的 Control Centre 的 Interfaces 分頁打開)。接好感測器後,用 i2cdetect 掃描匯流排 1:
$ sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- 27 -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
列是十位數、行是個位數(都是十六進位):第 20 列第 7 行出現 27,第 40 列第 8 行出現 48,表示匯流排上有 0x27 與 0x48 兩個裝置回應。 接線後先掃描,看到位址再寫程式,可以省下很多除錯時間:掃不到就是接線、電源或位址的問題,不是程式的問題。
在 Python 裡用 smbus2 套件操作 I2C:SMBus(1) 打開匯流排 1,再用位址與暫存器編號讀寫。
暫存器與 TMP102 的溫度格式
感測器內部有幾個小格子叫暫存器,每一格有編號。讀感測器通常是:「位址 0x48 的裝置,請給我暫存器 0x00 開始的 2 個位元組」。
data = bus.read_i2c_block_data(0x48, 0x00, 2) # 例如 [0x18, 0x80]
TMP102 的暫存器 0x00 是溫度,用 12 個位元表示,每一單位是 0.0625°C(1/16 度)。12 個位元分在兩個位元組裡: 第一個位元組是高的 8 位元,第二個位元組的前 4 位元是剩下的低位元。組起來的方法:
data[0] = 0x18 = 0001 1000 data[0] << 4 = 0001 1000 0000
data[1] = 0x80 = 1000 0000 data[1] >> 4 = 1000
兩者 OR = 0001 1000 1000 = 0x188 = 392
392 × 0.0625 = 24.5°C
<< 是往左移(後面補 0),>> 是往右移(丟掉最右邊的位元),| 把兩邊的 1 合在一起。
逐步看:讀 TMP102
模擬的溫度在 2.5 秒內從 24.5°C 升到 26°C。看每一輪讀到的兩個位元組,怎麼一步步變成溫度。
零下怎麼表示:二補數
TMP102 也量得到零下的溫度。12 個位元裡,最高的那一位如果是 1,代表這是負數,用二補數表示: 把整數減掉 2¹²(4096)就是真正的值。
raw = (data[0] << 4) | (data[1] >> 4)
if raw >= 2048: # 最高位元是 1
raw = raw - 4096
忘了處理的話,冷凍庫裡的 -5.25°C 會被讀成 250.75°C。下一個範例兩種算法並列。
逐步看:冷凍庫裡的溫度
感測器放在 -5.25°C 的地方。注意第 5 行的判斷,以及沒有處理負數時印出的荒謬數字。
逐步看:位元組的順序
smbus2 還有一個 read_word_data,一次讀 2 個位元組組成一個整數。可是 SMBus 規定先收到的是低位元組,
TMP102 卻是先送高位元組,所以直接用會把兩個位元組放反。比較兩種讀法的結果。
位址錯了、線鬆了:OSError
對一個沒有裝置回應的位址讀寫,smbus2 會丟出例外,Linux 上常見的訊息是:
OSError: [Errno 121] Remote I/O error
位址寫錯、SDA 與 SCL 接反、電源沒接、杜邦線鬆脫,都是同一個錯誤。長時間執行的程式(例如每分鐘記錄一次溫度) 不應該因為一次讀取失敗就整個停掉,可以用 try/except 接住,記下錯誤、等一下再試:
try:
data = bus.read_i2c_block_data(0x48, 0x00, 2)
except OSError as e:
print("讀取失敗:", e) # 記下來,下一輪再試
讀取經常失敗時,先用
i2cdetect -y 1確認位址還在,再檢查接線;不要只在程式裡一直重試。
小測驗:位址與接線
樹莓派的 I2C 匯流排 1 用哪兩支腳位?
i2cdetect 的表格在第 40 列、第 8 行出現 48,程式裡要用哪個位址?
同一條 I2C 匯流排上接兩顆 ADD0 都接地的 TMP102,會怎樣?
小測驗:位元組與溫度
下面的程式會印出什麼?
data = [0x19, 0x20]
raw = (data[0] << 4) | (data[1] >> 4)
print(raw * 0.0625)
下面的程式會印出什麼?
raw = 4032
if raw >= 2048:
raw = raw - 4096
print(raw * 0.0625)
用 smbus2 讀一個沒有接裝置的位址,常見的錯誤是什麼?
重點整理
- I2C 用 SDA(GPIO2)與 SCL(GPIO3)兩條線,同一條匯流排可以接多個裝置,靠 7 位元位址分辨;位址不能重複。
- 先用 raspi-config 啟用 I2C,接線後用
sudo i2cdetect -y 1掃描,確認位址再寫程式。 - smbus2:
SMBus(1)打開匯流排,read_i2c_block_data(位址, 暫存器, 個數)讀出位元組。 - TMP102:暫存器 0x00 是 12 位元溫度,
(data[0] << 4) | (data[1] >> 4)組成整數,乘 0.0625°C;最高位元是 1 時減 4096(二補數)。 - 位址錯誤或接線鬆脫會得到
OSError(errno 121),用 try/except 接住,讓程式不會整個停掉。
下一課把讀到的數字顯示出來:同樣接在 I2C 上的 16x2 LCD。