Skip to content

I2C devices

On the program side, machine.I2C makes the microcontroller the master of a real electrical I2C bus: it generates the clock on SCL and exchanges data on SDA (API). Peripherals provides three slave devices to connect to this bus. Their behaviour is always described by a Python script. Another MCU can also be the slave, with machine.I2CTarget (API, examples MultiMcu.I2c and MultiMcu.I2cIrq).

Start of an I2C write

SCL and SDA lines of the I2c.Echo example: START, address byte, slave acknowledge, then first data byte. Figure labels are in French: « temps » = time, « adresse 0x42 + écriture » = address 0x42 + write.

Wiring

Two wires shared by every bus member, plus ground:

Microcontroller Each device
SCL pin chosen by the program (e.g. GP4) SCL
SDA pin chosen by the program (e.g. GP5) SDA
GND GND
from machine import Pin, I2C

i2c = I2C(0, scl=Pin(4), sda=Pin(5), freq=100000)
print(i2c.scan())                    # addresses present, e.g. [66]
i2c.writeto(0x42, b'Hello')
print(i2c.readfrom(0x42, 5))

The bus is open-drain: each member can only pull a line low or release it, and pull-up resistors bring the lines back up. At least one device on the bus must carry these resistors (usePullUp = true). I2C() does switch on the internal pull-ups of the microcontroller on SCL and SDA, as on the Pico, but 50 kΩ against the input capacitances of the devices is far too slow: a released line has no time to rise, and every transaction raises OSError(ETIMEDOUT), as on a board where the resistors were forgotten (I2c.NoPullUp example). The Grove display carries them by default, like the real module.

Any number of devices can share the two wires; each one only answers its own addresses (I2c.MultiDevice example).

Three I2C devices on the same bus (Examples.I2c.MultiDevice)

Supplied devices

Component Address(es) Default script Role
I2cGroveLcdRgb 0x3E, 0x62 grove_lcd_rgb.py Grove - LCD RGB Backlight display, 16 × 2 characters, coloured backlight. Pull-ups enabled (usePullUp = true)
I2cEchoDevice 0x42 i2c_echo.py Test component: reads back to the master what it has just written
I2cGenericDevice 0x42, to be set i2c_generic.py Template: a bank of 16 registers, starting point for a new device

Default scripts live in Resources/Scripts/Device/.

The Grove LCD RGB display

The real module carries two chips, hence two addresses for a single component; the script emulates them from their datasheets:

  • JHD1313 (0x3E, HD44780-compatible display controller): clear, home, write position, display on or off, shift. The display is off at power-up, like the real one: the driver must switch it on.
  • PCA9633 (0x62, backlight driver): red, green, blue intensities.

The icon shows both lines and takes the backlight colour while replaying the result. valueOut returns (red, green, blue, display on), intensities from 0 to 255.

The I2c.GroveLcd example drives this display with an off-the-shelf MicroPython driver, run unmodified: driver_grove_lcd_rgb.py, placed next to the program that imports it.

Connectors

On the program side, the bus is written with machine.I2C (controller) or machine.I2CTarget (target): see the API and the official MicroPython documentation (machine.I2C, machine.I2CTarget, rp2 port).

Connector Role
SDA Bus data
SCL Bus clock
GND Ground, to connect to the microcontroller's, only when useGroundPin is checked (default)
VCC Supply of the peripheral, only when useSupplyPin is checked (bottom left of the icon): for instance 3V3(OUT) of a Raspberry Pi Pico
valueIn[nIn] Model quantities passed to the script (v argument)
valueOut[nOut] Quantities returned by the script (outputs()): the device becomes an actuator. May stay unconnected

Parameters

Bus and behaviour (General tab)

Parameter Default Group Role
addresses depends on the component I2C bus 7-bit address(es), as text: "0x42" or "0x3E, 0x62" (4 at most)
usePullUp false (true for the Grove) I2C bus Carry the SDA and SCL pull-up resistors to the supply (VOH, or VCC with useSupplyPin). Several devices may carry them: they end up in parallel
RPullUp 4.7 kΩ I2C bus Value of each pull-up resistor
scriptPath the component's script Behaviour .py file describing the device

