Files
forgefirm/scripts/bench/cp_watchdog_timing.py
T

178 lines
7.1 KiB
Python

#!/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 loop with a ~100 us sleep per pass, 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. The loop must not hog the CPU: only the
controller's shipper thread is SCHED_FIFO, its protocol/producer thread is
SCHED_OTHER, and a busy loop starves it so a run never ends.
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: 14 s, jogs "$J=G91 X5 F300" "$J=G91 X-5 F300" (out and back,
5 mm, head ends where it started), sent to the local grblHAL on
127.0.0.1:23 at t = 2 s and 6 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, 4.0
SLEEP_S = 0.0001
DEFAULT_JOGS = ['$J=G91 X5 F300', '$J=G91 X-5 F300']
def main():
dur = float(sys.argv[1]) if len(sys.argv) > 1 else 14.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
def cnc_state():
try:
with open('/sys/glowforge/cnc/state') as f:
return f.read().strip()
except OSError:
return '?'
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
events.append((time.monotonic_ns(), 'cnc/state=' + cnc_state()))
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
time.sleep(SLEEP_S)
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())