|
| 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) |
0 commit comments