i2ctarget – Two wire serial protocol target

In many cases, i2c is used by a controller to retrieve (or send) to a peripheral (target). It is also possible for a device to act as a target for another controller. However, a device can only be a controller or a target on an I2C bus (although many devices now support multiple I2C busses).

Note

I2CTarget takes a list of addresses, but not all devices support this feature

Example of emulating a simple device that can only handle single writes and reads:

import board
from i2ctarget import I2CTarget

import adafruit_logging as logging

logger = logging.getLogger('i2ctarget')
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())

logger.info("\n\ncode starting...")

# initialize an I2C target with a device address of 0x40
with I2CTarget(board.SCL, board.SDA, (0x40,)) as device:

    while True:
        # check if there's a pending device request
        i2c_target_request = device.request()

        if not i2c_target_request:
            # no request is pending
            continue

        # `with` invokes I2CTargetRequest's functions to handle the necessary opening and closing of a request
        with i2c_target_request:

            # the address associated with the request
            address = i2c_target_request.address

            if i2c_target_request.is_read:
                logger.info(f"read request to address '0x{address:02x}'")

                # for our emulated device, return a fixed value for the request
                buffer = bytes([0xaa])
                i2c_target_request.write(buffer)
            else:
                # transaction is a write request
                data = i2c_target_request.read(1)
                logger.info(f"write request to address 0x{address:02x}: {data}")
                # for our emulated device, writes have no effect

This example creates an I2C target device that can be accessed via another device as an I2C controller:

import busio
import board
i2c = busio.I2C(board.SCL, board.SDA)

# perform a single read
while not i2c.try_lock():
    pass
buffer = bytearray(1)
i2c.readfrom_into(0x40, buffer)
print(f"device responded with {buffer}")
i2c.unlock()

# perform a single write
while not i2c.try_lock():
    pass
buffer = bytearray(1)
buffer[0] = 0x12
i2c.writeto(0x40, buffer)
print(f"wrote {buffer} to device")
i2c.unlock()

Typically, i2c devices support writes and reads to/from multiple register indices as in this example

import board
from i2ctarget import I2CTarget

import adafruit_logging as logging

logger = logging.getLogger('i2ctarget')
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())

# emulate a target with 16 registers
regs = [0] * 16
register_index = None

logger.info("\n\ncode starting...")

# initialize an I2C target with a device address of 0x40
with I2CTarget(board.SCL, board.SDA, (0x40,)) as device:

    while True:
        # check if there's a pending device request
        i2c_target_request = device.request()

        if not i2c_target_request:
            # no request is pending
            continue

        # work with the i2c request
        with i2c_target_request:

            if not i2c_target_request.is_read:
                # a write request

                # bytearray contains the request's first byte, the register's index
                index = i2c_target_request.read(1)[0]

                # bytearray containing the request's second byte, the data
                data = i2c_target_request.read(1)

                # if the request doesn't have a second byte, this is read transaction
                if not data:

                    # since we're only emulating 16 registers, read from a larger address is an error
                    if index > 15:
                        logger.error(f"write portion of read transaction has invalid index {index}")
                        continue

                    logger.info(f"write portion of read transaction, set index to {index}'")
                    register_index = index
                    continue

                # since we're only emulating 16 registers, writing to a larger address is an error
                if index > 15:
                    logger.error(f"write request to incorrect index {index}")
                    continue

                logger.info(f"write request to index {index}: {data}")
                regs[index] = data[0]
            else:
                # our emulated device requires a read to be part of a full write-then-read transaction
                if not i2c_target_request.is_restart:
                    logger.warning(f"read request without first writing is not supported")
                    # still need to respond, but result data is not defined
                    i2c_target_request.write(bytes([0xff]))
                    register_index = None
                    continue

                # the single read transaction case is covered above, so we should always have a valid index
                assert(register_index is not None)

                # the write-then-read to an invalid address is covered above,
                #   but if this is a restarted read, index might be out of bounds so need to check
                if register_index > 16:
                    logger.error(f"restarted read yielded an unsupported index")
                    i2c_target_request.write(bytes([0xff]))
                    register_index = None
                    continue

                # retrieve the data from our register file and respond
                data = regs[register_index]
                logger.info(f"read request from index {register_index}: {data}")
                i2c_target_request.write(bytes([data]))

                # in our emulated device, a single read transaction is covered above
                #   so any subsequent restarted read gets the value at the next index
                assert(i2c_target_request.is_restart is True)
                register_index += 1

This second example creates I2C target device that can be accessed via another device as an I2C controller:

import busio
import board
i2c = busio.I2C(board.SCL, board.SDA)

# perform a write transaction
while not i2c.try_lock():
    pass
buffer = bytearray(2)
buffer[0] = 0x0b  # the register index
buffer[1] = 0xa1  # the value
i2c.writeto(0x40, buffer)
print(f"wrote {buffer} to device")
i2c.unlock()

