Skip to content

Commit d6b1e05

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

File tree

11 files changed

+367
-150
lines changed

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)
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