diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 7927f0d..c9c37ed 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -170,10 +170,26 @@ hardware I/O — host testing). cursor-measured **6.4 % vs 6.3 % commanded** (PWMSAR=8) — clean pulse, no runts, carrier stable across the full range. Matches the register-level audit numbers (divider 13 × 127 counts, 39.98 kHz). - - **≤1-tick laser drop at underrun/end-of-data: OPEN** — needs a - stream-path scope session (power bytes + bit-4 FIRE toggling with - the latch locked: FIRE is observable while LASER_ON stays gated - off by the hardware AND). + - **Stream-path power bytes: PASSED 2026-08-02** (scope on + LASER_PWM, `scripts/bench/pwm_stream_test.py`: power-bytes-only + program preloaded and played by the pulse engine; steppers + energized but motor_lock=15 + zero step bits — position counters + pinned at 0). Operator observed the full staircase AND both + contract rules on the pin: **run-start duty reset to 100%** + (first pulses would fire at full power unless the stream's first + power byte precedes its first FIRE bit) and **consecutive power + bytes dropped** (saw 25 % where a 75 % byte rode directly behind; + 75 % applied only after a spacer). Also measured: **duty persists + after end-of-data** (PWMSAR retains the last value; the end-of-data + backstop forces FIRE/step lines low, not the power setpoint) — the + laser-off guarantee rests entirely on FIRE. + - **≤1-tick FIRE drop at underrun/end-of-data: OPEN** — needs a + scope on the FIRE net (bit-4 toggling with the latch locked: FIRE + is observable while LASER_ON stays gated off by the hardware AND). + Signal naming (per the OpenGlow LASER SAFING sheet): FIRE = the + CPU's per-tick request (kernel attr `laser_enable`); OK_2_FIRE = + chain verdict; LASER_ON = FIRE∧OK_2_FIRE to the tube (active low); + HV_EN = HV supply enable, driven by the safing hardware only. - **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/pwm_stream_test.py b/scripts/bench/pwm_stream_test.py new file mode 100644 index 0000000..6e2aae8 --- /dev/null +++ b/scripts/bench/pwm_stream_test.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""LASER_PWM stream-path test - runs ON the board. + +Streams POWER BYTES ONLY (bit 7 set) through /dev/glowforge and lets the +kernel pulse engine play them, so the scope on LASER_PWM verifies the +real power path the laser milestone will use, including two contract +rules (run-start duty reset; consecutive power bytes dropped). + +Safety posture: +- The stream contains ZERO step bytes and ZERO FIRE (bit 4) bits. +- motor_lock=15 masks step output on all axes regardless. +- laser_latch re-asserted locked; lid closed; HV watchdog untouched. +- Position counters compared before/after - must be identical. +- streaming stays 0: end-of-data is a normal completion. +""" +import fcntl, mmap, os, struct, time + +TICK_HZ = 10000 +PWM2_BASE = 0x02084000 + + +def wr(attr, val): + with open('/sys/glowforge/' + attr, 'w') as f: + f.write(str(val)) + + +def rd(attr): + with open('/sys/glowforge/' + attr) as f: + return f.read().strip() + + +def rd_pos(): + with open('/sys/glowforge/cnc/position', 'rb') as f: + raw = f.read(32) + x, y, z, done, total = struct.unpack('<5i', raw[:20]) + return x, y, z, done, total + + +def pwmsar(): + with open('/dev/mem', 'rb') as f: + m = mmap.mmap(f.fileno(), 4096, mmap.MAP_SHARED, mmap.PROT_READ, + offset=PWM2_BASE) + sar = struct.unpack('