# perform a full read transaction (write-then-read)
while not i2c.try_lock():
    pass
index_buffer = bytearray(1)
index_buffer[0] = 0x0b
read_buffer = bytearray(1)
i2c.writeto_then_readfrom(0x40, index_buffer, read_buffer)
print(f"read from device index {index_buffer}: {read_buffer}")
i2c.unlock()

Or accessed from Linux like this:

$ i2cget -y 1 0x40 0x0b
0xff
$ i2cset -y 1 0x40 0x0b 0xa1
$ i2cget -y 1 0x40 0x01
0xa1

Warning

I2CTarget makes use of clock stretching in order to slow down the host. Make sure the I2C host supports this.

Raspberry Pi 3 and below, in particular, do not support this with its I2C hw block. This can be worked around by using the i2c-gpio bit banging driver. Since the RPi firmware uses the hw i2c, it’s not possible to emulate a HAT eeprom.

Available on these boards
  • 0xCB Helios
  • 42. Keebs Frood
  • 8086 USB Interposer
  • Adafruit EdgeBadge
  • Adafruit Feather M4 CAN
  • Adafruit Feather M4 Express
  • Adafruit Feather RP2040
  • Adafruit Feather RP2040 Adalogger
  • Adafruit Feather RP2040 CAN
  • Adafruit Feather RP2040 DVI
  • Adafruit Feather RP2040 Prop-Maker
  • Adafruit Feather RP2040 RFM
  • Adafruit Feather RP2040 Scorpio
  • Adafruit Feather RP2040 ThinkInk
  • Adafruit Feather RP2040 USB Host
  • Adafruit Feather RP2350
  • Adafruit Floppsy RP2040
  • Adafruit Grand Central M4 Express
  • Adafruit Hallowing M4 Express
  • Adafruit ItsyBitsy M4 Express
  • Adafruit ItsyBitsy RP2040
  • Adafruit KB2040
  • Adafruit Macropad RP2040
  • Adafruit Matrix Portal M4
  • Adafruit Metro M4 Airlift Lite
  • Adafruit Metro M4 Express
  • Adafruit Metro RP2040
  • Adafruit Metro RP2350
  • Adafruit Monster M4SK
  • Adafruit PyGamer
  • Adafruit PyPortal
  • Adafruit PyPortal Pynt
  • Adafruit PyPortal Titano
  • Adafruit Pybadge
  • Adafruit QT Py RP2040
  • Adafruit QT2040 Trinkey
  • Adafruit Trellis M4 Express
  • AloriumTech Evo M51
  • Archi RP2040
  • Arduino Nano RP2040 Connect
  • BBQ20KBD
  • BDMICRO VINA-D51
  • BLOK
  • Bradán Lane STUDIO Explorer Badge
  • COSMO-Pico
  • CP32-M4
  • Challenger NB RP2040 WiFi
  • Challenger RP2040 LTE
  • Challenger RP2040 LoRa
  • Challenger RP2040 SD/RTC
  • Challenger RP2040 SubGHz
  • Challenger RP2040 WiFi
  • Challenger RP2040 WiFi/BLE
  • Challenger+ RP2350 BConnect
  • Challenger+ RP2350 WiFi6/BLE5
  • CircuitBrains Deluxe
  • Cytron EDU PICO W
  • Cytron IRIV IO Controller
  • Cytron MOTION 2350 Pro
  • Cytron Maker Nano RP2040
  • Cytron Maker Pi RP2040
  • Cytron Maker Uno RP2040
  • Datanoise PicoADK
  • Datanoise PicoADK V2
  • DynOSSAT-EDU-OBC
  • E-Fidget
  • ELECFREAKS PICO:ED
  • Electrolama minik
  • EncoderPad RP2040
  • Fig Pi
  • HEIA-FR Picomo V2
  • Hack Club Sprig
  • LILYGO T-DISPLAY
  • Maple Computing Elite-Pi
  • Melopero Shake RP2040
  • Mini SAM M4
  • Oak Dev Tech BREAD2040
  • Oak Dev Tech Cast-Away RP2040
  • Oak Dev Tech RPGA Feather
  • P1AM-200
  • Pajenicko PicoPad
  • Pimoroni Badger 2040
  • Pimoroni Badger 2040 W
  • Pimoroni Inky Frame 5.7
  • Pimoroni Inky Frame 7.3
  • Pimoroni Interstate 75
  • Pimoroni Keybow 2040
  • Pimoroni Motor 2040
  • Pimoroni PGA2040
  • Pimoroni PGA2350
  • Pimoroni Pico DV Base W
  • Pimoroni Pico LiPo (16MB)
  • Pimoroni Pico LiPo (4MB)
  • Pimoroni Pico Plus 2
  • Pimoroni Pico dv Base
  • Pimoroni PicoSystem
  • Pimoroni Plasma 2040
  • Pimoroni Plasma 2040W
  • Pimoroni Plasma 2350
  • Pimoroni Servo 2040
  • Pimoroni Tiny 2040 (2MB)
  • Pimoroni Tiny 2040 (8MB)
  • Pimoroni Tiny 2350
  • Pimoroni Tiny FX
  • PyCubedv04
  • PyCubedv04-MRAM
  • PyCubedv05
  • PyCubedv05-MRAM
  • PyKey 18 Numpad
  • PyKey 44 Ergo
  • PyKey 60
  • PyKey 87 TKL
  • RF.Guru RP2040
  • RP2.65-F
  • RP2040 Stamp
  • RP2350 Stamp
  • RP2350 Stamp XL
  • Raspberry Breadstick
  • Raspberry Pi Pico
  • Raspberry Pi Pico 2
  • Raspberry Pi Pico W
  • Robo HAT MM1 M4
  • SAM E54 Xplained Pro
  • SAM32v26
  • Seeeduino Wio Terminal
  • Seeeduino XIAO RP2040
  • Seeeduino XIAO RP2350
  • Silicognition LLC M4-Shim
  • Silicognition LLC RP2040-Shim
  • SparkFun MicroMod RP2040 Processor
  • SparkFun MicroMod SAMD51 Processor
  • SparkFun Pro Micro RP2040
  • SparkFun Pro Micro RP2350
  • SparkFun Thing Plus - RP2040
  • SparkFun Thing Plus - SAMD51
  • Sprite_v2b
  • TG-Boards' Datalore IP M4
  • The Open Book Feather
  • UARTLogger II
  • VCC-GND Studio YD RP2040
  • W5100S-EVB-Pico
  • W5500-EVB-Pico
  • WK-50 Trackball Keyboard
  • Waveshare RP2040-GEEK
  • Waveshare RP2040-LCD-0.96
  • Waveshare RP2040-LCD-1.28
  • Waveshare RP2040-One
  • Waveshare RP2040-PiZero
  • Waveshare RP2040-Plus (16MB)
  • Waveshare RP2040-Plus (4MB)
  • Waveshare RP2040-TOUCH-LCD-1.28
  • Waveshare RP2040-Tiny
  • Waveshare RP2040-Zero
  • WeAct Studio Pico
  • WeAct Studio Pico 16MB
  • WisdPi Ardu2040M
  • WisdPi Tiny RP2040
  • nullbits Bit-C PRO
  • splitkb.com Liatris
  • takayoshiotake Octave RP2040
  • uGame22

