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,用软件位操作(bit-banging)实现该协议:
from machine import SoftI2C, Pin
i2c = SoftI2C(scl=Pin("P5"), sda=Pin("P4"), freq=100_000)
当设备需要接在没有连到硬件 I2C 块的引脚上,或者所有硬件块都已被占用时,就使用它。位操作循环比硬件 I2C 慢,并且 CPU 在整个事务期间都保持忙碌,但其 API 与 I2C 完全相同,因此现有代码只需更改构造函数即可切换过来。