Skip to content

I2C Controller Method Compatibility

PxI2CController exposes a high-level I2C controller API that matches MicroPython machine.I2C and CircuitPython. If you already use writeto, readfrom, readfrom_mem, or scan on embedded firmware, you can port that code to AQPXLIB with minimal changes.

The main setup difference is that bus pins are configured on PxI2CBus, and the controller must be attached to the bus before any transaction.

See

Refer to CircuitPython/MicroPython documentation for the original API definitions.

Setup

MicroPython constructs the bus and controller in one step. AQPXLIB separates bus creation from the controller object:

MicroPython AQPXLIB
I2C(freq=400000) PxI2CBus(px, scl=..., sda=...)
(same object) PxI2CController(px) + attach_to_bus(i2c_bus)
from aqpxlib import AqProtocolExerciser
from aqpxlib.i2c import PxI2CBus, PxI2CController

with AqProtocolExerciser.connect(port=60600) as px:
    i2c_bus = PxI2CBus(px, scl=0, sda=1)
    i2c = PxI2CController(px)
    i2c.attach_to_bus(i2c_bus)

    addrs = i2c.scan()
    print(f"Found devices at: {[hex(a) for a in addrs]}")

    i2c.detach_from_bus()

Method compatibility

MicroPython / CircuitPython PxI2CController Notes
scan() scan() Scans 0x08-0x77, returns sorted 7-bit addresses
writeto(addr, buf, stop=True) writeto() Optional start/end buffer slice (AQPX extension); stop accepted but ignored
readfrom(addr, nbytes, stop=True) readfrom() Returns bytes; raises on NACK
readfrom_into(addr, buf, stop=True) readfrom_into() Reads into an existing buffer
writevto(addr, vector, stop=True) writevto() Concatenates vector then writes
writeto_then_readfrom(...) writeto_then_readfrom() Combined write-then-read (repeated START)
readfrom_mem(addr, memaddr, nbytes, *, addrsize=8) readfrom_mem() addrsize 8, 16, 24, or 32
readfrom_mem_into(...) readfrom_mem_into() Reads len(buf) bytes
writeto_mem(addr, memaddr, buf, *, addrsize=8) writeto_mem() Returns None
init() / deinit() — Use attach_to_bus() / detach_from_bus() instead
start() / stop() / write() / readinto() — SoftI2C primitives not exposed

Examples

The examples below mirror the MicroPython I2C documentation. Each block shows the MicroPython call as a comment for easy porting.

Bus scan

# MicroPython:  i2c.scan()
addrs = i2c.scan()  # e.g. [8, 42, 119]

Simple write and read

# MicroPython:  i2c.writeto(42, b'123')
i2c.writeto(0x42, b"123")

# MicroPython:  i2c.readfrom(42, 4)
data = i2c.readfrom(0x42, 4)  # returns bytes

Read into a buffer

buf = bytearray(4)

# MicroPython:  i2c.readfrom_into(42, buf)
i2c.readfrom_into(0x42, buf)

Scatter write

# MicroPython:  i2c.writevto(42, [b'\x01', b'\x02\x03'])
i2c.writevto(0x42, [b"\x01", b"\x02\x03"])

Memory / register access

For devices that expose a register or memory map (EEPROM, sensor registers, etc.):

# MicroPython:  i2c.readfrom_mem(42, 8, 3)
reg_data = i2c.readfrom_mem(0x42, 8, 3)

# MicroPython:  i2c.writeto_mem(42, 2, b'\x10')
i2c.writeto_mem(0x42, 2, b"\x10")

# 16-bit register address
data = i2c.readfrom_mem(0x42, 0x0100, 4, addrsize=16)

# 24-bit or 32-bit register address (large EEPROMs, flash, etc.)
data = i2c.readfrom_mem(0x50, 0x123456, 4, addrsize=24)
data = i2c.readfrom_mem(0x50, 0x12345678, 4, addrsize=32)

Write-then-read (register pointer)

Use this when the device expects a register address write followed by a data read without a STOP between them:

write_buf = b"\x00"       # register pointer
read_buf = bytearray(4)

# MicroPython:  i2c.writeto_then_readfrom(42, write_buf, read_buf)
i2c.writeto_then_readfrom(0x42, write_buf, read_buf)

Buffer slicing

AQPXLIB extends writeto, readfrom_into, and writeto_then_readfrom with optional start/end slice parameters. This is useful when reusing a large buffer:

buf = bytearray(8)
i2c.writeto(0x42, buf, start=2, end=6)          # write buf[2:6]
i2c.readfrom_into(0x42, buf, start=1, end=5)    # read into buf[1:5]

Behavioral differences

NACK handling

MicroPython-compat methods raise OSError(errno.EIO, "I2C NACK") when the target does not acknowledge the transaction:

import errno

try:
    i2c.readfrom(0x7F, 1)
except OSError as e:
    assert e.errno == errno.EIO

For writeto and writeto_mem, if some bytes were ACKed before a NACK, the partial byte count is returned (matching MicroPython). A NACK on the address phase raises instead.

The native helpers i2c_write(), i2c_read(), i2c_write_addr(), and i2c_read_addr() use softer error handling: they return None or an empty list on NACK rather than raising.

stop parameter

The stop keyword argument is accepted on all compat methods for API compatibility, but is currently ignored. Each method call maps to a complete START…STOP transaction sequence on the bus.

Return types

API style Write return Read return
MicroPython-compat (writeto, readfrom, …) int (bytes ACKed) or None bytes
Native (i2c_write, i2c_read, …) int or None list[int]

Bus timing

MicroPython sets freq and timeout on the I2C constructor. In AQPXLIB, electrical and timing settings are configured on PxI2CBus when the bus is created. There is no per-controller freq/timeout parameter.

AQPX-specific extensions

These methods are not part of the MicroPython API but are available on PxI2CController:

Method Description
i2c_scan_all() Scan all 128 addresses; returns a list of booleans indexed by address
send_transactions() Send a custom sequence of read/write transactions

When to use which API

  • Porting MicroPython or CircuitPython scripts — use scan, writeto, readfrom, readfrom_mem, and related compat methods. Error handling and return types will match what you expect from embedded code.
  • AQPX-native code — use i2c_write, i2c_read, i2c_write_addr, and i2c_read_addr when you prefer soft NACK handling (None / []) or list[int] return values.
  • I3C controllers doing I2C-format transfers — PxI3CBaseController inherits the same compat methods for I2C-format bus operations.