Skip to content

Commit 58a2bf2

Browse files
committed
monitor: add SPI interface support.
1 parent 3e8a1e8 commit 58a2bf2

File tree

6 files changed

+195
-97
lines changed

6 files changed

+195
-97
lines changed

v3/as_demos/monitor/README.md

Lines changed: 96 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,70 @@
33
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
6-
statements. Communication with the Pico is uni-directional via a UART so only a
7-
single GPIO pin is used - at last a use for the ESP8266 transmit-only UART(1).
6+
statements. A logic analyser or scope provides an insight into the way an
7+
asynchronous application is working.
88

9-
A logic analyser or scope provides an insight into the way an asynchronous
10-
application is working.
9+
Communication with the Pico may be by UART or SPI, and is uni-directional from
10+
system under test to Pico. If a UART is used only one GPIO pin is used; at last
11+
a use for the ESP8266 transmit-only UART(1). SPI requires three - mosi, sck and
12+
cs/.
1113

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

1921
The following image shows the `quick_test.py` code being monitored at the point
2022
when a task hogs the CPU. The top line 00 shows the "hog detect" trigger. Line
2123
02 shows the fast running `hog_detect` task which cannot run at the time of the
22-
trigger. Lines 01 and 03 show the `foo` and `bar` tasks.
24+
trigger because another task is hogging the CPU. Lines 01 and 03 show the `foo`
25+
and `bar` tasks.
2326
![Image](./monitor.jpg)
2427

28+
### Breaking changes to support SPI
29+
30+
The `set_uart` method is replaced by `set_device`. Pin mappings on the Pico
31+
have changed.
32+
2533
## 1.1 Pre-requisites
2634

2735
The device being monitored must run firmware V1.17 or later. The `uasyncio`
28-
version should be V3 (as included in the firmware).
36+
version should be V3 (included in the firmware).
2937

3038
## 1.2 Usage
3139

32-
Example script `quick_test.py` provides a usage example.
40+
Example script `quick_test.py` provides a usage example. It may be adapted to
41+
use a UART or SPI interface: see commented-out code.
42+
43+
### 1.2.1 Interface selection set_device()
3344

