Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: defining an I2C slave class and behaviour #3935

dpgeorge opened this issue Jul 10, 2018 · 3 comments


Copy link

commented Jul 10, 2018

I2C slave mode is an often-requested feature. See eg #3680 and #3858, and #3114 for a previous discussion that was tied to esp8266 implementation. It would be good to define an I2C slave class and how it would behave, and then implementation for various ports could follow.

stm32 recently got some I2C slave code and the interface is in this header: . In this C implementation there are 4 external functions that should be provided and which are essentially callbacks from the low-level I2C slave driver:

// the slave's address was matched, and the master requested a read/write (via rw param)
// return 0 to ack address or 1 to nack it
int i2c_slave_process_addr_match(int rw);

// master sent a byte, slave received it (val param)
// return 0 to ack the byte or 1 to nack it
int i2c_slave_process_rx_byte(uint8_t val);

// master stopped sending data via a stop or repeated start condition
void i2c_slave_process_rx_end(void);

// master requests data, slave needs to send it
// return the byte to send back to the master
uint8_t i2c_slave_process_tx_byte(void);

This describes a very low level interface, at the byte level. With a bus at 100kHz bytes are transferred at about 10kHz. Python code might be able to respond at this rate to individual bytes, but only on fast hardware. And a 400kHz SCL will make it more difficult to respond to individual bytes. So, while the above interface allows to implement (almost) any I2C device, it's not really a solution for MicroPython.

We need to define a I2C slave API at a higher level than bytes. A callback for each full bus transaction would be the way to go.

The most obvious I2C behaviour to implement is a memory-mapped device, eg I2CSlaveMemory, where the master send the memory address (1 or 2 bytes) then either reads or writes the data. For example:

mem_buf = bytearray(32)
i2c_mem = I2CSlaveMemory(scl=scl, sda=sda, addr=67, mem_addr_len=8, mem=mem_buf)
# master can now access the mem_buf array
while True:
    mem_buf[0] += 1

For simple cases that doesn't even need a callback. But I2CSlaveMemory would support callbacks like memory_read(offset, len) and memory_written(offset, len) to signal when the master did a transaction.

Other types of high-level devices include:

  • A FIFO/stream: the master can write bytes to the slave which buffer up and the slave reads them in a FIFO manner. Similarly with data going from slave to master (with first byte read by the master indicating how many bytes are in the buffer to read). This would make I2C very similar to UART and could be the basis of an I2C REPL.
  • A packet interface: the master can write packets (just a bunch of bytes in one write transaction) and the slave gets a callback when a new packet comes in, with the packet data as the arg to the callback (or they get queued in a RX buffer). For the other direction the slave could queue up packets to go out to the master when it does a read.

In addition to defining the tx/rx behaviour (memory/stream/packet/etc) it's also important to consider whether a callback interface should be used for events as they happen, or something more like polling (eg like sockets). For polling the I2C slave device would have methods read() and write() and work with select.poll() to (efficiently) query when it was ready to read/write (or have a blocking behaviour, similar to sockets). Polling behaviour would probably need buffering of some sort.


This comment has been minimized.

Copy link

commented Jul 10, 2018

With FIFO buffering and a UART-like API (with an ioctl) it could be used with uasyncio StreamReader and StreamWriter classes. Default buffer sizes would need to be capable of being overridden in the constructor given uasyncio's lethargic approach to I/O polling :)


This comment has been minimized.

Copy link
Member Author

commented Jul 10, 2018

With FIFO buffering and a UART-like API (with an ioctl) it could be used with uasyncio StreamReader and StreamWriter classes.

Yes exactly. I think it'd be a high priority item to make sure it can work with uasyncio. But not only that, it should also work with other execution models like IRQs/callbacks.

@dpgeorge dpgeorge added the rfc label Dec 6, 2018


This comment has been minimized.

Copy link

commented Jun 7, 2019

IRQ/callback is best option, and it gonna be really nice to have i2c as slave on some devices as esp8266, I currently use an atmega processors with arduino id cuz it is only thing that support i2c slave mode and works stable. Micropython, lua-nodemcu do not support slave mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
None yet
3 participants
You can’t perform that action at this time.