Bidirectional communication using I2C

Discuss development of drivers for external hardware and components, such as LCD screens, sensors, motor drivers, etc.
Target audience: Users and developers of drivers.
Post Reply
User avatar
pythoncoder
Posts: 3060
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Bidirectional communication using I2C

Post by pythoncoder » Mon Oct 15, 2018 4:38 pm

My async library now includes a driver to enable a Pyboard to communicate with another MicroPython device using I2C. The Pyboard operates in slave mode. Two additional wires are used for synchronisation. The devices appear as stream devices, enabling them to be used in asynchronous code in the same way as UARTs. Either end can send unsolicited data: externally it appears as a full duplex link even though I2C is half duplex.

An obvious application is to enable a Pyboard to communicate with an ESP8266 while leaving the UART free for debugging via the REPL (or for other purposes). There is also scope for porting the master end of the interface to hardware such as the Raspberry Pi.

The Pyboard I2C slave code performs blocking reads and writes. You can't polish a ... oops ... rewind. ;)

You can't make a silk purse out of a sow's ear. So some blocking occurs. Much effort went into into synchronisation to ensure that this is typically in single digits of milliseconds. The following is a usage example where the two participants exchange Python objects serialised with ujson. ESP8266 code:

Code: Select all

import uasyncio as asyncio
from machine import Pin, I2C
import asi2c
import ujson

i2c = I2C(scl=Pin(0),sda=Pin(2))  # software I2C
syn = Pin(5)
ack = Pin(4)
chan = asi2c.Responder(i2c, syn, ack)

async def receiver():
    sreader = asyncio.StreamReader(chan)
    while True:
        res = await sreader.readline()
        print('Received', ujson.loads(res))

async def sender():
    swriter = asyncio.StreamWriter(chan, {})
    txdata = [0, 0]
    while True:
        await swriter.awrite(''.join((ujson.dumps(txdata), '\n')))
        txdata[1] += 1
        await asyncio.sleep_ms(1500)

loop = asyncio.get_event_loop()
loop.create_task(receiver())
loop.create_task(sender())
loop.run_forever()
Peter Hinch

jickster
Posts: 532
Joined: Thu Sep 07, 2017 8:57 pm

Re: Bidirectional communication using I2C

Post by jickster » Mon Oct 15, 2018 5:57 pm

Does this include functionality described in https://github.com/micropython/micropython/issues/3935 RFC: defining an I2C slave class and behaviour ?

User avatar
Roberthh
Posts: 1081
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Bidirectional communication using I2C

Post by Roberthh » Mon Oct 15, 2018 7:48 pm

@pythoncoder That looks very impressive. And I learned two new sayings.

uCTRL
Posts: 5
Joined: Fri Oct 12, 2018 11:50 pm

Re: Bidirectional communication using I2C

Post by uCTRL » Mon Oct 15, 2018 10:10 pm

Why not SPI, it already has bidirectional data path?
One extra wiring line can make SPI asynchronous.
As I understand, the whole point of I2C was to use minimal wiring configuration (2 wires)?
As you point out, pyboard has terrible I2C and Uart interrupt and callback implementation.
:?

User avatar
pythoncoder
Posts: 3060
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Bidirectional communication using I2C

Post by pythoncoder » Tue Oct 16, 2018 5:21 am

jickster wrote:
Mon Oct 15, 2018 5:57 pm
Does this include functionality described in https://github.com/micropython/micropython/issues/3935 RFC: defining an I2C slave class and behaviour ?
I haven't modified the C code. This is a pure Python driver using existing firmware.
Peter Hinch

User avatar
pythoncoder
Posts: 3060
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Bidirectional communication using I2C

Post by pythoncoder » Tue Oct 16, 2018 6:04 am

uCTRL wrote:
Mon Oct 15, 2018 10:10 pm
Why not SPI, it already has bidirectional data path?
One extra wiring line can make SPI asynchronous.
A number of people have enquired about I2C slave mode and various partial solutions have been produced. These had limitations and I wanted to produce a general solution. Crucially I wanted one which worked with existing firmware.

An SPI solution would use the same number of wires (assuming synchronisation could be achieved with one wire). It has the potential to be faster, but I don't think the implementation would be significantly simpler. The issue remains the same, because SPI.write_readinto is a blocking method. But I think there's a bigger problem.

There is an asymmetry in SPI. The clock signal is sourced by the CPU and sunk by the target. MicroPython devices see themselves as CPU's and hence source the clock, so an SPI slave would need a new driver. To get performance and nonblocking behaviour this would need to be written in C. I have neither the ambition nor the C skill to write such an SPI slave, and I've no idea whether such a driver would be accepted into mainstream. I don't recall this ever coming up for discussion.
As I understand, the whole point of I2C was to use minimal wiring configuration (2 wires)?
As you point out, pyboard has terrible I2C and Uart interrupt and callback implementation.
:?
In my experience the UART driver works very well, including in async applications. It uses hardware interrupts with a circular buffer so it's a genuinely nonblocking driver. People (yourself?) have requested support for callbacks. I've yet to see the need, but I use coroutines where possible and try to avoid callback soup. ;)
Peter Hinch

Post Reply