From 009944a29b24c0d5c6c20dd3c5c0d2c9f7d6c061 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sun, 2 Aug 2026 17:07:05 -0400 Subject: [PATCH] FIRE drop gate PASSED both paths - all pre-live-fire scope gates closed Scope on GPIO2_IO30 (the SoC FIRE drive): two 2.000 s FIRE windows, the second terminated only by the SDMA end-of-data backstop. Measured 2.0000 s exactly with clean edges on BOTH termination paths - normal completion and true underrun (streaming=1, kernel underrun state reached and acked; fire_test.py gains the U mode). The backstop drops FIRE within one tick regardless of how the stream dies. With the waveform, stream-path, latch, topology, and interlock- semantics results from earlier today, every standing hardware verification gate for live fire is now passed; what remains is the laser-milestone software and a chain-armed first-light procedure. --- docs/BRINGUP.md | 33 ++++++++++++++++++++++----------- scripts/bench/fire_test.py | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 8131ef4..643bb8d 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -198,17 +198,28 @@ hardware I/O — host testing). (13→7 during the unlocked FIRE window): b0 = SoC-side LASER_ON monitor, active LOW (1 = not lasing); b1 = FIRE, active high; b3 = latch, 1 = locked/0 = unlocked. - - **≤1-tick FIRE drop at underrun/end-of-data: OPEN (deferred).** - The SoC FIRE net is not probe-accessible on the bench, and the - chain-gated PSU pin cannot show FIRE without arming the full - chain (HV powered + pgood + HV_WDOG retriggered). Close it later - either by finding a probe point on the FIRE net (safing-logic - input) or as the first measurement of the eventual chain-armed - laser-milestone session, with the duty-0 discipline from this one. - Signal naming (per the OpenGlow LASER SAFING sheet, now confirmed - to match the factory board): FIRE = per-tick request (kernel - `laser_enable`); OK_2_FIRE = chain verdict; LASER_ON = - FIRE∧OK_2_FIRE to the PSU; HV_EN = HV enable, safing-driven only. + - **≤1-tick FIRE drop at underrun/end-of-data: PASSED 2026-08-02** + (scope on GPIO2_IO30, the SoC FIRE drive feeding the safing + logic; `fire_test.py` B and U, operator-executed, duty 0, chain + unarmed). Stream: two 2.000 s FIRE windows, the second ending + exactly at end-of-data so its falling edge IS the SDMA backstop. + Measured: **both pulses 2.0000 s exactly, clean edges, on BOTH + termination paths** — normal completion (streaming=0) and true + underrun (streaming=1, kernel `underrun` state reached and + acked). The backstop drops FIRE within one tick (≤100 µs at + 10 kHz) regardless of how the stream dies. + Signal naming (per the OpenGlow LASER SAFING sheet, confirmed to + match the factory board): FIRE = per-tick request (kernel + `laser_enable`, GPIO2_IO30); OK_2_FIRE = chain verdict; LASER_ON + = FIRE∧OK_2_FIRE to the PSU; HV_EN = HV enable, safing-driven + only. + - **ALL STANDING SCOPE GATES ARE NOW PASSED.** Live fire remains + gated on the laser-milestone software itself (power-byte + FIRE + emission in the stream engine with power-before-fire ordering, + HV_WDOG retriggering only while genuinely cutting, M3/M4/$32 + mapping) plus a chain-armed first-light procedure; the hardware + verification prerequisites are complete. Interlock-trip recovery + behavior remains to be exercised (non-scope check). - **Interlock readback semantics cross-check: OPEN** (see factory-laser-safety-readbacks notes). 3. **Homing**: X/Y home switch GPIOs exist in the cnc pin map (unused so diff --git a/scripts/bench/fire_test.py b/scripts/bench/fire_test.py index 15d940f..4b3503d 100644 --- a/scripts/bench/fire_test.py +++ b/scripts/bench/fire_test.py @@ -1,15 +1,21 @@ #!/usr/bin/env python3 -"""FIRE-line drop-timing test - runs ON the board. Usage: fire_test.py A|B +"""FIRE-line drop-timing test - runs ON the board. Usage: fire_test.py A|B|U Stream: power(0) first byte (duty forced to ZERO before any FIRE bit - the run-start reset would otherwise leave it at 100%), then 1 s pads / 2.000 s FIRE bits / 1 s pads / 2.000 s FIRE bits ending exactly at end-of-data (the backstop edge the gate wants timed). -No step bytes; motor_lock=15; streaming=0 (normal completion). +No step bytes; motor_lock=15. -Phase A: laser latch LOCKED - expects nothing on the LASER_ON pin. -Phase B: latch UNLOCKED for the run (re-locked in finally). Refuses to -run if laser_pgood reports the HV supply good. Duty is zero throughout. +Phase A: laser latch LOCKED - expects nothing on the FIRE/LASER_ON nets. +Phase B: latch UNLOCKED for the run (re-locked in finally), streaming=0 + (end-of-data = normal completion). Refuses to run if + laser_pgood reports the HV supply good. Duty is zero throughout. +Phase U: like B but with streaming=1 declared, so the terminal + end-of-data is a TRUE UNDERRUN: the kernel lands in the + `underrun` fault state (expected - the script acks it via + stop). Same SDMA backstop, exercised through the fault path; + the scope measurement on the FIRE net is identical. """ import fcntl, os, struct, sys, time @@ -41,6 +47,8 @@ def snap(tag): mode = sys.argv[1].upper() if len(sys.argv) > 1 else 'A' +unlock = mode in ('B', 'U') +underrun_mode = mode == 'U' stream = ( bytes([0x80]) + # power = 0: duty zero before any FIRE bit @@ -52,10 +60,10 @@ stream = ( print('phase %s: stream %d bytes = %.3f s' % (mode, len(stream), len(stream) / TICK_HZ)) -if mode == 'B': +if unlock: pgood = rd('cnc/laser_pgood') if pgood != '0': - print('ABORT: laser_pgood=%s (HV supply reports good) - refusing phase B' % pgood) + print('ABORT: laser_pgood=%s (HV supply reports good) - refusing latch unlock' % pgood) sys.exit(1) snap('pre ') @@ -72,9 +80,13 @@ try: os.write(fd, stream) pos_before = rd_pos() - if mode == 'B': + if underrun_mode: + wr('cnc/streaming', 1) # end-of-data mid-run = true underrun + print('streaming=1: terminal end-of-data will be a TRUE UNDERRUN (expected)') + + if unlock: wr('cnc/laser_latch', 0) # UNLOCK for this run only - print('latch UNLOCKED for phase B run') + print('latch UNLOCKED for phase %s run' % mode) wr('cnc/run', 1) t0 = time.time() @@ -89,6 +101,14 @@ try: break time.sleep(0.1) print('done: state=%s after %.1f s' % (state, time.time() - t0)) + if underrun_mode: + if state == 'underrun': + print('underrun state reached as EXPECTED; acking via stop') + else: + print('WARNING: expected underrun state, got %s' % state) + wr('cnc/stop', 1) # ack the underrun + wr('cnc/streaming', 0) + print('acked: state=%s' % rd('cnc/state')) finally: wr('cnc/laser_latch', 1) # re-lock unconditionally fcntl.flock(fd, fcntl.LOCK_UN)