Inputs / outputs (Inputs / outputs tab)

Parameter Default Role
useValueInput false Take the quantities from the valueIn connector; otherwise, fixedValue
nIn 1 Number of quantities received from the model (4 at most)
fixedValue 0 Value used when valueIn is not used
nOut 1 (3 for the echo, 4 for the Grove) Number of quantities returned by outputs() (4 at most)

Electrical (Electrical tab)

Parameter Default Role
useGroundPin true Shows the GND pin. Unchecked: the pin disappears and the peripheral is referenced to the simulation ground (0 V, common to every Ground block), nothing to wire
useSupplyPin false Shows the VCC pin: the peripheral is then supplied by the circuit, its high levels follow VCC and it draws its quiescent current IQ from it. Unchecked: ideal internal supply VOH, nothing to wire
VOH 3.3 V Ideal internal supply (without VCC): high level of the outputs and of the pull-ups
IQ 1 mA Quiescent current drawn from VCC (with useSupplyPin)
VIH, VIL 2.0 V, 0.8 V Reading thresholds of SDA and SCL
ROut 100 Ω Resistance of the transistor pulling SDA low
GOff 1 nS Leakage of the blocked transistor (line released)
CIn 10 pF Input capacitance of each pin. With RPullUp, it sets the rise time of the edges (4.7 kΩ × 10 pF = 47 ns): increasing it shows the degraded edges of an overloaded bus
RIn 10 Ω Series resistance of each pin, between the bus wire and the input capacitance. No visible effect (100 ps, about ten mV on the low level): it keeps apart the capacitances of several devices on the same bus

Writing a device script

The script only sees transactions: no bits, no START/STOP, no acknowledges. Four functions, all optional:

registers = bytearray(16)
pointer = 0

def on_write(addr, data, t, v):     # the master has just written data (bytes, never empty)
    global pointer
    pointer = data[0] % 16
    for byte in data[1:]:
        registers[pointer] = byte
        pointer = (pointer + 1) % 16

def on_read(addr, t, v):            # the master starts reading
    return bytes(registers[pointer:])    # bytes, str, list of ints or int

def outputs():                      # read after each call -> valueOut
    return registers[0]

def lines():                        # read after each call -> two lines of text (displays)
    return ('line 1', 'line 2')
Function Called Receives Returns
on_write(addr, data, t, v) at the end of an addressed write (STOP or repeated START) addr: the address used; data: the bytes; t: simulated time; v: tuple of valueIn nothing
on_read(addr, t, v) when the master starts a read same, without data the bytes to send; on_read is called again if the master wants more, 0xFF if nothing
outputs() after each handler — a number or a sequence, copied to valueOut
lines() after each handler — two strings, for a display component

addr lets a component with several addresses know which chip is being addressed. Same rules as for serial devices: a script loaded once per component, variables that persist from one call to the next, no waiting nor access to machine, print() in the log, simulation stopped on exception.

To create a new device: copy Resources/Scripts/Device/i2c_generic.py and select it in an I2cGenericDevice.

Variables to plot

Variable Contents
dev.SDA.v, dev.SCL.v Line voltages (for a device named dev)
dev.busy A transaction addressed to this device is in progress
dev.sdaDriveLow The device pulls SDA low (acknowledge, bit at 0)
dev.eventSeq Number of completed transactions

« Chattering detected » message in the log

OpenModelica reports a burst of events within a single output step this way. It is harmless: the I2C sequence is event-driven. The message appears when the output interval (Interval) is large compared with the bus clock period; reducing it makes the message go away.

Examples

Example What it shows
I2c.Echo Writing then reading back a frame; reading a register after a repeated START
I2c.MultiDevice Three devices on a 400 kHz bus, found by scan()
I2c.NoPullUp The same bus without external pull-up resistors: the internal pull-ups alone are too slow, OSError(ETIMEDOUT)
I2c.GroveLcd Grove LCD RGB display driven by an off-the-shelf driver
Weighing.KitchenScale Kitchen scale with a Grove display