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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy