Skip to content

Commit d6b1e05

Browse files
committed
monitor: New release with multiple changes.
1 parent 4a25c91 commit d6b1e05

11 files changed

+367
-150
lines changed

v3/as_demos/monitor/README.md

Lines changed: 172 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,19 @@ This library provides a means of examining the behaviour of a running
44
`uasyncio` system. The device under test is linked to a Raspberry Pi Pico. The
55
latter displays the behaviour of the host by pin changes and/or optional print
66
statements. A logic analyser or scope provides an insight into the way an
7-
asynchronous application is working, although valuable informtion can be
8-
gleaned without such tools.
7+
asynchronous application is working; valuable informtion can also be gleaned at
8+
the Pico command line.
99

1010
Communication with the Pico may be by UART or SPI, and is uni-directional from
11-
system under test to Pico. If a UART is used only one GPIO pin is used; at last
12-
a use for the ESP8266 transmit-only UART(1). SPI requires three - `mosi`, `sck`
13-
and `cs/`.
11+
system under test to Pico. If a UART is used only one GPIO pin is used. SPI
12+
requires three - `mosi`, `sck` and `cs/`.
1413

1514
Where an application runs multiple concurrent tasks it can be difficult to
16-
locate a task which is hogging CPU time. Long blocking periods can also result
17-
from several tasks each of which can block for a period. If, on occasion, these
18-
are scheduled in succession, the times can add. The monitor issues a trigger
19-
pulse when the blocking period exceeds a threshold. With a logic analyser the
20-
system state at the time of the transient event may be examined.
15+
identify a task which is hogging CPU time. Long blocking periods can also occur
16+
when several tasks each block for a period. If, on occasion, these are
17+
scheduled in succession, the times will add. The monitor issues a trigger pulse
18+
when the blocking period exceeds a threshold. With a logic analyser the system
19+
state at the time of the transient event may be examined.
2120

2221
The following image shows the `quick_test.py` code being monitored at the point
2322
when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line
@@ -29,27 +28,45 @@ detect" trigger 100ms after hogging starts.
2928
![Image](./monitor.jpg)
3029

3130
The following image shows brief (<4ms) hogging while `quick_test.py` ran. The
32-
likely cause is garbage collection on the Pyboard D host.
31+
likely cause is garbage collection on the Pyboard D host. The monitor was able
32+
to demostrate that this never exceeded 5ms.
3333
![Image](./monitor_gc.jpg)
3434

3535
### Status
3636

37-
2nd Oct 2021 Add trigger function.
38-
39-
30th Sep 2021 Pico code has improved hog detection.
40-
41-
27th Sep 2021 SPI support added. The `set_uart` method is replaced by
42-
`set_device`. Pin mappings on the Pico changed.
43-
44-
21st Sep 2021 Initial release.
37+
4th Oct 2021 Please regard this as "all new". Many functions have been renamed,
38+
error checking has been improved and code made more efficient.
4539

4640
## 1.1 Pre-requisites
4741

4842
The device being monitored must run firmware V1.17 or later. The `uasyncio`
49-
version should be V3 (included in the firmware).
43+
version should be V3 (included in the firmware). The file `monitor.py` should
44+
be copied to the target, and `monitor_pico` to the Pico.
5045

5146
## 1.2 Usage
5247

48+
A minimal example of a UART-monitored application looks like this:
49+
```python
50+
import uasyncio as asyncio
51+
from machine import UART # Using a UART for monitoring
52+
import monitor
53+
monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz.
54+
55+
@monitor.asyn(1) # Assign ident 1 to foo (GPIO 4)
56+
async def foo():
57+
await asyncio.sleep_ms(100)
58+
59+
async def main():
60+
monitor.init() # Initialise Pico state at the start of every run
61+
while True:
62+
await foo() # Pico GPIO4 will go high for duration
63+
await asyncio.sleep_ms(100)
64+
65+
try:
66+
asyncio.run(main())
67+
finally:
68+
asyncio.new_event_loop()
69+
```
5370
Example script `quick_test.py` provides a usage example. It may be adapted to
5471
use a UART or SPI interface: see commented-out code.
5572

