Skip to content

Commit 68f7145

Browse files
committed
I2C library added.
1 parent efeeff3 commit 68f7145

File tree

7 files changed

+803
-2
lines changed

7 files changed

+803
-2
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ This repository comprises the following parts.
2424
* [A driver for GPS modules](./gps/README.md) Runs a background task to read
2525
and decode NMEA sentences, providing constantly updated position, course,
2626
altitude and time/date information.
27+
* [Communication using I2C slave mode.](./i2c/README.md) Enables a Pyboard to
28+
to communicate with another MicroPython device using stream I/O. The Pyboard
29+
achieves bidirectional communication with targets such as an ESP8266.
2730
* [Communication between devices](./syncom_as/README.md) Enables MicroPython
28-
boards to communicate without using a UART. Primarily intended to enable a
29-
a Pyboard-like device to achieve bidirectional communication with an ESP8266.
31+
boards to communicate without using a UART. This is hardware agnostic but
32+
slower than the I2C version.
3033
* [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the
3134
`uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`.
3235

i2c/README.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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

Comments
 (0)