34-
An application to be monitored typically has the following setup code:
45+
An application to be monitored needs setup code to initialise the interface.
46+
This comprises a call to `monitor.set_device` with an initialised UART or SPI
47+
device. The Pico must be set up to match the interface chosen on the host: see
48+
[section 4](./README.md#4-the-pico-code).
49+
50+
In the case of a UART an initialised UART with 1MHz baudrate is passed:
51+
```python
52+
from machine import UART
53+
from monitor import monitor, monitor_init, hog_detect, set_device
54+
set_device(UART(2, 1_000_000)) # Baudrate MUST be 1MHz.
55+
```
56+
In the case of SPI initialised SPI and cs/ Pin instances are passed:
3557
```python
36-
from monitor import monitor, monitor_init, hog_detect, set_uart
37-
set_uart(2) # Define device under test UART no.
58+
from machine import Pin, SPI
59+
from monitor import monitor, monitor_init, hog_detect, set_device
60+
set_device(SPI(2, baudrate=5_000_000), Pin('X6', Pin.OUT)) # Device under test SPI
3861
```
39-
On application start it should issue
62+
The SPI instance must have default args; the one exception being baudrate which
63+
may be any value. I have tested up to 30MHz but there is no benefit in running
64+
above 1MHz. Hard or soft SPI may be used. It should be possible to share the
65+
bus with other devices, although I haven't tested this.
66+
67+
### 1.2.2 Monitoring
68+
69+
On startup, after defining the interface, an application should issue:
4070
```python
4171
monitor_init()
4272
```
@@ -52,14 +82,14 @@ The decorator args are as follows:
5282
2. An optional arg defining the maximum number of concurrent instances of the
5383
task to be independently monitored (default 1).
5484

55-
Whenever the code runs, a pin on the Pico will go high, and when the code
85+
Whenever the coroutine runs, a pin on the Pico will go high, and when the code
5686
terminates it will go low. This enables the behaviour of the system to be
5787
viewed on a logic analyser or via console output on the Pico. This behavior
5888
works whether the code terminates normally, is cancelled or has a timeout.
5989

6090
In the example above, when `my_coro` starts, the pin defined by `ident==2`
61-
(GPIO 4) will go high. When it ends, the pin will go low. If, while it is
62-
running, a second instance of `my_coro` is launched, the next pin (GPIO 5) will
91+
(GPIO 5) will go high. When it ends, the pin will go low. If, while it is
92+
running, a second instance of `my_coro` is launched, the next pin (GPIO 6) will
6393
go high. Pins will go low when the relevant instance terminates, is cancelled,
6494
or times out. If more instances are started than were specified to the
6595
decorator, a warning will be printed on the host. All excess instances will be
@@ -87,9 +117,9 @@ will cause the pin to go high for 30s, even though the task is consuming no
87117
resources for that period.
88118

89119
To provide a clue about CPU hogging, a `hog_detect` coroutine is provided. This
90-
has `ident=0` and, if used, is monitored on GPIO 2. It loops, yielding to the
120+
has `ident=0` and, if used, is monitored on GPIO 3. It loops, yielding to the
91121
scheduler. It will therefore be scheduled in round-robin fashion at speed. If
92-
long gaps appear in the pulses on GPIO 2, other tasks are hogging the CPU.
122+
long gaps appear in the pulses on GPIO 3, other tasks are hogging the CPU.
93123
Usage of this is optional. To use, issue
94124
```python
95125
import uasyncio as asyncio
@@ -139,55 +169,61 @@ It is advisable not to use the context manager with a function having the
139169

140170
# 3. Pico Pin mapping
141171

142-
The Pico GPIO numbers start at 2 to allow for UART(0) and also have a gap where
143-
GPIO's are used for particular purposes. This is the mapping between `ident`
144-
GPIO no. and Pico PCB pin, with the pins for the timer and the UART link also
172+
The Pico GPIO numbers used by idents start at 3 and have a gap where the Pico
173+
uses GPIO's for particular purposes. This is the mapping between `ident` GPIO
174+
no. and Pico PCB pin. Pins for the timer and the UART/SPI link are also
145175
identified:
146176

147-
| ident | GPIO | pin |
148-
|:-----:|:----:|:----:|
149-
| uart | 1 | 2 |
150-
| 0 | 2 | 4 |
151-
| 1 | 3 | 5 |
152-
| 2 | 4 | 6 |
153-
| 3 | 5 | 7 |
154-
| 4 | 6 | 9 |
155-
| 5 | 7 | 10 |
156-
| 6 | 8 | 11 |
157-
| 7 | 9 | 12 |
158-
| 8 | 10 | 14 |
159-
| 9 | 11 | 15 |
160-
| 10 | 12 | 16 |
161-
| 11 | 13 | 17 |
162-
| 12 | 14 | 19 |
163-
| 13 | 15 | 20 |
164-
| 14 | 16 | 21 |
165-
| 15 | 17 | 22 |
166-
| 16 | 18 | 24 |
167-
| 17 | 19 | 25 |
168-
| 18 | 20 | 26 |
169-
| 19 | 21 | 27 |
170-
| 20 | 22 | 29 |
171-
| 21 | 26 | 31 |
172-
| 22 | 27 | 32 |
173-
| timer | 28 | 34 |
174-
175-
The host's UART `txd` pin should be connected to Pico GPIO 1 (pin 2). There
176-
must be a link between `Gnd` pins on the host and Pico.
177+
| ident | GPIO | pin |
178+
|:-------:|:----:|:----:|
179+
| nc/mosi | 0 | 1 |
180+
| rxd/sck | 1 | 2 |
181+
| nc/cs/ | 2 | 4 |
182+
| 0 | 3 | 5 |
183+
| 1 | 4 | 6 |
184+
| 2 | 5 | 7 |
185+
| 3 | 6 | 9 |
186+
| 4 | 7 | 10 |
187+
| 5 | 8 | 11 |
188+
| 6 | 9 | 12 |
189+
| 7 | 10 | 14 |
190+
| 8 | 11 | 15 |
191+
| 9 | 12 | 16 |
192+
| 10 | 13 | 17 |
193+
| 11 | 14 | 19 |
194+
| 12 | 15 | 20 |
195+
| 13 | 16 | 21 |
196+
| 14 | 17 | 22 |
197+
| 15 | 18 | 24 |
198+
| 16 | 19 | 25 |
199+
| 17 | 20 | 26 |
200+
| 18 | 21 | 27 |
201+
| 19 | 22 | 29 |
202+
| 20 | 26 | 31 |
203+
| 21 | 27 | 32 |
204+
| timer | 28 | 34 |
205+
206+
For a UART interface the host's UART `txd` pin should be connected to Pico GPIO
207+
1 (pin 2).
208+
209+
For SPI the host's `mosi` goes to GPIO 0 (pin 1), and `sck` to GPIO 1 (pin 2).
210+
The host's CS Pin is connected to GPIO 2 (pin 4).
211+
212+
There must be a link between `Gnd` pins on the host and Pico.
177213

178214
# 4. The Pico code
179215

180-
Monitoring of the UART with default behaviour is started as follows:
216+
Monitoring via the UART with default behaviour is started as follows:
181217
```python
182218
from monitor_pico import run
183219
run()
184220
```
185221
By default the Pico does not produce console output and the timer has a period
186222
of 100ms - pin 28 will pulse if ident 0 is inactive for over 100ms. These
187223
behaviours can be modified by the following `run` args:
188-
1. `period=100` Define the timer period in ms.
224+
1. `period=100` Define the hog_detect timer period in ms.
189225
2. `verbose=()` Determines which `ident` values should produce console output.
190-
3. `device="uart"` Provides for future use of other interfaces.
226+
3. `device="uart"` Set to "spi" for an SPI interface.
191227

192228
Thus to run such that idents 4 and 7 produce console output, with hogging
193229
reported if blocking is for more than 60ms, issue
@@ -198,10 +234,12 @@ run(60, (4, 7))
198234

199235
# 5. Performance and design notes
200236

201-
The latency between a monitored coroutine starting to run and the Pico pin
202-
going high is about 20μs. This isn't as absurd as it sounds: theoretically the
203-
latency could be negative as the effect of the decorator is to send the
204-
character before the coroutine starts.
237+
Using a UART the latency between a monitored coroutine starting to run and the
238+
Pico pin going high is about 23μs. With SPI I measured -12μs. This isn't as
239+
absurd as it sounds: a negative latency is the effect of the decorator which
240+
sends the character before the coroutine starts. These values are small in the
241+
context of `uasyncio`: scheduling delays are on the order of 150μs or greater
242+
depending on the platform. See `quick_test.py` for a way to measure latency.
205243

206244
The use of decorators is intended to ease debugging: they are readily turned on
207245
and off by commenting out.
@@ -219,11 +257,3 @@ which can be scheduled at a high rate, can't overflow the UART buffer. The
219257

220258
This project was inspired by
221259
[this GitHub thread](https://github.com/micropython/micropython/issues/7456).
222-
223-
# 6. Work in progress
224-
225-
It is intended to add an option for SPI communication; `monitor.py` has a
226-
`set_device` method which can be passed an instance of an initialised SPI
227-
object. The Pico `run` method will be able to take a `device="spi"` arg which
228-
will expect an SPI connection on pins 0 (sck) and 1 (data). This requires a
229-
limited implementation of an SPI slave using the PIO, which I will do soon.

v3/as_demos/monitor/monitor.py

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,37 @@
55
# Released under the MIT License (MIT) - see LICENSE file
66

77
import uasyncio as asyncio
8-
from machine import UART
8+
from machine import UART, SPI, Pin
99

10-
device = None
11-
def set_uart(n): # Monitored app defines interface
12-
global device
13-
device = UART(n, 1_000_000)
10+
_write = lambda _ : print('Must run set_device')
11+
_dummy = lambda : None # If UART do nothing.
1412

15-
# For future use with SPI
16-
# Pass initialised instance of some device
17-
def set_device(dev):
18-
global device
19-
device = dev
13+
# For UART pass initialised UART. Baudrate must be 1_000_000.
14+
# For SPI pass initialised instance SPI. Can be any baudrate, but
15+
# must be default in other respects.
16+
def set_device(dev, cspin=None):
17+
global _write
18+
global _dummy
19+
if isinstance(dev, UART) and cspin is None: # UART
20+
_write = dev.write
21+
elif isinstance(dev, SPI) and isinstance(cspin, Pin):
22+
cspin(1)
23+
def spiwrite(data):
24+
cspin(0)
25+
dev.write(data)
26+
cspin(1)
27+
_write = spiwrite
28+
def clear_sm(): # Set Pico SM to its initial state
29+
cspin(1)
30+
dev.write(b'\0') # SM is now waiting for CS low.
31+
_dummy = clear_sm
32+
else:
33+
print('set_device: invalid args.')
2034

21-
_available = set(range(0, 23)) # Valid idents are 0..22
35+
_available = set(range(0, 22)) # Valid idents are 0..21
2236

2337
def _validate(ident, num=1):
24-
if ident >= 0 and ident + num <= 23:
38+
if ident >= 0 and ident + num < 22:
2539
try:
2640
for x in range(ident, ident + num):
2741
_available.remove(x)
@@ -44,22 +58,25 @@ async def wrapped_coro(*args, **kwargs):
4458
instance += 1
4559
if instance > max_instances:
4660
print(f'Monitor {n:02} max_instances reached')
47-
device.write(v)
61+
_write(v)
4862
try:
4963
res = await coro(*args, **kwargs)
5064
except asyncio.CancelledError:
5165
raise
5266
finally:
5367
d |= 0x20
5468
v = bytes(chr(d), 'utf8')
55-
device.write(v)
69+
_write(v)
5670
instance -= 1
5771
return res
5872
return wrapped_coro
5973
return decorator
6074

75+
# If SPI, clears the state machine in case prior test resulted in the DUT
76+
# crashing. It does this by sending a byte with CS\ False (high).
6177
def monitor_init():
62-
device.write(b'z')
78+
_dummy() # Does nothing if UART
79+
_write(b'z')
6380

6481
# Optionally run this to show up periods of blocking behaviour
6582
@monitor(0)
@@ -79,9 +96,9 @@ def decorator(func):
7996
dend = 0x60 + n
8097
vend = bytes(chr(dend), 'utf8')
8198
def wrapped_func(*args, **kwargs):
82-
device.write(vstart)
99+
_write(vstart)
83100
res = func(*args, **kwargs)
84-
device.write(vend)
101+
_write(vend)
85102
return res
86103
return wrapped_func
87104
return decorator
@@ -98,9 +115,9 @@ def __init__(self, n):
98115
self.vend = bytes(chr(self.dend), 'utf8')
99116

100117
def __enter__(self):
101-
device.write(self.vstart)
118+
_write(self.vstart)
102119
return self
103120

104121
def __exit__(self, type, value, traceback):
105-
device.write(self.vend)
122+
_write(self.vend)
106123
return False # Don't silence exceptions

0 commit comments

Comments
 (0)