@@ -4,20 +4,19 @@ This library provides a means of examining the behaviour of a running
4
4
` uasyncio ` system. The device under test is linked to a Raspberry Pi Pico. The
5
5
latter displays the behaviour of the host by pin changes and/or optional print
6
6
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 .
9
9
10
10
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/ ` .
14
13
15
14
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.
21
20
22
21
The following image shows the ` quick_test.py ` code being monitored at the point
23
22
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.
29
28
![ Image] ( ./monitor.jpg )
30
29
31
30
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.
33
33
![ Image] ( ./monitor_gc.jpg )
34
34
35
35
### Status
36
36
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.
45
39
46
40
## 1.1 Pre-requisites
47
41
48
42
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.
50
45
51
46
## 1.2 Usage
52
47
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
+ ```
53
70
Example script ` quick_test.py ` provides a usage example. It may be adapted to
54
71
use a UART or SPI interface: see commented-out code.
55
72
@@ -63,14 +80,14 @@ device. The Pico must be set up to match the interface chosen on the host: see
63
80
In the case of a UART an initialised UART with 1MHz baudrate is passed:
64
81
``` python
65
82
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.
68
85
```
69
86
In the case of SPI initialised SPI and cs/ Pin instances are passed:
70
87
``` python
71
88
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
74
91
```
75
92
The SPI instance must have default args; the one exception being baudrate which
76
93
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.
81
98
82
99
On startup, after defining the interface, an application should issue:
83
100
``` python
84
- monitor_init ()
101
+ monitor.init ()
85
102
```
86
- Coroutines to be monitored are prefixed with the ` @monitor ` decorator:
103
+ Coroutines to be monitored are prefixed with the ` @monitor.asyn ` decorator:
87
104
``` python
88
- @monitor (2 , 3 )
105
+ @monitor.asyn (2 , 3 )
89
106
async def my_coro ():
90
107
# code
91
108
```
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
96
114
task to be independently monitored (default 1).
97
115
98
116
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.
122
140
The pin state only indicates that the task is running. A pin state of 1 does
123
141
not imply CPU hogging. Thus
124
142
``` python
125
- @monitor (3 )
143
+ @monitor.asyn (3 )
126
144
async def long_time ():
127
145
await asyncio.sleep(30 )
128
146
```
@@ -136,9 +154,9 @@ long gaps appear in the pulses on GPIO 3, other tasks are hogging the CPU.
136
154
Usage of this is optional. To use, issue
137
155
``` python
138
156
import uasyncio as asyncio
139
- from monitor import monitor, hog_detect
157
+ import monitor
140
158
# code omitted
141
- asyncio.create_task(hog_detect())
159
+ asyncio.create_task(monitor. hog_detect())
142
160
# code omitted
143
161
```
144
162
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
148
166
timer may be adjusted. Other modes of hog detection are also supported. See
149
167
[ section 4] ( ./README.md~4-the-pico-code ) .
150
168
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
+
151
174
# 2. Monitoring synchronous code
152
175
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
+ ```
159
192
160
- ## 2.1 The mon_func decorator
193
+ ## 2.1 The sync decorator
161
194
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() ` :
165
198
``` python
166
- @mon_func (20 )
199
+ @monitor.sync (20 )
167
200
def sync_func ():
168
201
pass
169
202
```
203
+ Note that the ident must not be reserved.
170
204
171
205
## 2.2 The mon_call context manager
172
206
173
207
This may be used to monitor a function only when called from specific points in
174
208
the code.
175
209
``` python
210
+ monitor.reserve(22 )
211
+
176
212
def another_sync_func ():
177
213
pass
178
214
179
- with mon_call(22 ):
215
+ with monitor. mon_call(22 ):
180
216
another_sync_func()
181
217
```
182
218
@@ -187,7 +223,13 @@ It is advisable not to use the context manager with a function having the
187
223
188
224
A call to ` monitor.trigger(n) ` may be inserted anywhere in synchronous or
189
225
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
+ ```
191
233
192
234
# 3. Pico Pin mapping
193
235
@@ -290,20 +332,61 @@ The mode also affects reporting. The effect of mode is as follows:
290
332
* ` MAX ` Report at end of outage but only when prior maximum exceeded. This
291
333
ensures worst-case is not missed.
292
334
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
294
376
295
377
Using a UART the latency between a monitored coroutine starting to run and the
296
378
Pico pin going high is about 23μs. With SPI I measured -12μs. This isn't as
297
379
absurd as it sounds: a negative latency is the effect of the decorator which
298
380
sends the character before the coroutine starts. These values are small in the
299
381
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.
301
383
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.
304
386
305
387
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.
307
390
308
391
Symbols transmitted by the UART are printable ASCII characters to ease
309
392
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`,
313
396
which can be scheduled at a high rate, can't overflow the UART buffer. The
314
397
1Mbps rate seems widely supported.
315
398
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
+
316
435
This project was inspired by
317
436
[ this GitHub thread] ( https://github.com/micropython/micropython/issues/7456 ) .
437
+
0 commit comments