mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
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:
@@ -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. |
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user