# asi2c.py A communications link using I2C slave mode on Pyboard. # Channel and Responder classes. Adapted for uasyncio V3, WBUS DIP28. # The MIT License (MIT) # # Copyright (c) 2018-2020 Peter Hinch # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import uasyncio as asyncio import machine import utime from micropython import const import io _MP_STREAM_POLL_RD = const(1) _MP_STREAM_POLL_WR = const(4) _MP_STREAM_POLL = const(3) _MP_STREAM_ERROR = const(-1) # Delay compensates for short Responder interrupt latency. Must be >= max delay # between Initiator setting a pin and initiating an I2C transfer: ensure # Initiator sets up first. _DELAY = const(20) # μs # Base class provides user interface and send/receive object buffers class Channel(io.IOBase): def __init__(self, i2c, own, rem, verbose, rxbufsize): self.rxbufsize = rxbufsize self.verbose = verbose self.synchronised = False # Hardware self.i2c = i2c self.own = own self.rem = rem own.init(mode=machine.Pin.OUT, value=1) rem.init(mode=machine.Pin.IN, pull=machine.Pin.PULL_UP) # I/O self.txbyt = b'' # Data to send self.txsiz = bytearray(2) # Size of .txbyt encoded as 2 bytes self.rxbyt = b'' self.rxbuf = bytearray(rxbufsize) self.rx_mv = memoryview(self.rxbuf) self.cantx = True # Remote can accept data async def _sync(self): self.verbose and print('Synchronising') self.own(0) while self.rem(): await asyncio.sleep_ms(100) # Both pins are now low await asyncio.sleep(0) self.verbose and print('Synchronised') self.synchronised = True def waitfor(self, val): # Initiator overrides while not self.rem() == val: pass # Get incoming bytes instance from memoryview. def _handle_rxd(self, msg): self.rxbyt = bytes(msg) def _txdone(self): self.txbyt = b'' self.txsiz[0] = 0 self.txsiz[1] = 0 # Stream interface def ioctl(self, req, arg): ret = _MP_STREAM_ERROR if req == _MP_STREAM_POLL: ret = 0 if self.synchronised: if arg & _MP_STREAM_POLL_RD: if self.rxbyt: ret |= _MP_STREAM_POLL_RD if arg & _MP_STREAM_POLL_WR: if (not self.txbyt) and self.cantx: ret |= _MP_STREAM_POLL_WR return ret def readline(self): n = self.rxbyt.find(b'\n') if n == -1: t = self.rxbyt[:] self.rxbyt = b'' else: t = self.rxbyt[: n + 1] self.rxbyt = self.rxbyt[n + 1:] return t.decode() def read(self, n): t = self.rxbyt[:n] self.rxbyt = self.rxbyt[n:] return t.decode() # Set .txbyt to the required data. Return its size. So awrite returns # with transmission occurring in tha background. # uasyncio V3: Stream.drain() calls write with buf being a memoryview # and no off or sz args. def write(self, buf): if self.synchronised: if self.txbyt: # Initial call from awrite return 0 # Waiting for existing data to go out l = len(buf) self.txbyt = buf self.txsiz[0] = l & 0xff self.txsiz[1] = l >> 8 return l return 0 # User interface # Wait for sync async def ready(self): while not self.synchronised: await asyncio.sleep_ms(100) # Leave pin high in case we run again def close(self): self.own(1) # Responder is I2C master. It is cross-platform and uses machine. # It does not handle errors: if I2C fails it dies and awaits reset by initiator. # send_recv is triggered by Interrupt from Initiator. class Responder(Channel): addr = 0x12 rxbufsize = 200 def __init__(self, i2c, pin, pinack, verbose=True): super().__init__(i2c, pinack, pin, verbose, self.rxbufsize) loop = asyncio.get_event_loop() loop.create_task(self._run()) async def _run(self): await self._sync() # own pin ->0, wait for remote pin == 0 self.rem.irq(handler=self._handler, trigger=machine.Pin.IRQ_RISING) # Request was received: immediately read payload size, then payload # On Pyboard blocks for 380μs to 1.2ms for small amounts of data def _handler(self, _, sn=bytearray(2), txnull=bytearray(2)): addr = Responder.addr self.rem.irq(handler=None) utime.sleep_us(_DELAY) # Ensure Initiator has set up to write. self.i2c.readfrom_into(addr, sn) self.own(1) self.waitfor(0) self.own(0) n = sn[0] + ((sn[1] & 0x7f) << 8) # no of bytes to receive if n > self.rxbufsize: raise ValueError('Receive data too large for buffer.') self.cantx = not bool(sn[1] & 0x80) # Can Initiator accept a payload? if n: self.waitfor(1) utime.sleep_us(_DELAY) mv = memoryview(self.rx_mv[0: n]) # allocates self.i2c.readfrom_into(addr, mv) self.own(1) self.waitfor(0) self.own(0) self._handle_rxd(mv) self.own(1) # Request to send self.waitfor(1) utime.sleep_us(_DELAY) dtx = self.txbyt != b'' and self.cantx # Data to send siz = self.txsiz if dtx else txnull if self.rxbyt: siz[1] |= 0x80 # Hold off Initiator TX else: siz[1] &= 0x7f self.i2c.writeto(addr, siz) # Was getting ENODEV occasionally on Pyboard self.own(0) self.waitfor(0) if dtx: self.own(1) self.waitfor(1) utime.sleep_us(_DELAY) self.i2c.writeto(addr, self.txbyt) self.own(0) self.waitfor(0) self._txdone() # Invalidate source self.rem.irq(handler=self._handler, trigger=machine.Pin.IRQ_RISING)