Skip to content

Commit 84ae852

Browse files
committed
primitives/irq_event.py added. Document ISR interfacing.
1 parent 8dc241f commit 84ae852

File tree

5 files changed

+227
-18
lines changed

5 files changed

+227
-18
lines changed

v3/docs/DRIVERS.md

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ events.
1111
The asynchronous ADC supports pausing a task until the value read from an ADC
1212
goes outside defined bounds.
1313

14+
An IRQ_EVENT class provides a means of interfacing uasyncio to hard or soft
15+
interrupt service routines.
16+
1417
# 1. Contents
1518

1619
1. [Contents](./DRIVERS.md#1-contents)
@@ -24,9 +27,10 @@ goes outside defined bounds.
2427
5. [ADC monitoring](./DRIVERS.md#5-adc-monitoring) Pause until an ADC goes out of bounds
2528
5.1 [AADC class](./DRIVERS.md#51-aadc-class)
2629
5.2 [Design note](./DRIVERS.md#52-design-note)
27-
6. [Additional functions](./DRIVERS.md#6-additional-functions)
28-
6.1 [launch](./DRIVERS.md#61-launch) Run a coro or callback interchangeably
29-
6.2 [set_global_exception](./DRIVERS.md#62-set_global_exception) Simplify debugging with a global exception handler
30+
6. [IRQ_EVENT](./DRIVERS.md#6-irq_event)
31+
7. [Additional functions](./DRIVERS.md#6-additional-functions)
32+
7.1 [launch](./DRIVERS.md#71-launch) Run a coro or callback interchangeably
33+
7.2 [set_global_exception](./DRIVERS.md#72-set_global_exception) Simplify debugging with a global exception handler
3034

3135
###### [Tutorial](./TUTORIAL.md#contents)
3236

@@ -331,9 +335,95 @@ this for applications requiring rapid response.
331335

332336
###### [Contents](./DRIVERS.md#1-contents)
333337

334-
# 6. Additional functions
338+
# 6. IRQ_EVENT
339+
340+
Interfacing an interrupt service routine to `uasyncio` requires care. It is
341+
invalid to issue `create_task` or to trigger an `Event` in an ISR as it can
342+
cause a race condition in the scheduler. It is intended that `Event` will
343+
become compatible with soft IRQ's in a future revison of `uasyncio`.
344+
345+
Currently there are two ways of interfacing hard or soft IRQ's with `uasyncio`.
346+
One is to use a busy-wait loop as per the
347+
[Message](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#36-message)
348+
primitive. A more efficient approach is to use this `IRQ_EVENT` class. The API
349+
is a subset of the `Event` class, so if official `Event` becomes thread-safe
350+
it may readily be substituted. The `IRQ_EVENT` class uses uses the `uasyncio`
351+
I/O mechanism to achieve thread-safe operation.
352+
353+
Unlike `Event` only one task can wait on an `IRQ_EVENT`.
354+
355+
Constructor:
356+
* This has no args.
357+
358+
Synchronous Methods:
359+
* `set()` Initiates the event. May be called from a hard or soft ISR. Returns
360+
fast.
361+
* `is_set()` Returns `True` if the irq_event is set.
362+
* `clear()` This does nothing; its purpose is to enable code to be written
363+
compatible with a future thread-safe `Event` class, with the ISR setting then
364+
immediately clearing the event.
365+
366+
Asynchronous Method:
367+
* `wait` Pause until irq_event is set. The irq_event is cleared.
368+
369+
A single task waits on the event by issuing `await irq_event.wait()`; execution
370+
pauses until the ISR issues `irq_event.set()`. Execution of the paused task
371+
resumes when it is next scheduled. Under current `uasyncio` (V3.0.0) scheduling
372+
of the paused task does not occur any faster than using busy-wait. In typical
373+
use the ISR services the interrupting device, saving received data, then sets
374+
the irq_event to trigger processing of the received data.
375+
376+
If interrupts occur faster than `uasyncio` can schedule the paused task, more
377+
than one interrupt may occur before the paused task runs.
378+
379+
Example usage (assumes a Pyboard with pins X1 and X2 linked):
380+
```python
381+
from machine import Pin
382+
from pyb import LED
383+
import uasyncio as asyncio
384+
import micropython
385+
from primitives.irq_event import IRQ_EVENT
386+
387+
micropython.alloc_emergency_exception_buf(100)
388+
389+
driver = Pin(Pin.board.X2, Pin.OUT)
390+
receiver = Pin(Pin.board.X1, Pin.IN)
391+
evt_rx = IRQ_EVENT() # IRQ_EVENT instance for receiving Pin
392+
393+
def pin_han(pin): # Hard IRQ handler. Typically services a device
394+
evt_rx.set() # then issues this which returns quickly
395+
396+
receiver.irq(pin_han, Pin.IRQ_FALLING, hard=True) # Set up hard ISR
397+
398+
async def pulse_gen(pin):
399+
while True:
400+
await asyncio.sleep_ms(500)
401+
pin(not pin())
402+
403+
async def red_handler(evt_rx, iterations):
404+
led = LED(1)
405+
for x in range(iterations):
406+
await evt_rx.wait() # Pause until next interrupt
407+
print(x)
408+
led.toggle()
409+
410+
async def irq_test(iterations):
411+
pg = asyncio.create_task(pulse_gen(driver))
412+
await red_handler(evt_rx, iterations)
413+
pg.cancel()
414+
415+
def test(iterations=20):
416+
try:
417+
asyncio.run(irq_test(iterations))
418+
finally:
419+
asyncio.new_event_loop()
420+
```
421+
422+
###### [Contents](./DRIVERS.md#1-contents)
423+
424+
# 7. Additional functions
335425

336-
## 6.1 Launch
426+
## 7.1 Launch
337427

338428
Importe as follows:
339429
```python
@@ -345,7 +435,7 @@ runs it and returns the callback's return value. If a coro is passed, it is
345435
converted to a `task` and run asynchronously. The return value is the `task`
346436
instance. A usage example is in `primitives/switch.py`.
347437

348-
## 6.2 set_global_exception
438+
## 7.2 set_global_exception
349439

350440
Import as follows:
351441
```python

v3/docs/TUTORIAL.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ REPL.
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 [Synchronising to hardware](./TUTORIAL.md#39-synchronising-to-hardware)
38-
Debouncing switches and pushbuttons. Taming ADC's.
38+
Debouncing switches and pushbuttons. Taming ADC's. Interfacing interrupts.
3939
4. [Designing classes for asyncio](./TUTORIAL.md#4-designing-classes-for-asyncio)
4040
4.1 [Awaitable classes](./TUTORIAL.md#41-awaitable-classes)
4141
     4.1.1 [Use in context managers](./TUTORIAL.md#411-use-in-context-managers)
@@ -879,22 +879,30 @@ asyncio.run(queue_go(4))
879879

880880
This is an unofficial primitive with no counterpart in CPython asyncio.
881881

882-
This is similar to the `Event` class. It provides the following:
882+
This is similar to the `Event` class. It differs in that:
883883
* `.set()` has an optional data payload.
884884
* `.set()` is capable of being called from a hard or soft interrupt service
885885
routine - a feature not yet available in the more efficient official `Event`.
886886
* It is an awaitable class.
887887

888-
The `.set()` method can accept an optional data value of any type. A task
888+
For interfacing to interrupt service routines see also
889+
[the IRQ_EVENT class](./DRIVERS.md#6-irq_event) which is more efficient but
890+
lacks the payload feature.
891+
892+
Limitation: `Message` is intended for 1:1 operation where a single task waits
893+
on a message from another task or ISR. The receiving task should issue
894+
`.clear`.
895+
896+
The `.set()` method can accept an optional data value of any type. The task
889897
waiting on the `Message` can retrieve it by means of `.value()`. Note that
890898
`.clear()` will set the value to `None`. One use for this is for the task
891-
setting the `Message` to issue `.set(utime.ticks_ms())`. A task waiting on the
892-
`Message` can determine the latency incurred, for example to perform
899+
setting the `Message` to issue `.set(utime.ticks_ms())`. The task waiting on
900+
the `Message` can determine the latency incurred, for example to perform
893901
compensation for this.
894902

895-
Like `Event`, `Message` provides a way for one or more tasks to pause until
896-
another flags them to continue. A `Message` object is instantiated and made
897-
accessible to all tasks using it:
903+
Like `Event`, `Message` provides a way a task to pause until another flags it
904+
to continue. A `Message` object is instantiated and made accessible to the task
905+
using it:
898906

899907
```python
900908
import uasyncio as asyncio
@@ -920,9 +928,16 @@ A `Message` can provide a means of communication between an interrupt handler
920928
and a task. The handler services the hardware and issues `.set()` which is
921929
tested in slow time by the task.
922930

923-
Currently its behaviour differs from that of `Event` where multiple tasks wait
924-
on a `Message`. This may change: it is therefore recommended to use `Message`
925-
instances with only one receiving task.
931+
Constructor:
932+
* Optional arg `delay_ms=0` Polling interval.
933+
Synchronous methods:
934+
* `set(data=None)` Trigger the message with optional payload.
935+
* `is_set()` Return `True` if the message is set.
936+
* `clear()` Clears the triggered status and sets payload to `None`.
937+
* `value()` Return the payload.
938+
Asynchronous Method:
939+
* `wait` Pause until message is triggered. You can also `await` the message as
940+
per the above example.
926941

927942
###### [Contents](./TUTORIAL.md#contents)
928943

@@ -1110,6 +1125,9 @@ The following hardware-related classes are documented [here](./DRIVERS.md):
11101125
* `AADC` Asynchronous ADC. A task can pause until the value read from an ADC
11111126
goes outside defined bounds. Bounds can be absolute or relative to the current
11121127
value.
1128+
* `IRQ_EVENT` A way to interface between hard or soft interrupt service
1129+
routines and `uasyncio`. Discusses the hazards of apparently obvious ways such
1130+
as issuing `.create_task` or using the `Event` class.
11131131

11141132
###### [Contents](./TUTORIAL.md#contents)
11151133

v3/primitives/irq_event.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# irq_event.py Interface between uasyncio and asynchronous events
2+
# A thread-safe class. API is a subset of Event.
3+
4+
# Copyright (c) 2020 Peter Hinch
5+
# Released under the MIT License (MIT) - see LICENSE file
6+
7+
import uasyncio as asyncio
8+
import io
9+
10+
MP_STREAM_POLL_RD = const(1)
11+
MP_STREAM_POLL = const(3)
12+
MP_STREAM_ERROR = const(-1)
13+
14+
class IRQ_EVENT(io.IOBase):
15+
def __init__(self):
16+
self.state = False # False=unset; True=set
17+
self.sreader = asyncio.StreamReader(self)
18+
19+
def wait(self):
20+
await self.sreader.readline()
21+
self.state = False
22+
23+
def set(self):
24+
self.state = True
25+
return self
26+
27+
def is_set(self):
28+
return self.state
29+
30+
def readline(self):
31+
return b'\n'
32+
33+
def clear(self):
34+
pass # See docs
35+
36+
def ioctl(self, req, arg):
37+
ret = MP_STREAM_ERROR
38+
if req == MP_STREAM_POLL:
39+
ret = 0
40+
if arg & MP_STREAM_POLL_RD:
41+
if self.state:
42+
ret |= MP_STREAM_POLL_RD
43+
return ret

v3/primitives/message.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
# message.clear() should be issued
1717

1818
# This more efficient version is commented out because Event.set is not ISR
19-
# friendly. TODO If it gets fixed, reinstate this (tested) version.
19+
# friendly. TODO If it gets fixed, reinstate this (tested) version and update
20+
# tutorial for 1:n operation.
2021
#class Message(asyncio.Event):
2122
#def __init__(self, _=0):
2223
#self._data = None

v3/primitives/tests/irq_event_test.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# irq_event_test.py Test for irq_event class
2+
# Run on Pyboard with link between X1 and X2
3+
4+
# Copyright (c) 2020 Peter Hinch
5+
# Released under the MIT License (MIT) - see LICENSE file
6+
7+
# from primitives.tests.irq_event_test import test
8+
# test()
9+
10+
from machine import Pin
11+
from pyb import LED
12+
import uasyncio as asyncio
13+
import micropython
14+
from primitives.irq_event import IRQ_EVENT
15+
16+
def printexp():
17+
print('Test expects a Pyboard with X1 and X2 linked. Expected output:')
18+
print('\x1b[32m')
19+
print('Flashes red LED and prints numbers 0-19')
20+
print('\x1b[39m')
21+
print('Runtime: 20s')
22+
23+
printexp()
24+
25+
micropython.alloc_emergency_exception_buf(100)
26+
27+
driver = Pin(Pin.board.X2, Pin.OUT)
28+
receiver = Pin(Pin.board.X1, Pin.IN)
29+
evt_rx = IRQ_EVENT()
30+
31+
def pin_han(pin):
32+
evt_rx.set()
33+
34+
receiver.irq(pin_han, Pin.IRQ_FALLING, hard=True)
35+
36+
async def pulse_gen(pin):
37+
while True:
38+
await asyncio.sleep_ms(500)
39+
pin(not pin())
40+
41+
async def red_handler(evt_rx):
42+
led = LED(1)
43+
for x in range(20):
44+
await evt_rx.wait()
45+
print(x)
46+
led.toggle()
47+
48+
async def irq_test():
49+
pg = asyncio.create_task(pulse_gen(driver))
50+
await red_handler(evt_rx)
51+
pg.cancel()
52+
53+
def test():
54+
try:
55+
asyncio.run(irq_test())
56+
finally:
57+
asyncio.new_event_loop()

0 commit comments

Comments
 (0)