Skip to content

Commit d9b47cc

Browse files
committed
monitor: Fixes and improvements to synchronous monitoring.
1 parent a87bda1 commit d9b47cc

File tree

8 files changed

+105
-74
lines changed

8 files changed

+105
-74
lines changed

v3/as_demos/monitor/README.md

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ timer may be adjusted. Other modes of hog detection are also supported. See
184184

185185
Re-using idents would lead to confusing behaviour. If an ident is out of range
186186
or is assigned to more than one coroutine an error message is printed and
187-
execution terminates.
187+
execution terminates. See [section 7](./README.md#7-validation) for a special
188+
case where validation must be defeated.
188189

189190
# 2. Monitoring synchronous code
190191

@@ -193,17 +194,10 @@ timing of synchronous code, or simply to create a trigger pulse at one or more
193194
known points in the code. The following are provided:
194195
* A `sync` decorator for synchronous functions or methods: like `async` it
195196
monitors every call to the function.
196-
* A `trigger` function which issues a brief pulse on the Pico.
197197
* A `mon_call` context manager enables function monitoring to be restricted to
198198
specific calls.
199-
200-
Idents used by `trigger` or `mon_call` must be reserved: this is because these
201-
may occur in a looping construct. This enables the validation to protect
202-
against inadvertent multiple usage of an ident. The `monitor.reserve()`
203-
function can reserve one or more idents:
204-
```python
205-
monitor.reserve(4, 9, 10)
206-
```
199+
* A `trigger` function which issues a brief pulse on the Pico or can set and
200+
clear the pin on demand.
207201

208202
## 2.1 The sync decorator
209203

@@ -215,15 +209,16 @@ duration of every call to `sync_func()`:
215209
def sync_func():
216210
pass
217211
```
218-
Note that idents used by decorators must not be reserved.
219212

220213
## 2.2 The mon_call context manager
221214

222215
This may be used to monitor a function only when called from specific points in
223-
the code.
224-
```python
225-
monitor.reserve(22)
216+
the code. Validation of idents is looser here because a context manager is
217+
often used in a looping construct: it seems impractical to distinguish this
218+
case from that where two context managers are instantiated with the same ID.
226219

220+
Usage:
221+
```python
227222
def another_sync_func():
228223
pass
229224

@@ -236,14 +231,23 @@ It is advisable not to use the context manager with a function having the
236231

237232
## 2.3 The trigger timing marker
238233

239-
A call to `monitor.trigger(n)` may be inserted anywhere in synchronous or
240-
asynchronous code. When this runs, a brief (~80μs) pulse will occur on the Pico
241-
pin with ident `n`. As per `mon_call`, ident `n` must be reserved.
234+
The `trigger` closure is intended for timing blocks of code. A closure instance
235+
is created by passing the ident. If the instance is run with no args a brief
236+
(~80μs) pulse will occur on the Pico pin. If `True` is passed, the pin will go
237+
high until `False` is passed.
238+
239+
The closure should be instantiated once only. If instantiated in a loop the
240+
ident will fail the check on re-use.
242241
```python
243-
monitor.reserve(10)
242+
trig = monitor.trigger(10) # Associate trig with ident 10.
244243

245244
def foo():
246-
monitor.trigger(10) # Pulse ident 10, GPIO 13
245+
trig() # Pulse ident 10, GPIO 13
246+
247+
def bar():
248+
trig(True) # set pin high
249+
# code omitted
250+
trig(False) # set pin low
247251
```
248252

249253
# 3. Pico Pin mapping
@@ -443,7 +447,7 @@ the pin goes high and the instance count is incremented. If it is lowercase the
443447
instance count is decremented: if it becomes 0 the pin goes low.
444448

445449
The `init` function on the host sends `b"z"` to the Pico. This sets each pin
446-
int `pins` low and clears its instance counter (the program under test may have
450+
in `pins` low and clears its instance counter (the program under test may have
447451
previously failed, leaving instance counters non-zero). The Pico also clears
448452
variables used to measure hogging. In the case of SPI communication, before
449453
sending the `b"z"`, a 0 character is sent with `cs/` high. The Pico implements
@@ -465,3 +469,22 @@ In the following, `thresh` is the time passed to `run()` in `period[0]`.
465469
This project was inspired by
466470
[this GitHub thread](https://github.com/micropython/micropython/issues/7456).
467471

472+
# 7. Validation
473+
474+
The `monitor` module attempts to protect against inadvertent multiple use of an
475+
`ident`. There are use patterns which are incompatible with this, notably where
476+
a decorated function or coroutine is instantiated in a looping construct. To
477+
cater for such cases validation can be defeated. This is done by issuing:
478+
```python
479+
import monitor
480+
monitor.validation(False)
481+
```
482+
483+
# 8. A hardware implementation
484+
485+
The device under test is on the right, linked to the Pico board by means of a
486+
UART.
487+
488+
![Image](./monitor_hw.jpg)
489+
490+
I can supply a schematic and PCB details if anyone is interested.

v3/as_demos/monitor/monitor.py

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -44,36 +44,25 @@ def clear_sm(): # Set Pico SM to its initial state
4444
else:
4545
_quit("set_device: invalid args.")
4646

47-
4847
# Justification for validation even when decorating a method
4948
# /mnt/qnap2/data/Projects/Python/AssortedTechniques/decorators
5049
_available = set(range(0, 22)) # Valid idents are 0..21
51-
_reserved = set() # Idents reserved for synchronous monitoring
52-
50+
_do_validate = True
5351

5452
def _validate(ident, num=1):
55-
if ident >= 0 and ident + num < 22:
56-
try:
57-
for x in range(ident, ident + num):
58-
_available.remove(x)
59-
except KeyError:
60-
_quit("error - ident {:02d} already allocated.".format(x))
61-
else:
62-
_quit("error - ident {:02d} out of range.".format(ident))
63-
64-
65-
# Reserve ID's to be used for synchronous monitoring
66-
def reserve(*ids):
67-
for ident in ids:
68-
_validate(ident)
69-
_reserved.add(ident)
70-
71-
72-
# Check whether a synchronous ident was reserved
73-
def _check(ident):
74-
if ident not in _reserved:
75-
_quit("error: synchronous ident {:02d} was not reserved.".format(ident))
53+
if _do_validate:
54+
if ident >= 0 and ident + num < 22:
55+
try:
56+
for x in range(ident, ident + num):
57+
_available.remove(x)
58+
except KeyError:
59+
_quit("error - ident {:02d} already allocated.".format(x))
60+
else:
61+
_quit("error - ident {:02d} out of range.".format(ident))
7662

63+
def validation(do=True):
64+
global _do_validate
65+
_do_validate = do
7766

7867
# asynchronous monitor
7968
def asyn(n, max_instances=1, verbose=True):
@@ -104,22 +93,19 @@ async def wrapped_coro(*args, **kwargs):
10493

10594
return decorator
10695

107-
10896
# If SPI, clears the state machine in case prior test resulted in the DUT
10997
# crashing. It does this by sending a byte with CS\ False (high).
11098
def init():
11199
_ifrst() # Reset interface. Does nothing if UART.
112100
_write(b"z") # Clear Pico's instance counters etc.
113101

114-
115102
# Optionally run this to show up periods of blocking behaviour
116103
async def hog_detect(s=(b"\x40", b"\x60")):
117104
while True:
118105
for v in s:
119106
_write(v)
120107
await asyncio.sleep_ms(0)
121108

122-
123109
# Monitor a synchronous function definition
124110
def sync(n):
125111
def decorator(func):
@@ -137,12 +123,14 @@ def wrapped_func(*args, **kwargs):
137123

138124
return decorator
139125

140-
141-
# Runtime monitoring: can't validate because code may be looping.
142-
# Monitor a synchronous function call
126+
# Monitor a function call
143127
class mon_call:
128+
_cm_idents = set() # Idents used by this CM
129+
144130
def __init__(self, n):
145-
_check(n)
131+
if n not in self._cm_idents: # ID can't clash with other objects
132+
_validate(n) # but could have two CM's with same ident
133+
self._cm_idents.add(n)
146134
self.vstart = int.to_bytes(0x40 + n, 1, "big")
147135
self.vend = int.to_bytes(0x60 + n, 1, "big")
148136

@@ -154,10 +142,17 @@ def __exit__(self, type, value, traceback):
154142
_write(self.vend)
155143
return False # Don't silence exceptions
156144

157-
158-
# Cause pico ident n to produce a brief (~80μs) pulse
145+
# Either cause pico ident n to produce a brief (~80μs) pulse or turn it
146+
# on or off on demand.
159147
def trigger(n):
160-
_check(n)
161-
_write(int.to_bytes(0x40 + n, 1, "big"))
162-
sleep_us(20)
163-
_write(int.to_bytes(0x60 + n, 1, "big"))
148+
_validate(n)
149+
on = int.to_bytes(0x40 + n, 1, "big")
150+
off = int.to_bytes(0x60 + n, 1, "big")
151+
def wrapped(state=None):
152+
if state is None:
153+
_write(on)
154+
sleep_us(20)
155+
_write(off)
156+
else:
157+
_write(on if state else off)
158+
return wrapped

v3/as_demos/monitor/monitor_hw.JPG

254 KB
Loading

v3/as_demos/monitor/monitor_pico.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,13 @@ def _cb(_):
8383
SOON = const(0)
8484
LATE = const(1)
8585
MAX = const(2)
86+
WIDTH = const(3)
8687
# Modes. Pulses and reports only occur if an outage exceeds the threshold.
8788
# SOON: pulse early when timer times out. Report at outage end.
8889
# LATE: pulse when outage ends. Report at outage end.
8990
# MAX: pulse when outage exceeds prior maximum. Report only in that instance.
91+
# WIDTH: for measuring time between arbitrary points in code. When duration
92+
# between 0x40 and 0x60 exceeds previosu max, pulse and report.
9093

9194
# native reduced latency to 10μs but killed the hog detector: timer never timed out.
9295
# Also locked up Pico so ctrl-c did not interrupt.
@@ -121,23 +124,33 @@ def read():
121124

122125
vb and print("Awaiting communication.")
123126
h_max = 0 # Max hog duration (ms)
124-
h_start = 0 # Absolute hog start time
127+
h_start = -1 # Absolute hog start time: invalidate.
125128
while True:
126129
if x := read(): # Get an initial 0 on UART
130+
tarr = ticks_ms() # Arrival time
127131
if x == 0x7A: # Init: program under test has restarted
128132
vb and print("Got communication.")
129133
h_max = 0 # Restart timing
130-
h_start = 0
134+
h_start = -1
131135
for pin in pins:
132136
pin[0](0) # Clear pin
133137
pin[1] = 0 # and instance counter
134138
continue
135-
if x == 0x40: # hog_detect task has started.
136-
t = ticks_ms() # Arrival time
139+
if mode == WIDTH:
140+
if x == 0x40: # Leading edge on ident 0
141+
h_start = tarr
142+
elif x == 0x60 and h_start != -1: # Trailing edge
143+
dt = ticks_diff(tarr, h_start)
144+
if dt > h_max:
145+
h_max = dt
146+
print(f"Max width {dt}ms")
147+
pin_t(1)
148+
pin_t(0)
149+
elif x == 0x40: # hog_detect task has started.
137150
if mode == SOON: # Pulse on absence of activity
138151
tim.init(period=t_ms, mode=Timer.ONE_SHOT, callback=_cb)
139-
if h_start: # There was a prior trigger
140-
dt = ticks_diff(t, h_start)
152+
if h_start != -1: # There was a prior trigger
153+
dt = ticks_diff(tarr, h_start)
141154
if dt > t_ms: # Delay exceeds threshold
142155
if mode != MAX:
143156
print(f"Hog {dt}ms")
@@ -150,10 +163,11 @@ def read():
150163
if mode == MAX:
151164
pin_t(1)
152165
pin_t(0)
153-
h_start = t
166+
h_start = tarr
154167
p = pins[x & 0x1F] # Key: 0x40 (ord('@')) is pin ID 0
155168
if x & 0x20: # Going down
156-
p[1] -= 1
169+
if p[1] > 0: # Might have restarted this script with a running client.
170+
p[1] -= 1 # or might have sent trig(False) before True.
157171
if not p[1]: # Instance count is zero
158172
p[0](0)
159173
else:

v3/as_demos/monitor/tests/full_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from machine import Pin, UART, SPI
1010
import monitor
1111

12-
monitor.reserve(4)
12+
trig = monitor.trigger(4)
1313
# Define interface to use
1414
monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz
1515
#monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz
@@ -24,7 +24,7 @@ async def main():
2424
monitor.init()
2525
asyncio.create_task(monitor.hog_detect()) # Watch for gc dropouts on ID0
2626
while True:
27-
monitor.trigger(4)
27+
trig()
2828
try:
2929
await asyncio.wait_for_ms(forever(), 100) # 100ms pulse on ID1
3030
except asyncio.TimeoutError: # Mandatory error trapping

v3/as_demos/monitor/tests/latency.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
# Pin on host: modify for other platforms
1313
test_pin = Pin('X6', Pin.OUT)
14-
monitor.reserve(2)
14+
trig = monitor.trigger(2)
1515

1616
# Define interface to use
1717
monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz
@@ -21,7 +21,7 @@
2121
async def pulse(pin):
2222
pin(1) # Pulse pin
2323
pin(0)
24-
monitor.trigger(2) # Pulse Pico pin ident 2
24+
trig() # Pulse Pico pin ident 2
2525
await asyncio.sleep_ms(30)
2626

2727
async def main():

v3/as_demos/monitor/tests/quick_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz
1414
# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz
1515

16-
monitor.reserve(4) # ident for trigger
16+
trig = monitor.trigger(4)
1717

1818
@monitor.asyn(1)
1919
async def foo(t):
@@ -22,7 +22,7 @@ async def foo(t):
2222
@monitor.asyn(2)
2323
async def hog():
2424
await asyncio.sleep(5)
25-
monitor.trigger(4) # Hog start
25+
trig() # Hog start
2626
time.sleep_ms(500)
2727

2828
@monitor.asyn(3)

v3/as_demos/monitor/tests/syn_test.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
monitor.set_device(UART(2, 1_000_000)) # UART must be 1MHz
1414
# monitor.set_device(SPI(2, baudrate=5_000_000), Pin('X1', Pin.OUT)) # SPI suggest >= 1MHz
1515

16-
monitor.reserve(4, 5) # Reserve trigger and mon_call idents only
17-
1816

1917
class Foo:
2018
def __init__(self):
@@ -41,8 +39,9 @@ async def main():
4139
asyncio.create_task(monitor.hog_detect()) # Make 10ms waitx gaps visible
4240
foo1 = Foo()
4341
foo2 = Foo()
42+
trig = monitor.trigger(5)
4443
while True:
45-
monitor.trigger(5) # Mark start with pulse on ident 5
44+
trig() # Mark start with pulse on ident 5
4645
# Create two instances of .pause separated by 50ms
4746
asyncio.create_task(foo1.pause())
4847
await asyncio.sleep_ms(50)

0 commit comments

Comments
 (0)