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.
This commit is contained in:
ScottW514
2026-08-02 16:08:34 -04:00
parent c7e419ff6b
commit fb956971b7
4 changed files with 118 additions and 3 deletions
+17 -2
View File
@@ -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).
+3 -1
View File
@@ -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 <hz> <seconds> <depth_ms>`. 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 <sar> <seconds>`), 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 <path>/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. |
+24
View File
@@ -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('<I', m[SAR_OFF:SAR_OFF + 4])[0]
m[SAR_OFF:SAR_OFF + 4] = struct.pack('<I', sar)
print('PWMSAR=%d held for %d s (was %d)...' % (sar, secs, sar0))
sys.stdout.flush()
time.sleep(secs)
m[SAR_OFF:SAR_OFF + 4] = struct.pack('<I', sar0)
print('restored PWMSAR=%d' % sar0)
with open('/sys/glowforge/cnc/laser_on_sampled') as s:
print('laser_on_sampled =', s.read().strip())
m.close()
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""LASER_PWM scope test - runs ON the board.
check: read-only - safety-chain readbacks + PWM2 register dump.
sweep: step the PWM duty register through known values with pauses so
the scope can capture each, then restore the original sample.
The duty register (PWMSAR) is the laser power SETPOINT only. No motion
subsystem is touched (steppers are disabled before this runs), no
stream runs, the laser latch is locked, FIRE is never asserted.
"""
import mmap, struct, sys, time
PWM2_BASE = 0x02084000
PERCLK_HZ = 66_000_000
SAR_OFF = 0x0C
def rd(name):
try:
with open('/sys/glowforge/' + name) as f:
return f.read().strip()
except OSError as e:
return '<%s>' % 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('<I', sar)
time.sleep(0.1)
cur = struct.unpack('<I', m[SAR_OFF:SAR_OFF + 4])[0]
print(' PWMSAR=%-3d (%s of %d counts) readback=%d' % (sar, label, period, cur))
time.sleep(4)
m[SAR_OFF:SAR_OFF + 4] = struct.pack('<I', sar0)
print('--- restored PWMSAR=%d' % sar0)
print('--- safety readback (after)')
safety_readback()
dump_regs(m)
m.close()