3.24. 以程式碼操作 I2C

machine.I2C 封裝了一個硬體 I2C 控制器。以匯流排 id 與一個選擇性的時脈頻率來建構:

from machine import I2C

i2c = I2C(2, freq=400_000)

id 選擇要使用哪個硬體 I2C 區塊;可用的編號以及它們對應的 SDA / SCL 接腳因板而異(參見 OpenMV 開發板)。freq 是 SCL 時脈速率——100_000 為標準模式,400_000 為多數感測器都支援的常見「快速」速度。

3.24.1. 掃描匯流排

scan() 會遍歷 7 位元位址空間,並回傳每一個對自身位址做出回應的裝置清單:

>>> i2c.scan()
[0x40, 0x68]

這是接好一條新匯流排後第一件該嘗試的事。若裝置出現在清單中,代表接線與上拉電阻都沒問題。若清單是空的(或比預期短),在懷疑程式碼之前,先懷疑上拉電阻、接線或裝置的位址接腳。

3.24.2. 記憶體映射模式

大多數 I2C 感測器把自身狀態以一組內部暫存器的形式呈現:狀態旗標在某個位址、量測位元組在另一個位址、組態在第三個位址。這個函式庫提供兩個便利方法,可直接對應到「對裝置定址、指向某個暫存器、然後讀取或寫入資料」這個模式。

讀取一個暫存器:

addr = 0x68
reg  = 0x3B
data = i2c.readfrom_mem(addr, reg, 6)
# `data` is 6 bytes starting at register 0x3B

這單一呼叫會發出一個 START、送出帶寫入位元的裝置位址、送出暫存器位址(一個重複起始)、再以讀取位元重送位址、讀取 6 個位元組、對最後一個回應 NACK,然後 STOP。裝置看到的是標準的「從暫存器讀取」序列;指令碼看到的則是一次 Python 呼叫。

寫入一個暫存器:

i2c.writeto_mem(addr, 0x6B, b"\x00")

這個序列是上述的鏡像:START、位址 + 寫入、暫存器、酬載位元組、STOP。

對於暫存器位址寬達兩個位元組的裝置(某些 EEPROM 與 16 位元定址的記憶體),請傳入 addrsize=16

data = i2c.readfrom_mem(addr, 0x0100, 32, addrsize=16)

3.24.3. 讀取感測器

一段在典型 MEMS 感測器上讀取 WHO_AM_I 識別碼與加速度計 X 軸的簡短迴圈:

import time
import struct
from machine import I2C

i2c         = I2C(2, freq=400_000)
ADDR        = 0x68
WHO_AM_I    = 0x75
ACCEL_X_HI  = 0x3B

print("WHO_AM_I:", i2c.readfrom_mem(ADDR, WHO_AM_I, 1)[0])

while True:
    raw = i2c.readfrom_mem(ADDR, ACCEL_X_HI, 2)
    x   = struct.unpack(">h", raw)[0]
    print("accel X:", x)
    time.sleep_ms(100)

readfrom_mem() 會回傳一個 bytes 物件,因此 WHO_AM_I 的讀取以 [0] 取出第一個(也是唯一的)位元組。加速度計的讀取則是兩個原始位元組,必須組合成一個帶號的 16 位元值;struct 的格式 ">h" 會以大端帶號的方式完成此事——這是多數 I2C 感測器所用的位元組順序,與 UART 連結上常見的小端慣例相反。struct.unpack() 永遠回傳一個元組(每個格式字元對應一個元素),因此 [0] 取出那唯一的值。

3.24.4. 較低階的存取

對於不遵循暫存器映射模式的裝置,有兩個較低階的方法可對位址階段與資料方向提供直接控制:

i2c.writeto(addr, b"\x01\x02\x03")    # write 3 bytes to addr
data = i2c.readfrom(addr, 4)          # read 4 bytes from addr

這些方法適用於具有命令串流介面的裝置(顯示器、某些即時時鐘),這類裝置的暫存器映射便利輔助方法並不太合用。

3.24.5. 在沒有硬體區塊可用時進行位元觸發(bit-banging)

machine.SoftI2C 在任意 GPIO 接腳上提供相同的 API,以軟體位元觸發的方式實作此協定:

from machine import SoftI2C, Pin

i2c = SoftI2C(scl=Pin("P5"), sda=Pin("P4"), freq=100_000)

當裝置需要接在沒有接到硬體 I2C 區塊的接腳上,或當每個硬體區塊都已被佔用時,便可使用它。位元觸發迴圈比硬體 I2C 慢,且整個交易期間 CPU 都會保持忙碌,但其 API 與 I2C 完全相同,因此既有程式碼只需更改建構子即可切換過去。