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.