@@ -63,14 +80,14 @@ device. The Pico must be set up to match the interface chosen on the host: see
6380
In the case of a UART an initialised UART with 1MHz baudrate is passed:
6481
```python
6582
from machine import UART
66-
from monitor import monitor, monitor_init, hog_detect, set_device
67-
set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz.
83+
import monitor
84+
monitor.set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz.
6885
```
6986
In the case of SPI initialised SPI and cs/ Pin instances are passed:
7087
```python
7188
from machine import Pin, SPI
72-
from monitor import monitor, monitor_init, hog_detect, set_device
73-
set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI
89+
import monitor
90+
monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI
7491
```
7592
The SPI instance must have default args; the one exception being baudrate which
7693
may be any value. I have tested up to 30MHz but there is no benefit in running
@@ -81,18 +98,19 @@ bus with other devices, although I haven't tested this.
8198

8299
On startup, after defining the interface, an application should issue:
83100
```python
84-
monitor_init()
101+
monitor.init()
85102
```
86-
Coroutines to be monitored are prefixed with the `@monitor` decorator:
103+
Coroutines to be monitored are prefixed with the `@monitor.asyn` decorator:
87104
```python
88-
@monitor(2, 3)
105+
@monitor.asyn(2, 3)
89106
async def my_coro():
90107
# code
91108
```
92-
The decorator args are as follows:
93-
1. A unique `ident` for the code being monitored. Determines the pin number on
94-
the Pico. See [Pico Pin mapping](./README.md#3-pico-pin-mapping).
95-
2. An optional arg defining the maximum number of concurrent instances of the
109+
The decorator positional args are as follows:
110+
1. `n` A unique `ident` in range `0 <= ident <= 21` for the code being
111+
monitored. Determines the pin number on the Pico. See
112+
[Pico Pin mapping](./README.md#3-pico-pin-mapping).
113+
2. `max_instances=1` Defines the maximum number of concurrent instances of the
96114
task to be independently monitored (default 1).
97115

98116
Whenever the coroutine runs, a pin on the Pico will go high, and when the code
@@ -122,7 +140,7 @@ tasks from running. Determining the task responsible can be difficult.
122140
The pin state only indicates that the task is running. A pin state of 1 does
123141
not imply CPU hogging. Thus
124142
```python
125-
@monitor(3)
143+
@monitor.asyn(3)
126144
async def long_time():
127145
await asyncio.sleep(30)
128146
```
@@ -136,9 +154,9 @@ long gaps appear in the pulses on GPIO 3, other tasks are hogging the CPU.
136154
Usage of this is optional. To use, issue
137155
```python
138156
import uasyncio as asyncio
139-
from monitor import monitor, hog_detect
157+
import monitor
140158
# code omitted
141-
asyncio.create_task(hog_detect())
159+
asyncio.create_task(monitor.hog_detect())
142160
# code omitted
143161
```
144162
To aid in detecting the gaps in execution, the Pico code implements a timer.
@@ -148,35 +166,53 @@ pulse can be used to trigger a scope or logic analyser. The duration of the
148166
timer may be adjusted. Other modes of hog detection are also supported. See
149167
[section 4](./README.md~4-the-pico-code).
150168

169+
## 1.4 Validation of idents
170+
171+
Re-using idents would lead to confusing behaviour. A `ValueError` is thrown if
172+
an ident is out of range or is assigned to more than one coroutine.
173+
151174
# 2. Monitoring synchronous code
152175

153-
In general there are easier ways to debug synchronous code. However in the
154-
context of a monitored asynchronous application there may be a need to view the
155-
timing of synchronous code. Functions and methods may be monitored either in
156-
the declaration via a decorator or when called via a context manager. Timing
157-
markers may be inserted in code: a call to `monitor.trigger` will cause a Pico
158-
pin to pulse.
176+
In the context of an asynchronous application there may be a need to view the
177+
timing of synchronous code, or simply to create a trigger pulse at a known
178+
point in the code. The following are provided:
179+
* A `sync` decorator for synchronous functions or methods: like `async` it
180+
monitors every call to the function.
181+
* A `trigger` function which issues a brief pulse on the Pico.
182+
* A `mon_call` context manager enables function monitoring to be restricted to
183+
specific calls.
184+
185+
Idents used by `trigger` or `mon_call` must be reserved: this is because these
186+
may occur in a looping construct. This enables the validation to protect
187+
against inadvertent multiple usage of an ident. The `monitor.reserve()`
188+
function can reserve one or more idents:
189+
```python
190+
monitor.reserve(4, 9, 10)
191+
```
159192

160-
## 2.1 The mon_func decorator
193+
## 2.1 The sync decorator
161194

162-
This works as per the asynchronous decorator, but without the `max_instances`
163-
arg. This will activate the GPIO associated with ident 20 for the duration of
164-
every call to `sync_func()`:
195+
This works as per the `@async` decorator, but with no `max_instances` arg. This
196+
will activate GPIO 26 (associated with ident 20) for the duration of every call
197+
to `sync_func()`:
165198
```python
166-
@mon_func(20)
199+
@monitor.sync(20)
167200
def sync_func():
168201
pass
169202
```
203+
Note that the ident must not be reserved.
170204

171205
## 2.2 The mon_call context manager
172206

173207
This may be used to monitor a function only when called from specific points in
174208
the code.
175209
```python
210+
monitor.reserve(22)
211+
176212
def another_sync_func():
177213
pass
178214

179-
with mon_call(22):
215+
with monitor.mon_call(22):
180216
another_sync_func()
181217
```
182218

@@ -187,7 +223,13 @@ It is advisable not to use the context manager with a function having the
187223

188224
A call to `monitor.trigger(n)` may be inserted anywhere in synchronous or
189225
asynchronous code. When this runs, a brief (~80μs) pulse will occur on the Pico
190-
pin with ident `n`.
226+
pin with ident `n`. As per `mon_call`, ident `n` must be reserved.
227+
```python
228+
monitor.reserve(10)
229+
230+
def foo():
231+
monitor.trigger(10) # Pulse ident 10, GPIO 13
232+
```
191233

192234
# 3. Pico Pin mapping
193235

@@ -290,20 +332,61 @@ The mode also affects reporting. The effect of mode is as follows:
290332
* `MAX` Report at end of outage but only when prior maximum exceeded. This
291333
ensures worst-case is not missed.
292334

293-
# 5. Performance and design notes
335+
Running the following produce instructive console output:
336+
```python
337+
from monitor_pico import run, MAX
338+
run((1, MAX))
339+
```
340+
341+
# 5. Test and demo scripts
342+
343+
`quick_test.py` Primarily tests deliberate CPU hogging. Discussed in section 1.
344+
345+
`full_test.py` Tests task timeout and cancellation, also the handling of
346+
multiple task instances. If the Pico is run with `run((1, MAX))` it reveals
347+
the maximum time the host hogs the CPU. On a Pyboard D I measured 5ms.
348+
349+
The sequence here is a trigger is issued on ident 4. The task on ident 1 is
350+
started, but times out after 100ms. 100ms later, five instances of the task on
351+
ident 1 are started, at 100ms intervals. They are then cancelled at 100ms
352+
intervals. Because 3 idents are allocated for multiple instances, these show up
353+
on idents 1, 2, and 3 with ident 3 representing 3 instances. Ident 3 therefore
354+
only goes low when the last of these three instances is cancelled.
355+
356+
![Image](./tests/full_test.jpg)
357+
358+
`latency.py` Measures latency between the start of a monitored task and the
359+
Pico pin going high. The sequence below is first the task pulses a pin (ident
360+
6). Then the Pico pin monitoring the task goes high (ident 1 after ~20μs). Then
361+
the trigger on ident 2 occurs 112μs after the pin pulse.
362+
363+
![Image](./tests/latency.jpg)
364+
365+
`syn_test.py` Demonstrates two instances of a bound method along with the ways
366+
of monitoring synchronous code. The trigger on ident 5 marks the start of the
367+
sequence. The `foo1.pause` method on ident 1 starts and runs `foo1.wait1` on
368+
ident 3. 100ms after this ends, `foo`.wait2` on ident 4 is triggered. 100ms
369+
after this ends, `foo1.pause` on ident 1 ends. The second instance of `.pause`
370+
(`foo2.pause`) on ident 2 repeats this sequence shifted by 50ms. The 10ms gaps
371+
in `hog_detect` show the periods of deliberate CPU hogging.
372+
373+
![Image](./tests/syn_test.jpg)
374+
375+
# 6. Performance and design notes
294376

295377
Using a UART the latency between a monitored coroutine starting to run and the
296378
Pico pin going high is about 23μs. With SPI I measured -12μs. This isn't as
297379
absurd as it sounds: a negative latency is the effect of the decorator which
298380
sends the character before the coroutine starts. These values are small in the
299381
context of `uasyncio`: scheduling delays are on the order of 150μs or greater
300-
depending on the platform. See `quick_test.py` for a way to measure latency.
382+
depending on the platform. See `tests/latency.py` for a way to measure latency.
301383

302-
The use of decorators is intended to ease debugging: they are readily turned on
303-
and off by commenting out.
384+
The use of decorators eases debugging: they are readily turned on and off by
385+
commenting out.
304386

305387
The Pico was chosen for extremely low cost. It has plenty of GPIO pins and no
306-
underlying OS to introduce timing uncertainties.
388+
underlying OS to introduce timing uncertainties. The PIO enables a simple SPI
389+
slave.
307390

308391
Symbols transmitted by the UART are printable ASCII characters to ease
309392
debugging. A single byte protocol simplifies and speeds the Pico code.
@@ -313,5 +396,42 @@ fast in the context of uasyncio). It also ensures that tasks like `hog_detect`,
313396
which can be scheduled at a high rate, can't overflow the UART buffer. The
314397
1Mbps rate seems widely supported.
315398

399+
## 6.1 How it works
400+
401+
This is for anyone wanting to modify the code. Each ident is associated with
402+
two bytes, `0x40 + ident` and `0x60 + ident`. These are upper and lower case
403+
printable ASCII characters (aside from ident 0 which is `@` and the backtick
404+
character). When an ident becomes active (e.g. at the start of a coroutine),
405+
uppercase is transmitted, when it becomes inactive lowercase is sent.
406+
407+
The Pico maintains a list `pins` indexed by `ident`. Each entry is a 3-list
408+
comprising:
409+
* The `Pin` object associated with that ident.
410+
* An instance counter.
411+
* A `verbose` boolean defaulting `False`.
412+
413+
When a character arrives, the `ident` value is recovered. If it is uppercase
414+
the pin goes high and the instance count is incremented. If it is lowercase the
415+
instance count is decremented: if it becomes 0 the pin goes low.
416+
417+
The `init` function on the host sends `b"z"` to the Pico. This clears down the
418+
instance counters (the program under test may have previously failed, leaving
419+
instance counters non-zero). The Pico also clears variables used to measure
420+
hogging. In the case of SPI communication, before sending the `b"z"`, a 0
421+
character is sent with `cs/` high. The Pico implements a basic SPI slave using
422+
the PIO. This may have been left in an invalid state by a crashing host. It is
423+
designed to reset to a known state if it receives a character with `cs/` high.
424+
425+
The ident `@` (0x40) is assumed to be used by the `hog_detect()` function. When
426+
the Pico receives it, processing occurs to aid in hog detection and creating a
427+
trigger on GPIO28. Behaviour depends on the mode passed to the `run()` command.
428+
In the following, `thresh` is the time passed to `run()` in `period[0]`.
429+
* `SOON` This retriggers a timer with period `thresh`. Timeout causes a
430+
trigger.
431+
* `LATE` Trigger occurs if the period since the last `@` exceeds `thresh`.
432+
* `MAX` Trigger occurs if period exceeds `thresh` and also exceeds the prior
433+
maximum.
434+
316435
This project was inspired by
317436
[this GitHub thread](https://github.com/micropython/micropython/issues/7456).
437+

0 commit comments

Comments
 (0)