From fe6a65b7fe6ac841f73445c70dc4d860a15149e8 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sat, 15 Aug 2026 13:49:50 -0400 Subject: [PATCH] bench: cp_watchdog_timing.py - measure the HV watchdog one-shot from the SoC pins --- scripts/bench/README.md | 1 + scripts/bench/cp_watchdog_timing.py | 169 ++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 scripts/bench/cp_watchdog_timing.py diff --git a/scripts/bench/README.md b/scripts/bench/README.md index f58d489..deb85c6 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -37,6 +37,7 @@ to go through a WSL distro from Windows). | `bump_seek.py` | Accelerometer bump-seek homing prototype (runs on the board): creeps toward a rail in bounded jog segments via grblHAL TCP, learns the moving-noise baseline per segment, detects the contact jolt (~530 Hz sampling, 2-sample confirm), jog-cancels (0x85) and backs off. CSV to /tmp/bump.csv. | | `build-feeder.sh` | Cross-compiles `feeder.c` the same way. | | `puls_profile.py` | Decodes factory `.puls` streams (raw or GF1-headered) into velocity/accel profiles: peak speeds, ramp-slope fits, per-move segments, Z cadence. Runs anywhere (stdlib only). Source of the factory-true grblHAL defaults: 700/590 mm/s² accel, 200 mm/s max rate, 28160 Hz travel tick. | +| `cp_watchdog_timing.py` | HV charge-pump watchdog one-shot timing (runs on the board): latches every CHG_PUMP feed pulse in GPIO3's edge detector (pin 24 only, IMR untouched, ICR2 restored on exit) and polls the `!Q` (`charge_pump_alive`) and `!HV_ENABLE` (`hv_enable`) pads through /dev/mem in a tight loop while it commands short local jogs; prints per-run t_w (last pulse → Q fall), Q → HV_ENABLE delay, priming latency and the feed period, with the loop's worst gap as the resolution. Motion only, laser locked, no other Grbl client attached. | | `bench_m2.py` | Motion-quality bench, runs against the board over TCP:23: bounded round-trip jogs (sanity, max-rate, diagonal) + feed-hold/resume mid-move, reporting peak feed, state transitions, and position drift. | Data files kept beside the tools: `flow_matrix_results.json` / diff --git a/scripts/bench/cp_watchdog_timing.py b/scripts/bench/cp_watchdog_timing.py new file mode 100644 index 0000000..a892030 --- /dev/null +++ b/scripts/bench/cp_watchdog_timing.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Charge-pump watchdog timing (runs ON the board, as root). + +Measures the HV watchdog one-shot (U1-1, SN74AHC123A) directly from the +SoC's own pins, with no kernel change and no scope: + + CHG_PUMP GPIO3_24 the kernel's feed pulse (sub-microsecond, so it is + latched by GPIO3's edge detector, not caught live) + !Q readback GPIO1_08 inverted one-shot Q (`cnc/charge_pump_alive`) + !HV_ENABLE GPIO4_06 the `hv_enable` switch pad, inverted + +The sampler arms rising-edge detection for GPIO3 pin 24 only (ICR2 bits +17:16), clears that pin's sticky ISR flag, then polls ISR bit 24 and the two +readback pads in a tight loop, restoring ICR2 on exit. GPIO3 IMR bit 24 must +be clear (nothing has an interrupt on that pin; the run refuses otherwise), +so the kernel never sees the flag. Reads and writes go through /dev/mem; +the CHG_PUMP pad has SION set in the device tree, so its pad state is +visible. + +Every run of the pulse engine (a jog is enough) primes the pump once and +then feeds it every 200 ms while `state == running`; when the run ends the +feed stops and Q falls one one-shot period after the last pulse. Reported per +run: t_w = last latched pulse -> !Q rising (Q fell), the !HV_ENABLE edge +relative to Q, the priming latency (first pulse -> !Q falling), the pulse +count and period. Resolution = the loop period (tens of microseconds; the +worst gap is printed). + +Usage: cp_watchdog_timing.py [seconds] [jog ...] + default: 16 s, jogs "$J=G91 X5 F300" "$J=G91 X-5 F300" x2 (out and back + twice, 5 mm, head ends where it started), sent to the local grblHAL on + 127.0.0.1:23 at t = 2, 4.6, 7.2, 9.8 s. Pass "-" as the only jog to sample + without commanding motion (drive the runs yourself). +Motion only, laser locked; needs the GRBL controller idle with no other +Grbl client attached (a connection here displaces the sender). +""" +import mmap, os, socket, sys, time + +GPIO1, GPIO3, GPIO4 = 0x0209C000, 0x020A4000, 0x020A8000 +PSR, ICR2, IMR, ISR = 0x08, 0x10, 0x14, 0x18 +PULSE_PIN = 24 # GPIO3_24 CHG_PUMP +NQ_BIT, NHV_BIT = 8, 6 # GPIO1_08 !Q, GPIO4_06 !HV_ENABLE +JOG_T0, JOG_DT = 2.0, 2.6 + +DEFAULT_JOGS = ['$J=G91 X5 F300', '$J=G91 X-5 F300'] * 2 + + +def main(): + dur = float(sys.argv[1]) if len(sys.argv) > 1 else 16.0 + jogs = sys.argv[2:] if len(sys.argv) > 2 else DEFAULT_JOGS + if jogs == ['-']: + jogs = [] + + fd = os.open('/dev/mem', os.O_RDWR | os.O_SYNC) + + def M(base): + return mmap.mmap(fd, 4096, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offset=base) + + g1, g3, g4 = M(GPIO1), M(GPIO3), M(GPIO4) + + def rd(m, o): + return int.from_bytes(m[o:o + 4], 'little') + + def wr(m, o, v): + m[o:o + 4] = v.to_bytes(4, 'little') + + if rd(g3, IMR) & (1 << PULSE_PIN): + print('ABORT: GPIO3 IMR bit %d is set - something has an interrupt on the pump pin' % PULSE_PIN) + return 2 + try: + os.nice(-20) + except OSError: + pass + + sock = None + if jogs: + sock = socket.create_connection(('127.0.0.1', 23), timeout=3) + sock.settimeout(0.05) + end = time.monotonic() + 1.5 + while time.monotonic() < end: # drain the greeting + try: + sock.recv(4096) + except socket.timeout: + pass + + icr2_orig = rd(g3, ICR2) + isr_clear = (1 << PULSE_PIN).to_bytes(4, 'little') + events = [] + maxgap = n = 0 + try: + wr(g3, ICR2, (icr2_orig & ~(0x3 << 16)) | (0x2 << 16)) # pin 24: rising edge + wr(g3, ISR, 1 << PULSE_PIN) # clear the stale flag (w1c) + nq_prev = (rd(g1, PSR) >> NQ_BIT) & 1 + nhv_prev = (rd(g4, PSR) >> NHV_BIT) & 1 + t0 = time.monotonic_ns() + tend = t0 + int(dur * 1e9) + tprev = t0 + sent = 0 + events.append((t0, 'start !Q=%d !HV=%d' % (nq_prev, nhv_prev))) + while True: + t = time.monotonic_ns() + isr = g3[ISR:ISR + 4] + p1 = g1[PSR:PSR + 4] + p4 = g4[PSR:PSR + 4] + if isr[3] & 0x01: # bit 24 -> byte 3 bit 0 + g3[ISR:ISR + 4] = isr_clear + events.append((t, 'PULSE')) + nq = p1[1] & 0x01 # bit 8 -> byte 1 bit 0 + nhv = (p4[0] >> 6) & 0x01 # bit 6 -> byte 0 bit 6 + if nq != nq_prev: + events.append((t, '!Q->%d' % nq)) + nq_prev = nq + if nhv != nhv_prev: + events.append((t, '!HV->%d' % nhv)) + nhv_prev = nhv + gap = t - tprev + if gap > maxgap: + maxgap = gap + tprev = t + n += 1 + if sock and sent < len(jogs) and t - t0 > (JOG_T0 + JOG_DT * sent) * 1e9: + sock.sendall((jogs[sent] + '\n').encode()) + events.append((t, 'JOG %s' % jogs[sent])) + sent += 1 + if t > tend: + break + finally: + wr(g3, ICR2, icr2_orig) + wr(g3, ISR, 1 << PULSE_PIN) + if sock: + sock.close() + + print('samples=%d mean period=%.1f us worst gap=%.1f us' % (n, (tprev - t0) / max(n, 1) / 1e3, maxgap / 1e3)) + print() + tw, prime, hvq, periods = [], [], [], [] + last_pulse = first_pulse = q_fall = None + for t, k in events: + line = '%9.4f %s' % ((t - t0) / 1e9, k) + if k == 'PULSE': + if last_pulse is not None and first_pulse is not None and t - last_pulse < 1e9: + periods.append((t - last_pulse) / 1e6) + else: + first_pulse = t + last_pulse = t + elif k == '!Q->0' and first_pulse is not None: + prime.append((t - first_pulse) / 1e3) + line += ' (Q rose %.1f us after the priming pulse)' % prime[-1] + elif k == '!Q->1' and last_pulse is not None: + tw.append((t - last_pulse) / 1e6) + q_fall = t + line += ' t_w = %.2f ms after the last pulse' % tw[-1] + first_pulse = None + elif k == '!HV->1' and q_fall is not None: + hvq.append((t - q_fall) / 1e3) + line += ' (%.1f us after Q fell)' % hvq[-1] + print(line) + print() + if tw: + print('one-shot period t_w: n=%d mean %.2f ms min %.2f max %.2f' % (len(tw), sum(tw) / len(tw), min(tw), max(tw))) + if periods: + print('feed period: n=%d mean %.3f ms min %.3f max %.3f' % (len(periods), sum(periods) / len(periods), min(periods), max(periods))) + if prime: + print('prime -> Q high: mean %.1f us (n=%d)' % (sum(prime) / len(prime), len(prime))) + if hvq: + print('Q fall -> HV_ENABLE fall: mean %.1f us (n=%d)' % (sum(hvq) / len(hvq), len(hvq))) + return 0 + + +if __name__ == '__main__': + sys.exit(main())