class i2ctarget.I2CTarget(scl: microcontroller.Pin, sda: microcontroller.Pin, addresses: Sequence[int], smbus: bool = False)

Two wire serial protocol target

I2C is a two-wire protocol for communicating between devices. This implements the target (peripheral, sensor, secondary) side.

Parameters:
  • scl (Pin) – The clock pin

  • sda (Pin) – The data pin

  • addresses (list[int]) – The I2C addresses to respond to (how many is hardware dependent).

  • smbus (bool) – Use SMBUS timings if the hardware supports it

deinit() None

Releases control of the underlying hardware so other classes can use it.

__enter__() I2CTarget

No-op used in Context Managers.

__exit__() None

Automatically deinitializes the hardware on context exit. See Lifetime and ContextManagers for more info.

request(*, timeout: float = -1) I2CTargetRequest

Wait for an I2C request.

Parameters:

timeout (float) – Timeout in seconds. Zero means wait forever, a negative value means check once

Returns:

I2CTargetRequest or None if timeout=-1 and there’s no request

Return type:

I2CTargetRequest

class i2ctarget.I2CTargetRequest(target: I2CTarget, address: int, is_read: bool, is_restart: bool)

Information about an I2C transfer request This cannot be instantiated directly, but is returned by I2CTarget.request().

Parameters:
  • target – The I2CTarget object receiving this request

  • address – I2C address

  • is_read – True if the main target is requesting data

  • is_restart – Repeated Start Condition

__enter__() I2CTargetRequest

No-op used in Context Managers.

__exit__() None

Close the request.

address: int

The I2C address of the request.

is_read: bool

The I2C main controller is reading from this target.

is_restart: bool

Is Repeated Start Condition.

read(n: int = -1, ack: bool = True) bytearray

Read data. If ack=False, the caller is responsible for calling I2CTargetRequest.ack().

Parameters:
  • n – Number of bytes to read (negative means all)

  • ack – Whether or not to send an ACK after the n’th byte

Returns:

Bytes read

write(buffer: circuitpython_typing.ReadableBuffer) int

Write the data contained in buffer.

Parameters:

buffer (ReadableBuffer) – Write out the data in this buffer

Returns:

Number of bytes written

ack(ack: bool = True) None

Acknowledge or Not Acknowledge last byte received. Use together with I2CTargetRequest.read() ack=False.

Parameters:

ack – Whether to send an ACK or NACK