From fb956971b728a727c069356242192d8dbd001775 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Sun, 2 Aug 2026 16:08:34 -0400 Subject: [PATCH] LASER_PWM waveform gate PASSED - scope-verified on the physical pin Direct PWMSAR duty steps with the machine in the locked state (controller stopped, cnc disabled so steppers are unpowered, laser latch locked, lid closed; laser_on_sampled stayed 0 throughout). Measured on the pin: 25.0 us period / 40 kHz stable across the full duty range; 50/25/75 percent confirmed visually; low end cursor-measured 6.4 vs 6.3 percent commanded (PWMSAR=8), clean pulse. Matches the register-level audit numbers (divider 13 x 127 counts). Adds pwm_sweep.py / pwm_hold.py to the bench kit and records the two remaining pre-live-fire gates (1-tick laser drop at underrun via the stream path with FIRE observable under a locked latch; interlock readback semantics) in BRINGUP. --- docs/BRINGUP.md | 19 ++++++++-- scripts/bench/README.md | 4 ++- scripts/bench/pwm_hold.py | 24 +++++++++++++ scripts/bench/pwm_sweep.py | 74 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 scripts/bench/pwm_hold.py create mode 100644 scripts/bench/pwm_sweep.py diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 217c901..7927f0d 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -159,8 +159,23 @@ hardware I/O — host testing). decay mode. Milestone closed. 2. **Laser mapping** (gated on the scope session): spindle → power bytes (bit 7) + bit 4 laser-enable, M3/M4/$32 semantics, PWM-reset rule per - the contract. **No live fire before the standing scope gates**: LASER_PWM - waveform vs factory capture + ≤1-tick laser drop at underrun. + the contract. **No live fire before the standing scope gates.** Gate + status: + - **LASER_PWM waveform: PASSED 2026-08-02** (scope on the physical + pin). Method: direct PWMSAR duty steps (`scripts/bench/pwm_sweep.py` + / `pwm_hold.py`) with the controller stopped, cnc `disabled` + (steppers unpowered), laser latch locked, lid closed; + `laser_on_sampled` stayed 0 throughout. Measured: 25.0 µs period / + 40 kHz at every duty; 50/25/75 % confirmed visually; low end + 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). + - **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 far); wire as grblHAL limits or keep StallGuard-less factory scheme; Z homes against the hall sensor (top). diff --git a/scripts/bench/README.md b/scripts/bench/README.md index fb5848d..f3840d0 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -7,7 +7,9 @@ target board (dev image, python3 present) unless noted. |---|---| | `feeder.c` | Spike-step-3 underrun proof: streams NOP pulse bytes to `/dev/glowforge` with wall-clock pacing, bounded queue depth, deadman flock, SCHED_FIFO. Usage: `feeder `. Passed 100 kHz × 120 s under full load with 0.2 ms worst write latency. Cross-compile with `build-feeder.sh` (WSL). | | `bench_phase2.py` | End-of-data protocol bench (audit M2–M5): underrun detection/ack, parked no-replay guard, resume(0), continuous-feed stability, 20× run/underrun cycles. Motion-safe (motors locked, laser latched). 16/16 PASS on 2026-07-26. | -| `check_pwm.py` | Laser PWM register check (audit M8): reads PWM2 PWMCR/PWMPR via /dev/mem, expects divider 13 × ~127 counts ≈ 40 kHz. The scope on LASER_PWM remains the final pre-live-fire gate. | +| `check_pwm.py` | Laser PWM register check (audit M8): reads PWM2 PWMCR/PWMPR via /dev/mem, expects divider 13 × ~127 counts ≈ 40 kHz. | +| `pwm_sweep.py` | LASER_PWM scope test (runs on the board): `check` = read-only safety readbacks + PWM2 dump; `sweep` = steps PWMSAR through 50/25/75/6/100 % duty with 4 s holds, then restores. Run only in the locked state (controller stopped, cnc disabled, latch locked). Waveform gate PASSED with it 2026-08-02: 40 kHz stable, duty tracks PWMSAR (6.4 % measured vs 6.3 % commanded at the low end). | +| `pwm_hold.py` | Holds one PWMSAR value for a scope-measurement window (`pwm_hold.py `), then restores. Same locked-state rule. | | `build-glowforge.sh` | Cross-compiles **grblHAL-glowforge** (the canonical driver repo, `../../../grblHAL-glowforge`) in the forge-yocto WSL distro. Run: `wsl -d forge-yocto -- bash /build-glowforge.sh` (from PowerShell; Git Bash mangles /mnt/c paths). This is the production controller build. | | `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 (milestone 2): 700/590 mm/s² accel, 200 mm/s max rate, 28160 Hz travel tick. | diff --git a/scripts/bench/pwm_hold.py b/scripts/bench/pwm_hold.py new file mode 100644 index 0000000..3d5adf5 --- /dev/null +++ b/scripts/bench/pwm_hold.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Hold LASER_PWM at one duty value for a measurement window, then restore. +Usage: pwm_hold.py [sar] [seconds] (defaults: 8, 30)""" +import mmap, struct, sys, time + +PWM2_BASE = 0x02084000 +SAR_OFF = 0x0C + +sar = int(sys.argv[1]) if len(sys.argv) > 1 else 8 +secs = int(sys.argv[2]) if len(sys.argv) > 2 else 30 + +with open('/dev/mem', 'r+b') as f: + m = mmap.mmap(f.fileno(), 4096, mmap.MAP_SHARED, + mmap.PROT_READ | mmap.PROT_WRITE, offset=PWM2_BASE) + sar0 = struct.unpack('' % e + + +def dump_regs(m): + cr, sr, ir, sar, pr = struct.unpack('<5I', m[:20]) + divider = ((cr >> 4) & 0xFFF) + 1 + counts = pr + 2 + freq = PERCLK_HZ / (divider * counts) if counts else 0 + print('PWMCR=0x%08x PWMSAR=%d PWMPR=%d enabled=%s divider=%d counts=%d carrier=%.2f kHz' + % (cr, sar, pr, bool(cr & 1), divider, counts, freq / 1000)) + return sar, pr + + +def safety_readback(): + print('cnc/state =', rd('cnc/state')) + print('cnc/laser_on =', rd('cnc/laser_on')) + print('cnc/laser_enable =', rd('cnc/laser_enable')) + print('cnc/laser_pgood =', rd('cnc/laser_pgood')) + print('cnc/interlock_circuit =', rd('cnc/interlock_circuit'), + '(b0 LASER_ON b1 LASER_ENABLE b2 BUTTON_LATCH b3 LASER_LATCH b4 ILK_RESET)') + print('cnc/laser_on_sampled =', rd('cnc/laser_on_sampled')) + + +mode = sys.argv[1] if len(sys.argv) > 1 else 'check' + +with open('/dev/mem', 'r+b') as f: + m = mmap.mmap(f.fileno(), 4096, mmap.MAP_SHARED, + mmap.PROT_READ | mmap.PROT_WRITE, offset=PWM2_BASE) + + print('--- safety readback (before)') + safety_readback() + print('--- PWM2 registers') + sar0, pr = dump_regs(m) + + if mode == 'sweep': + period = pr + 2 + steps = [(64, '50%'), (32, '25%'), (96, '75%'), (8, '6%'), (127, '100%')] + print('--- duty sweep: 4 s per step, watch the scope') + for sar, label in steps: + m[SAR_OFF:SAR_OFF + 4] = struct.pack('