6.24. I2C in code¶
machine.I2C wraps a hardware I2C controller. Construct
one with the bus id and an optional clock frequency:
from machine import I2C
i2c = I2C(2, freq=400_000)
id selects which hardware I2C block to use; the available
numbers and the SDA / SCL pins they map to depend on the board
(see the OpenMV Boards). freq is the SCL
clock rate – 100_000 is standard mode, 400_000 is the
common “fast” speed that most sensors support.
6.24.1. Scanning the bus¶
scan() walks the 7-bit address space and
returns a list of every device that acknowledged its address:
>>> i2c.scan()
[0x40, 0x68]
This is the first thing to try after wiring up a new bus. If a device is on the list, the wiring and pull-ups are fine. If the list is empty (or shorter than expected), suspect the pull-ups, the wiring, or the device’s address pins before suspecting code.
6.24.2. The memory-mapped pattern¶
Most I2C sensors expose their state as a set of internal registers: status flags at one address, measurement bytes at another, configuration at a third. The library has two convenience methods that map directly to the “address the device, point at a register, then read or write data” pattern.
Reading a register:
addr = 0x68
reg = 0x3B
data = i2c.readfrom_mem(addr, reg, 6)
# `data` is 6 bytes starting at register 0x3B
That single call issues a START, sends the device address with the write bit, sends the register address (a repeated start), re-sends the address with the read bit, reads 6 bytes, NACKs the last one, and STOPs. The device sees the standard “read from register” sequence; the script sees one Python call.
Writing to a register:
i2c.writeto_mem(addr, 0x6B, b"\x00")
That sequence is the same in mirror image: START, address + write, register, payload byte, STOP.
For devices whose register address is two bytes wide (some
EEPROMs and 16-bit-addressed memories) pass addrsize=16:
data = i2c.readfrom_mem(addr, 0x0100, 32, addrsize=16)
6.24.3. Reading a sensor¶
A short loop reading the WHO_AM_I identifier and the accelerometer X axis on a typical MEMS sensor:
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() returns a bytes object,
so the WHO_AM_I read indexes the first (and only) byte with
[0]. The accelerometer read is two raw bytes that have to
be assembled into one signed 16-bit value; the struct
format ">h" does that as big-endian signed – the byte
order most I2C sensors use, opposite of the little-endian
convention common on UART links.
struct.unpack() always returns a tuple (one element per
format character), so [0] pulls out the single value.
6.24.4. Lower-level access¶
For devices that do not follow the register-map pattern, two lower-level methods give direct control over the address phase and data direction:
i2c.writeto(addr, b"\x01\x02\x03") # write 3 bytes to addr
data = i2c.readfrom(addr, 4) # read 4 bytes from addr
These are useful for devices with command-stream interfaces (displays, some real-time clocks) where the register-map convenience helpers do not quite fit.
6.24.5. Bit-banging when no hardware block is available¶
machine.SoftI2C provides the same API on arbitrary
GPIO pins, bit-banging the protocol in software:
from machine import SoftI2C, Pin
i2c = SoftI2C(scl=Pin("P5"), sda=Pin("P4"), freq=100_000)
Use it when the device needs to be on pins that are not wired
to a hardware I2C block, or when every hardware block is in
use. The bit-bang loop is slower than hardware I2C and the CPU
stays busy for the whole transaction, but the API is identical
to I2C so existing code switches over by
changing only the constructor.