|
| 1 | +# A communication link using I2C |
| 2 | + |
| 3 | +This library implements an asynchronous bidirectional communication link |
| 4 | +between MicroPython targets using I2C. It presents a UART-like interface |
| 5 | +supporting `StreamReader` and `StreamWriter` classes. In doing so, it emulates |
| 6 | +the behaviour of a full duplex link despite the fact that the underlying I2C |
| 7 | +link is half duplex. |
| 8 | + |
| 9 | +One use case is to provide a UART-like interface to an ESP8266 while leaving |
| 10 | +the one functional UART free for the REPL. |
| 11 | + |
| 12 | +The blocking nature of the MicroPython I2C device driver is mitigated by |
| 13 | +hardware synchronisation on two wires. This ensures that the slave is |
| 14 | +configured for a transfer before the master attempts to access it. |
| 15 | + |
| 16 | +The Pyboard or similar STM based boards are currently the only targets |
| 17 | +supporting I2C slave mode. Consequently at least one end of the interface |
| 18 | +(known as the`Initiator`) must be a Pyboard. The other end may be any hardware |
| 19 | +running MicroPython. |
| 20 | + |
| 21 | +The `Initiator` implements a timeout enabling it to detect failure of the other |
| 22 | +end of the interface (the `Responder`). There is optional provision to reset |
| 23 | +the `Responder` in this event. |
| 24 | + |
| 25 | +###### [Main README](../README.md) |
| 26 | + |
| 27 | +# Contents |
| 28 | + |
| 29 | + 1. [Files](./README.md#1-files) |
| 30 | + 2. [Wiring](./README.md#2-wiring) |
| 31 | + 3. [Design](./README.md#3-design) |
| 32 | + 4. [API](./README.md#4-api) |
| 33 | + 4.1 [Channel class](./README.md#41-channel-class) |
| 34 | + 4.2 [Initiator class](./README.md#42-initiator-class) |
| 35 | + 4.2.1 [Configuration](./README.md#21-configuration) Fine-tuning the interface. |
| 36 | + 4.3 [Responder class](./README.md#431-responder-class) |
| 37 | + 5. [Limitations](./README.md#5-limitations) |
| 38 | + 5.1 [Blocking](./TUTORIAL.md#51-blocking) |
| 39 | + 5.2 [Buffering](./TUTORIAL.md#52-buffering) |
| 40 | + 5.3 [Responder crash detection](./TUTORIAL.md#53-responder-crash-detection) |
| 41 | + |
| 42 | +# 1. Files |
| 43 | + |
| 44 | + 1. `asi2c.py` Module for the `Responder` target. |
| 45 | + 2. `asi2c_i.py` The `Initiator` target requires this and `asi2c.py`. |
| 46 | + 3. `i2c_init.py` Initiator test/demo to run on a Pyboard. |
| 47 | + 4. `i2c_resp.py` Responder test/demo to run on a Pyboard. |
| 48 | + 5. `i2c_esp.py` Responder test/demo for ESP8266. |
| 49 | + |
| 50 | +Dependency: |
| 51 | + 1. `uasyncio` Official library or my fork. |
| 52 | + |
| 53 | +# 2. Wiring |
| 54 | + |
| 55 | +| Pyboard | Target | Comment | |
| 56 | +|:-------:|:------:|:-------:| |
| 57 | +| gnd | gnd | | |
| 58 | +| sda | sda | I2C | |
| 59 | +| scl | scl | I2C | |
| 60 | +| sync | sync | Any pin may be used. | |
| 61 | +| ack | ack | Any pin. | |
| 62 | +| rs_out | rst | Optional reset link. | |
| 63 | + |
| 64 | +The `sync` and `ack` wires provide synchronisation: pins used are arbitrary. In |
| 65 | +addition provision may be made for the Pyboard to reset the target if it |
| 66 | +crashes and fails to respond. If this is required, link a Pyboard pin to the |
| 67 | +target's `reset` pin. |
| 68 | + |
| 69 | +I2C requires the devices to be connected via short links and to share a common |
| 70 | +ground. The `sda` and `scl` lines also require pullup resistors. On the Pyboard |
| 71 | +V1.x these are fitted. If pins lacking these resistors are used, pullups to |
| 72 | +3.3V should be supplied. A typical value is 4.7KΩ. |
| 73 | + |
| 74 | +###### [Contents](./README.md#contents) |
| 75 | + |
| 76 | +# 3. Design |
| 77 | + |
| 78 | +The I2C specification is asymmetrical: only master devices can initiate |
| 79 | +transfers. This library enables slaves to initiate a data exchange by |
| 80 | +interrupting the master which then starts the I2C transactions. There is a |
| 81 | +timing issue in that the I2C master requires that the slave be ready before it |
| 82 | +initiates a transfer. Further, in the MicroPython implementation, a slave which |
| 83 | +is ready will block until the transfer is complete. |
| 84 | + |
| 85 | +To meet the timing constraint the slave must initiate all exchanges; it does |
| 86 | +this by interrupting the master. The slave is therefore termed the `Initiator` |
| 87 | +and the master `Responder`. The `Initiator` must be a Pyboard or other STM |
| 88 | +board supporting slave mode via the `pyb` module. |
| 89 | + |
| 90 | +To enable `Responder` to start an unsolicited data transfer, `Initiator` |
| 91 | +periodically interrupts `Responder` to cause a data exchange. If either |
| 92 | +participant has no data to send it sends an empty string. Strings are exchanged |
| 93 | +at a fixed rate to limit the interrupt overhead on `Responder`. This implies a |
| 94 | +latency on communications in either direction; the rate (maximum latency) is |
| 95 | +under application control and may be as low as 100ms. |
| 96 | + |
| 97 | +The module will run under official or `fast_io` builds of `uasyncio`. Owing to |
| 98 | +the latency discussed above the performance of this interface is largely |
| 99 | +unaffected. |
| 100 | + |
| 101 | +A further issue common to most communications protocols is synchronisation: |
| 102 | +the devices won't boot simultaneously. Initially, and after the `Initiator` |
| 103 | +reboots the `Responder`, both ends run a synchronisation phase. The iterface |
| 104 | +starts to run once each end has determined that its counterpart is ready. |
| 105 | + |
| 106 | +The design assumes exclusive use of the I2C interface. Hard or soft I2C may be |
| 107 | +used. |
| 108 | + |
| 109 | +###### [Contents](./README.md#contents) |
| 110 | + |
| 111 | +# 4. API |
| 112 | + |
| 113 | +The following is a typical `Initiator` usage example where the two participants |
| 114 | +exchange Python objects serialised using `ujson`: |
| 115 | + |
| 116 | +```python |
| 117 | +import uasyncio as asyncio |
| 118 | +from pyb import I2C # Only pyb supports slave mode |
| 119 | +from machine import Pin |
| 120 | +import asi2c |
| 121 | +import ujson |
| 122 | + |
| 123 | +i2c = I2C(1, mode=I2C.SLAVE) # Soft I2C may be used |
| 124 | +syn = Pin('X11') # Pins are arbitrary but must be declared |
| 125 | +ack = Pin('Y8') # using machine |
| 126 | +rst = (Pin('X12'), 0, 200) # Responder reset is low for 200ms |
| 127 | +chan = asi2c.Initiator(i2c, syn, ack, rst) |
| 128 | + |
| 129 | +async def receiver(): |
| 130 | + sreader = asyncio.StreamReader(chan) |
| 131 | + while True: |
| 132 | + res = await sreader.readline() |
| 133 | + print('Received', ujson.loads(res)) |
| 134 | + |
| 135 | +async def sender(): |
| 136 | + swriter = asyncio.StreamWriter(chan, {}) |
| 137 | + txdata = [0, 0] |
| 138 | + while True: |
| 139 | + await swriter.awrite(''.join((ujson.dumps(txdata), '\n'))) |
| 140 | + txdata[0] += 1 |
| 141 | + await asyncio.sleep_ms(800) |
| 142 | +``` |
| 143 | + |
| 144 | +Code for `Responder` is very similar. See `i2c_init.py` and `i2c_resp.py` for |
| 145 | +complete examples. |
| 146 | + |
| 147 | +###### [Contents](./README.md#contents) |
| 148 | + |
| 149 | +## 4.1 Channel class |
| 150 | + |
| 151 | +This is the base class for `Initiator` and `Responder` subclasses and provides |
| 152 | +support for the streaming API. Applications do not instantiate `Channel` |
| 153 | +objects. |
| 154 | + |
| 155 | +Method: |
| 156 | + 1. `close` No args. Restores the interface to its power-up state. |
| 157 | + |
| 158 | +Coroutine: |
| 159 | + 1. `ready` No args. Pause until synchronisation has been achieved. |
| 160 | + |
| 161 | +## 4.2 Initiator class |
| 162 | + |
| 163 | +Constructor args: |
| 164 | + 1. `i2c` An `I2C` instance. |
| 165 | + 2. `pin` A `Pin` instance for the `sync` signal. |
| 166 | + 3. `pinack` A `Pin` instance for the `ack` signal. |
| 167 | + 4. `reset=None` Optional tuple defining a reset pin (see below). |
| 168 | + 5. `verbose=True` If `True` causes debug messages to be output. |
| 169 | + |
| 170 | +The `reset` tuple consists of (`pin`, `level`, `time`). If provided, and the |
| 171 | +`Responder` times out, `pin` will be set to `level` for duration `time` ms. A |
| 172 | +Pyboard target with an active high reset might have: |
| 173 | + |
| 174 | +```python |
| 175 | +(machine.Pin('X12'), 0, 200) |
| 176 | +``` |
| 177 | + |
| 178 | +If the `Initiator` has no `reset` tuple and the `Responder` times out, an |
| 179 | +`OSError` will be raised. |
| 180 | + |
| 181 | +`Pin` instances passed to the constructor must be instantiated by `machine`. |
| 182 | + |
| 183 | +Class variables: |
| 184 | + 1. `timeout=1000` Timeout (in ms) before `Initiator` assumes `Responder` has |
| 185 | + failed. |
| 186 | + 2. `t_poll=100` Interval (ms) for `Initiator` polling `Responder`. |
| 187 | + |
| 188 | +Class variables should be set before instantiating `Initiator` or `Responder`. |
| 189 | +See [Section 4.4](./README.md#44-configuration). |
| 190 | + |
| 191 | +Instance variables: |
| 192 | +The `Initiator` maintains instance variables which may be used to measure its |
| 193 | +peformance. See [Section 4.4](./README.md#44-configuration). |
| 194 | + |
| 195 | +Coroutine: |
| 196 | + 1. `reboot` If a `reset` tuple was provided, reboot the `Responder`. |
| 197 | + |
| 198 | +## 4.2.1 Configuration |
| 199 | + |
| 200 | +The `Initiator` class variables determine the behaviour of the interface. Where |
| 201 | +these are altered, it should be done before instantiation. |
| 202 | + |
| 203 | +`Initiator.timeout` If the `Responder` fails the `Initiator` times out and |
| 204 | +resets the `Responder`; this occurs if `reset` tuple with a pin is supplied. |
| 205 | +Otherwise the `Initiator` raises an `OSError`. |
| 206 | + |
| 207 | +`Initiator.t_poll` This defines the polling interval for incoming data. Shorter |
| 208 | +values reduce the latency when the `Responder` sends data; at the cost of a |
| 209 | +raised CPU overhead (at both ends) in processing `Responder` polling. |
| 210 | + |
| 211 | +Times are in ms. |
| 212 | + |
| 213 | +To measure performance when running application code these `Initiator` instance |
| 214 | +variables may be read: |
| 215 | + 1. `nboots` Number of times `Responder` has failed and been rebooted. |
| 216 | + 2. `block_max` Maximum blocking time in μs. |
| 217 | + 3. `block_sum` Cumulative total of blocking time (μs). |
| 218 | + 4. `block_cnt` Transfer count: mean blocking time is `block_sum/block_cnt`. |
| 219 | + |
| 220 | +###### [Contents](./README.md#contents) |
| 221 | + |
| 222 | +## 4.3 Responder class |
| 223 | + |
| 224 | +Constructor args: |
| 225 | + 1. `i2c` An `I2C` instance. |
| 226 | + 2. `pin` A `Pin` instance for the `sync` signal. |
| 227 | + 3. `pinack` A `Pin` instance for the `ack` signal. |
| 228 | + 4. `verbose=True` If `True` causes debug messages to be output. |
| 229 | + |
| 230 | +`Pin` instances passed to the constructor must be instantiated by `machine`. |
| 231 | + |
| 232 | +Class variable: |
| 233 | + 1. `addr=0x12` Address of I2C slave. This should be set before instantiating |
| 234 | + `Initiator` or `Responder`. If the default address (0x12) is to be overriden, |
| 235 | + `Initiator` application code must instantiate the I2C accordingly. |
| 236 | + |
| 237 | +###### [Contents](./README.md#contents) |
| 238 | + |
| 239 | +# 5. Limitations |
| 240 | + |
| 241 | +## 5.1 Blocking |
| 242 | + |
| 243 | +Exchanges of data occur via `Initiator._sendrx()`, a synchronous method. This |
| 244 | +blocks the schedulers at each end for a duration dependent on the number of |
| 245 | +bytes being transferred. Tests were conducted with the supplied test scripts |
| 246 | +and the official version of `uasyncio`. Note that these scripts send short |
| 247 | +strings. |
| 248 | + |
| 249 | +With `Responder` running on a Pyboard V1.1 the duration of the ISR was up to |
| 250 | +1.3ms. |
| 251 | + |
| 252 | +With `Responder` on an ESP8266 running at 80MHz, `Initiator` blocked for up to |
| 253 | +10ms with a mean time of 2.7ms; at 160MHz the figures were 7.5ms and 2.1ms. The |
| 254 | +ISR uses soft interrupts, and blocking commences as soon as the interrupt pin |
| 255 | +is asserted. Consequently the time for which `Initiator` blocks depends on |
| 256 | +`Responder`'s interrupt latency; this may be extended by garbage collection. |
| 257 | + |
| 258 | +Figures are approximate: actual blocking time is dependent on the length of the |
| 259 | +strings, the speed of the processors, soft interrupt latency and the behaviour |
| 260 | +of other coroutines. If blocking time is critical it should be measured while |
| 261 | +running application code. |
| 262 | + |
| 263 | +I tried a variety of approaches before settling on a synchronous method for |
| 264 | +data exchange coupled with 2-wire hardware handshaking. The chosen approach |
| 265 | +minimises the time for which the schedulers are blocked. This is because of |
| 266 | +the need to initiate a blocking transfer on the I2C slave before the master can |
| 267 | +initiate a transfer. A one-wire handshake using open drain outputs is feasible |
| 268 | +but involves explicit delays. I took the view that a 2-wire solution is easier |
| 269 | +should anyone want to port the `Responder` to a platform such as the Raspberry |
| 270 | +Pi. The design has no timing constraints and uses normal I/O pins. |
| 271 | + |
| 272 | +## 5.2 Buffering |
| 273 | + |
| 274 | +The protocol does not implement flow control, incoming data being buffered |
| 275 | +until read. To avoid the risk of memory errors a coroutine should read incoming |
| 276 | +data as it arrives. Since this is the normal mode of using such an interface I |
| 277 | +see little merit in increasing the complexity of the code with flow control. |
| 278 | + |
| 279 | +Outgoing data is unbuffered. `StreamWriter.awrite` will pause until pending |
| 280 | +data has been transmitted. |
| 281 | + |
| 282 | +## 5.3 Responder crash detection |
| 283 | + |
| 284 | +The `Responder` protocol executes in a soft interrupt context. This means that |
| 285 | +the application code might fail (for example executing an infinite loop) while |
| 286 | +the ISR continues to run; `Initiator` would therefore see no problem. To trap |
| 287 | +this condition regular messages should be sent from `Responder`, with |
| 288 | +`Initiator` application code timing out on their absence and issuing `reboot`. |
| 289 | + |
| 290 | +###### [Contents](./README.md#contents) |
0 commit comments