Skip to content

Commit e9e734e

Browse files
committed
Add INTERRUPTS.md FAQ.
1 parent 859360b commit e9e734e

File tree

2 files changed

+194
-2
lines changed

2 files changed

+194
-2
lines changed

v3/docs/INTERRUPTS.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Interfacing uasyncio to interrupts
2+
3+
This note aims to provide guidance in resolving common queries about the use of
4+
interrupts in `uasyncio` applications.
5+
6+
# 1. Does the requirement warrant an interrupt?
7+
8+
Writing an interrupt service routine (ISR) requires care: see the
9+
[official docs](https://docs.micropython.org/en/latest/reference/isr_rules.html).
10+
There are restrictions (detailed below) on the way an ISR can interface with
11+
`uasyncio`. Finally, on many platforms interrupts are a limited resource. In
12+
short interrupts are extremely useful but, if a practical alternative exists,
13+
it should be seriously considered.
14+
15+
Requirements that warrant an interrupt along with a `uasyncio` interface are
16+
ones that require a microsecond-level response, followed by later processing.
17+
Examples are:
18+
* Where the event requires an accurate timestamp.
19+
* Where a device supplies data and needs to be rapidly serviced. Data is put
20+
in a pre-allocated buffer for later processing.
21+
22+
Examples needing great care:
23+
* Where arrival of data triggers an interrupt and subsequent interrupts may
24+
occur after a short period of time.
25+
* Where arrival of an interrupt triggers complex application behaviour: see
26+
notes on [context](./INTERRUPTS.md#32-context)
27+
28+
# 2. Alternatives to interrupts
29+
30+
## 2.1 Polling
31+
32+
An alternative to interrupts is to use polling. For values that change slowly
33+
such as ambient temperature or pressure this simplification is achieved with no
34+
discernible impact on performance.
35+
```python
36+
temp = 0
37+
async def read_temp():
38+
global temp
39+
while True:
40+
temp = thermometer.read()
41+
await asyncio.sleep(60)
42+
```
43+
In cases where interrupts arrive slowly it is worth considering whether there
44+
is any gain in using an interrupt rather than polling the hardware:
45+
46+
```python
47+
async def read_data():
48+
while True:
49+
while not device.ready():
50+
await uasyncio.sleep_ms(0)
51+
data = device.read()
52+
# process the data
53+
```
54+
The overhead of polling is typically low. The MicroPython VM might use
55+
300μs to determine that the device is not ready. This will occur once per
56+
iteration of the scheduler, during which time every other pending task gets a
57+
slice of execution. If there were five tasks, each of which used 5ms of VM time,
58+
the overhead would be `0.3*100/(5*5)=1.2%` - see [latency](./INTERRUPTS.md#31-latency-in-uasyncio).
59+
60+
Devices such as pushbuttons and switches are best polled as, in most
61+
applications, latency of (say) 100ms is barely detectable. Interrupts lead to
62+
difficulties with
63+
[contact bounce](http://www.ganssle.com/debouncing.htm) which is readily
64+
handled using a simple [uasyncio driver](./DRIVERS.md). There may be exceptions
65+
which warrant an interrupt such as fast games or cases where switches are
66+
machine-operated such as limit switches.
67+
68+
## 2.2 The I/O mechanism
69+
70+
Devices such as UARTs and sockets are supported by the `uasyncio` stream
71+
mechanism. The UART driver uses interrupts at a firmware level, but exposes
72+
its interface to `uasyncio` by the `StreamReader` and `StreamWriter` classes.
73+
These greatly simplify the use of such devices.
74+
75+
It is also possible to write device drivers in Python enabling the use of the
76+
stream mechanism. This is covered in
77+
[the tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#64-writing-streaming-device-drivers).
78+
79+
# 3. Using interrupts
80+
81+
This section details some of the issues to consider where interrupts are to be
82+
used with `uasyncio`.
83+
84+
## 3.1 Latency in uasyncio
85+
86+
Consider an application with four continuously running tasks, plus a fifth
87+
which is paused waiting on an interrupt. Each of the four tasks will yield to
88+
the scheduler at intervals. Each task will have a worst-case period
89+
of blocking between yields. Assume that the worst-case times for each task are
90+
50, 30, 25 and 10ms. If the program logic allows it, the situation may arise
91+
where all of these tasks are queued for execution, and all are poised to block
92+
for the maximum period. Assume that at that moment the fifth task is triggered.
93+
94+
With current `uasyncio` design that fifth task will be queued for execution
95+
after the four pending tasks. It will therefore run after
96+
(50+30+25+10) = 115ms
97+
An enhancement to `uasyncio` has been discussed that would reduce that to 50ms,
98+
but that is the irreduceable minimum for any cooperative scheduler.
99+
100+
The key issue with latency is the case where a second interrupt occurs while
101+
the first is still waiting for its `uasyncio` handler to be scheduled. If this
102+
is a possibility, mechanisms such as buffering or queueing must be considered.
103+
104+
## 3.2 Context
105+
106+
Consider an incremental encoder providing input to a GUI. Owing to the need to
107+
track phase information an interrupt must be used for the encoder's two
108+
signals. An ISR determines the current position of the encoder, and if it has
109+
changed, calls a method in the GUI code.
110+
111+
The consequences of this can be complex. A widget's visual appearance may
112+
change. User callbacks may be triggered, running arbitrary Python code.
113+
Crucially all of this occurs in an ISR context. This is unacceptable for all
114+
the reasons identified in
115+
[this doc](https://docs.micropython.org/en/latest/reference/isr_rules.html).
116+
117+
Note that using `micropython.schedule` does not address every issue associated
118+
with ISR context. In particular restictions remain on the use of `uasyncio`
119+
operations. This is because such code can pre-empt the `uasyncio` scheduler.
120+
This is discussed further below.
121+
122+
A solution to the encoder problem is to have the ISR maintain a value of the
123+
encoder's position, with a `uasyncio` task polling this and triggering the GUI
124+
callback. This ensures that the callback runs in a `uasyncio` context and can
125+
run any Python code, including `uasyncio` operations such as creating and
126+
cancelling tasks. This will work if the position value is stored in a single
127+
word, because changes to a word are atomic (non-interruptible). A more general
128+
solution is to use `uasyncio.ThreadSafeFlag`.
129+
130+
## 3.3 Interfacing an ISR with uasyncio
131+
132+
This should be read in conjunction with the discussion of the `ThreadSafeFlag`
133+
in [the tutorial](./TUTORIAL.md#36-threadsafeflag).
134+
135+
Assume a hardware device capable of raising an interrupt when data is
136+
available. The requirement is to read the device fast and subsequently process
137+
the data using a `uasyncio` task. An obvious (but wrong) approach is:
138+
139+
```python
140+
data = bytearray(4)
141+
# isr runs in response to an interrupt from device
142+
def isr():
143+
device.read_into(data) # Perform a non-allocating read
144+
uasyncio.create_task(process_data()) # BUG
145+
```
146+
147+
This is incorrect because when an ISR runs, it can pre-empt the `uasyncio`
148+
scheduler with the result that `uasyncio.create_task()` may disrupt the
149+
scheduler. This applies whether the interrupt is hard or soft and also applies
150+
if the ISR has passed execution to another function via `micropython.schedule`:
151+
as described above, all such code runs in an ISR context.
152+
153+
The safe way to interface between ISR-context code and `uasyncio` is to have a
154+
coroutine with synchronisation performed by `uasyncio.ThreadSafeFlag`. The
155+
following fragment illustrates the creation of a task in response to an
156+
interrupt:
157+
```python
158+
tsf = uasyncio.ThreadSafeFlag()
159+
data = bytearray(4)
160+
161+
def isr(_): # Interrupt handler
162+
device.read_into(data) # Perform a non-allocating read
163+
tsf.set() # Trigger task creation
164+
165+
async def check_for_interrupts():
166+
while True:
167+
await tsf.wait()
168+
uasyncio.create_task(process_data())
169+
```
170+
It is worth considering whether there is any point in creating a task rather
171+
than using this template:
172+
```python
173+
tsf = uasyncio.ThreadSafeFlag()
174+
data = bytearray(4)
175+
176+
def isr(_): # Interrupt handler
177+
device.read_into(data) # Perform a non-allocating read
178+
tsf.set() # Trigger task creation
179+
180+
async def process_data():
181+
while True:
182+
await tsf.wait()
183+
# Process the data here before waiting for the next interrupt
184+
```
185+
186+
# 4. Conclusion
187+
188+
The key take-away is that `ThreadSafeFlag` is the only `uasyncio` construct
189+
which can safely be used in an ISR context.
190+
191+
###### [Main tutorial](./TUTORIAL.md#contents)

v3/docs/TUTORIAL.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ REPL.
3131
3.4 [Semaphore](./TUTORIAL.md#34-semaphore)
3232
     3.4.1 [BoundedSemaphore](./TUTORIAL.md#341-boundedsemaphore)
3333
3.5 [Queue](./TUTORIAL.md#35-queue)
34-
3.6 [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) Synchronisation with asynchronous events.
34+
3.6 [ThreadSafeFlag](./TUTORIAL.md#36-threadsafeflag) Synchronisation with asynchronous events and interrupts.
3535
3.7 [Barrier](./TUTORIAL.md#37-barrier)
3636
3.8 [Delay_ms](./TUTORIAL.md#38-delay_ms-class) Software retriggerable delay.
3737
3.9 [Message](./TUTORIAL.md#39-message)
@@ -893,7 +893,8 @@ asyncio.run(queue_go(4))
893893

894894
## 3.6 ThreadSafeFlag
895895

896-
This requires firmware V1.15 or later.
896+
This requires firmware V1.15 or later.
897+
See also [Interfacing uasyncio to interrupts](./INTERRUPTS.md).
897898

898899
This official class provides an efficient means of synchronising a task with a
899900
truly asynchronous event such as a hardware interrupt service routine or code

0 commit comments

Comments
 (0)