mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
docs: bring-up runbook + bench tools from the audit corrective work
BRINGUP.md is the cold-start reference: project status (audit phases 0-5 complete, Phase 6 spike achieved - first grblHAL-commanded motion 2026-07-26), bench/board access, build+deploy procedures, the step backend runbook incl. the required analog machine config, the measured hardware facts bank, and the ordered next-work list. scripts/bench/ preserves the hardware-verification tools (underrun feeder, end-of- data protocol bench, PWM register check, cross-build scripts).
This commit is contained in:
+123
@@ -0,0 +1,123 @@
|
||||
# ForgeFIRM bring-up status & cold-start runbook
|
||||
|
||||
Last updated: **2026-07-26** — the day the machine first moved under grblHAL.
|
||||
Read together with `AUDIT_ACTION_PLAN.md` in the project root (sibling of
|
||||
this repo; per-finding status of the 2026-07-03 audit) and
|
||||
`kernel-module-glowforge/UAPI.md` (the pulse-stream feeder contract).
|
||||
|
||||
## Where the project stands
|
||||
|
||||
**Audit phases 0–5: complete and hardware-verified.** Both motion blockers
|
||||
fixed (cnc probe / 40v-supply; SDMA script relocated to `<26 0xF00>` with a
|
||||
pre-run integrity guard); the end-of-data protocol reworked and bench-proven
|
||||
(underrun is a first-class `underrun` state behind the `streaming` attr;
|
||||
16/16 protocol bench); laser PWM verified at 39.98 kHz (register level);
|
||||
`CONFIG_PREEMPT=y`; uEnv/u-boot/ulfius build integrity restored; legacy
|
||||
cloud mode repaired (nvmem identity → hostname XXX-XXX verified on fuses;
|
||||
deadman/safety loop; camera error paths).
|
||||
|
||||
**Phase 6 spike: achieved.**
|
||||
- grblHAL (unmodified core) runs on the board, speaking Grbl
|
||||
1.1f over **TCP port 23** (LightBurn-confirmed).
|
||||
- Underrun proof: 100 kHz × 120 s under full load, 150 ms queue, 0.2 ms
|
||||
worst write latency, zero underruns. Measured SDMA script ceiling:
|
||||
**~165 kHz effective** (~6 µs/byte).
|
||||
- **The step backend works**: the driver resamples grblHAL's step
|
||||
events into pulse bytes and live-feeds `/dev/glowforge`. X and Y jogs
|
||||
from TCP G-code move the real gantry; grblHAL and kernel position
|
||||
counters agree step-for-step. Motion-only: the laser latch is forced
|
||||
locked, byte bit 4 is never emitted.
|
||||
|
||||
## The bench
|
||||
|
||||
- **Board**: SSH `root@172.16.1.130`, empty password
|
||||
(`ssh -o PreferredAuthentications=none` logs straight in). Dev image
|
||||
(`forgefirm-image-dev`) on SD; BusyBox userland + python3 + gdb/strace.
|
||||
Serial console on ttymxc0 available at the bench.
|
||||
- **Deploying kernels**: re-burn the SD with the freshly built
|
||||
`forgefirm-image-dev-glowforge.rootfs.wic.gz` (deploy dir below). Where
|
||||
the boot flow loads the kernel from was never fully traced (the wic has
|
||||
no boot partition; the eMMC env area reads empty) — re-burning works and
|
||||
is the procedure. **Module-only changes hot-swap**: scp `glowforge.ko`
|
||||
over `/lib/modules/<kver>/extras/`, then `rmmod glowforge && modprobe
|
||||
glowforge`. NOTE: a module reload turns off the lid LED (relight via
|
||||
`/sys/class/leds/lid_led*/target`) and resets analog config (below).
|
||||
- **Build host**: WSL2 distro `forge-yocto`, tree at
|
||||
`~/dev/openglow-forgefirm`. `~/src-sync.sh` rsyncs the Windows repos in
|
||||
(now includes `python3-gfhardware`). Build:
|
||||
`cd ~/dev/openglow-forgefirm/forgefirm && kas shell
|
||||
kas/forgefirm-glowforge.yml -c 'bitbake forgefirm-image
|
||||
forgefirm-image-dev'`. Artifacts:
|
||||
`forgefirm/build/tmp/deploy/images/glowforge/`.
|
||||
- **Shell gotchas** (cost real time): PowerShell mangles embedded double
|
||||
quotes in git-commit here-strings (avoid `"` in messages); `wsl -- bash
|
||||
-c '...'` eats `$VAR` expansions (use script files run via PowerShell,
|
||||
not Git Bash, which MSYS-mangles `/mnt/c` paths).
|
||||
|
||||
## Running the step backend (grblHAL on the board)
|
||||
|
||||
Source: the ForgeFIRM grblHAL step backend (grblHAL core + the
|
||||
glowforge pulse-stream sink).
|
||||
|
||||
1. Build: cross-compile the backend in the forge-yocto WSL distro (from
|
||||
PowerShell). Produces `build-arm/grblHAL_glowforge` in the WSL tree
|
||||
(`-O1 -g`, `GLOWFORGE_DEFAULTS=ON` → machine scaling baked in:
|
||||
53.333 µsteps/mm XY @ ×8, 2.832 half-steps/mm Z, 0.417" Z travel).
|
||||
2. Deploy to `/usr/bin/grblHAL_glowforge` on the board.
|
||||
3. **Analog machine config — required after every module reload/boot; the
|
||||
kernel does NOT do this** (the cloud stack normally did):
|
||||
```sh
|
||||
echo 150 > /sys/glowforge/pic/x_step_current # run current; 33 = weak hold.
|
||||
echo 150 > /sys/glowforge/pic/y_step_current # Factory-true values TBD
|
||||
echo 8 > /sys/glowforge/cnc/x_mode # ×8 microstepping to match
|
||||
echo 8 > /sys/glowforge/cnc/y_mode # the baked steps/mm
|
||||
echo 8 > /sys/glowforge/cnc/motor_lock # Z locked; X/Y free
|
||||
echo 1 > /sys/glowforge/cnc/laser_latch # laser locked out
|
||||
```
|
||||
4. Start: `cd /data && GFSINK=/dev/glowforge grblHAL_glowforge -p 23 -n -t 1.0`
|
||||
(`-t 1.0` is REQUIRED: the real-time throttle is what bounds the
|
||||
queue). Env knobs: `GFSINK_RATE` (machine tick, default 10000 Hz),
|
||||
`GFSINK_DEPTH_MS` (queue depth = feed-hold latency, default 200).
|
||||
5. Connect LightBurn/UGS to `172.16.1.130:23`, or jog raw:
|
||||
`$J=G91X40F1200`.
|
||||
|
||||
## Hardware facts bank (measured)
|
||||
|
||||
- SDMA pulse engine: ring free = 128 MiB − 32 KiB gap; script effective
|
||||
ceiling ~165 kHz; position counters (`sdma_context` sc0/1/2 = X/Y/Z
|
||||
steps, sc3 = bytes) match grblHAL exactly.
|
||||
- Byte layout & rules: see the UAPI.md feeder contract (authoritative).
|
||||
- Z: bit 6 SET = lens UP = +Z (hardware-verified; pulsedata.py was the
|
||||
inverted party, fixed). Home = hall trigger at TOP; usable travel ≈ 30
|
||||
half-steps ≈ 10.6 mm ≈ 0.417"; 0.3534 mm/half-step. Never blind-drive Z
|
||||
— hall-supervised only.
|
||||
- XY: 0.15 mm per full step; DIR bit set = −X / +Y (Y1/Y2 complementary).
|
||||
- Laser PWM: 39.98 kHz register-verified (divider 13 × 127 counts).
|
||||
- Switches: truthy = closed/OK; SW_INTERLOCK reads False on units without
|
||||
the rear plug — must NOT gate motion (beam is hardware-gated).
|
||||
- Machine identity from OCOTP nvmem: serial 00000000 → hostname XXX-XXX
|
||||
(matches the factory label).
|
||||
|
||||
## Next work (in rough order)
|
||||
|
||||
1. **Backend milestone 2 — motion quality**: motion is loud/jerky at the
|
||||
10 kHz tick. Raise `GFSINK_RATE` to 20–50 kHz (finer step-timing
|
||||
quantization), find factory-true run currents (puls-file headers carry
|
||||
them — see gfutilities settings map XSrc/YSrc) and sane accel/max-rate;
|
||||
verify smooth diagonal moves and feed-hold mid-move.
|
||||
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.
|
||||
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).
|
||||
4. **6.5 safety mapping**: door/estop evdev → feed-hold/halt in the
|
||||
backend; underrun → grblHAL alarm; interlock-trip recovery check.
|
||||
5. **6.6 camera service**: persistent MJPEG (ulfius, forgectrl) — also the
|
||||
natural time for the deferred 5.6 emulator smoke (homing images).
|
||||
6. **Housekeeping**: upstream the settings-write crash fix (grblHAL
|
||||
crashes on every runtime $-settings write — NULL chained
|
||||
`grbl.on_settings_changed` in gcode.c's gc_init); Phase 7 doc sweep
|
||||
(CLAUDE.md charter refresh, README roadmap); kas flip + first GitHub
|
||||
release per kas/README.md once ready to publish.
|
||||
@@ -0,0 +1,16 @@
|
||||
# ForgeFIRM bench tools
|
||||
|
||||
Hardware-verification tools for the ForgeFIRM bench. All run ON the
|
||||
target board (dev image, python3 present) unless noted.
|
||||
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `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. |
|
||||
| `build-feeder.sh` | Cross-compiles `feeder.c` the same way. |
|
||||
|
||||
The build scripts borrow the Yocto cross toolchain + sysroot from the ulfius
|
||||
2.7.15 work directory in the WSL build tree; if that path ages out after a
|
||||
`bitbake -c clean`, point `TC` at any current target recipe workdir (or build
|
||||
a proper SDK with `bitbake meta-toolchain`).
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 2.8 bench test: the reworked end-of-data protocol (audit M2-M5).
|
||||
|
||||
Motion-safe by construction: all four motors are locked via motor_lock (the
|
||||
SDMA script masks the STEP bits), the laser latch is locked (LASER_ON pin is
|
||||
Hi-Z and the hardware safety chain is open anyway), and only NOP (0x00) and
|
||||
laser-bit (0x10) bytes are streamed.
|
||||
|
||||
Exercises: normal completion, underrun detection/ack (M5), parked no-replay
|
||||
guard (M4), alldone GPIO clear (M2, via GPIO2_DR readback - the data register
|
||||
retains the last value the script wrote), resume(0) (M3), continuous-feed
|
||||
stability, and 20x run/underrun cycling (M3 wedge check).
|
||||
"""
|
||||
import os, re, sys, time, mmap, struct
|
||||
|
||||
C = "/sys/glowforge/cnc"
|
||||
DEV = "/dev/glowforge"
|
||||
GPIO2_BASE = 0x020A0000 # GPIO2 DR at offset 0
|
||||
|
||||
passed = failed = 0
|
||||
def chk(name, cond, detail=""):
|
||||
global passed, failed
|
||||
tag = "PASS" if cond else "FAIL"
|
||||
if cond: passed += 1
|
||||
else: failed += 1
|
||||
print(f"{tag}: {name} [{detail}]", flush=True)
|
||||
|
||||
def rd(attr):
|
||||
with open(f"{C}/{attr}") as f: return f.read().strip()
|
||||
def wr(attr, val):
|
||||
with open(f"{C}/{attr}", "w") as f: f.write(str(val))
|
||||
def state(): return rd("state")
|
||||
|
||||
def gpio2_dr():
|
||||
with open("/dev/mem", "rb") as f:
|
||||
m = mmap.mmap(f.fileno(), 4096, mmap.MAP_SHARED, mmap.PROT_READ,
|
||||
offset=GPIO2_BASE)
|
||||
v = struct.unpack("<I", m[:4])[0]
|
||||
m.close()
|
||||
return v
|
||||
|
||||
def sc(ctx, n):
|
||||
return int(re.search(rf"sc{n}=([0-9a-f]{{8}})", ctx).group(1), 16)
|
||||
|
||||
def wait_state(target, timeout=15):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
s = state()
|
||||
if s == target: return s
|
||||
time.sleep(0.05)
|
||||
return state()
|
||||
|
||||
# ---- setup: everything locked ----
|
||||
wr("laser_latch", 1) # LASER_ON pin Hi-Z
|
||||
wr("motor_lock", 15) # X|Y1|Y2|Z locked: no motion regardless of data
|
||||
if state() == "disabled":
|
||||
wr("enable", 1)
|
||||
time.sleep(0.5)
|
||||
SEC = int(rd("step_freq")) # bytes per second of playback
|
||||
print(f"setup: state={state()} step_freq={SEC} underruns={rd('underruns')}", flush=True)
|
||||
|
||||
fd = os.open(DEV, os.O_WRONLY) # exclusive open, held for the whole bench
|
||||
|
||||
def clear_data():
|
||||
os.lseek(fd, 1, os.SEEK_SET) # clear pulse data + byte counters
|
||||
def feed(b, n):
|
||||
return os.write(fd, bytes([b]) * n)
|
||||
|
||||
# ---- T1: normal completion (streaming=0) -> idle ----
|
||||
wr("streaming", 0)
|
||||
clear_data()
|
||||
feed(0x00, 2 * SEC)
|
||||
wr("run", 1)
|
||||
chk("T1 run starts", state() == "running", state())
|
||||
s = wait_state("idle", 10)
|
||||
chk("T1 normal completion -> idle", s == "idle", s)
|
||||
|
||||
# ---- T2: underrun (streaming=1): state, counter, run refused ----
|
||||
u0 = int(rd("underruns"))
|
||||
wr("streaming", 1)
|
||||
clear_data()
|
||||
feed(0x00, 1 * SEC)
|
||||
wr("run", 1)
|
||||
s = wait_state("underrun", 10)
|
||||
chk("T2 underrun state entered", s == "underrun", s)
|
||||
chk("T2 underrun counter incremented", int(rd("underruns")) == u0 + 1, rd("underruns"))
|
||||
try:
|
||||
wr("run", 1)
|
||||
chk("T2 run refused while unacked", False, "run was accepted!")
|
||||
except OSError as e:
|
||||
chk("T2 run refused while unacked", True, str(e))
|
||||
|
||||
# ---- T3: parked no-replay guard (M4): append while parked, counters frozen ----
|
||||
ctx_a = rd("sdma_context")
|
||||
feed(0x01, 1000) # X-step bytes appended while parked (motors locked anyway)
|
||||
time.sleep(1.0)
|
||||
ctx_b = rd("sdma_context")
|
||||
chk("T3 byte counter frozen while parked", sc(ctx_a, 3) == sc(ctx_b, 3),
|
||||
f"sc3 {sc(ctx_a,3)} -> {sc(ctx_b,3)}")
|
||||
chk("T3 head frozen while parked", sc(ctx_a, 4) == sc(ctx_b, 4),
|
||||
f"sc4 {sc(ctx_a,4)} -> {sc(ctx_b,4)}")
|
||||
wr("stop", 1)
|
||||
chk("T3 stop acks underrun -> idle", state() == "idle", state())
|
||||
clear_data()
|
||||
|
||||
# ---- T4: alldone clears laser+step GPIO bits (M2) ----
|
||||
wr("streaming", 1)
|
||||
feed(0x10, 1 * SEC) # laser bit set, no steps
|
||||
wr("run", 1)
|
||||
s = wait_state("underrun", 10)
|
||||
chk("T4 underrun after laser-bit stream", s == "underrun", s)
|
||||
time.sleep(0.2)
|
||||
dr = gpio2_dr()
|
||||
chk("T4 LASER_ENABLE/LASER_ON_HEAD bits low in GPIO2_DR", (dr >> 30) == 0,
|
||||
f"DR=0x{dr:08x}")
|
||||
chk("T4 STEP bits low in GPIO2_DR",
|
||||
(dr & ((1 << 20) | (1 << 21) | (1 << 22) | (1 << 29))) == 0, f"DR=0x{dr:08x}")
|
||||
wr("stop", 1)
|
||||
clear_data()
|
||||
|
||||
# ---- T5: resume(0) completes instead of wedging (M3) ----
|
||||
wr("streaming", 0)
|
||||
feed(0x00, SEC // 2)
|
||||
wr("resume", 0)
|
||||
chk("T5 resume(0) starts", state() == "running", state())
|
||||
s = wait_state("idle", 10)
|
||||
chk("T5 resume(0) completes -> idle (no wedge)", s == "idle", s)
|
||||
clear_data()
|
||||
|
||||
# ---- T6: continuous feed never underruns; stopping the feed does ----
|
||||
wr("streaming", 1)
|
||||
feed(0x00, SEC)
|
||||
wr("run", 1)
|
||||
t0 = time.time(); ok = True
|
||||
while time.time() - t0 < 5:
|
||||
feed(0x00, SEC // 4)
|
||||
if state() != "running":
|
||||
ok = False
|
||||
break
|
||||
time.sleep(0.2)
|
||||
chk("T6 no underrun while feeding (5 s)", ok, state())
|
||||
s = wait_state("underrun", 10)
|
||||
chk("T6 underrun after feed stops", s == "underrun", s)
|
||||
wr("stop", 1)
|
||||
clear_data()
|
||||
|
||||
# ---- T7: 20x run/underrun cycles, no wedge (M3) ----
|
||||
anomalies = 0
|
||||
for i in range(20):
|
||||
feed(0x00, SEC // 10)
|
||||
wr("run", 1)
|
||||
s = wait_state("underrun", 5)
|
||||
if s != "underrun":
|
||||
anomalies += 1
|
||||
print(f" cycle {i}: state={s}", flush=True)
|
||||
wr("stop", 1)
|
||||
chk("T7 20 run/underrun cycles clean", anomalies == 0, f"{anomalies} anomalies")
|
||||
|
||||
# ---- wrap up ----
|
||||
wr("streaming", 0)
|
||||
clear_data()
|
||||
os.close(fd)
|
||||
wr("motor_lock", 0)
|
||||
print(f"\nRESULT: {passed} passed, {failed} failed; underruns total={rd('underruns')}",
|
||||
flush=True)
|
||||
sys.exit(1 if failed else 0)
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
TC=/home/builder/dev/openglow-forgefirm/forgefirm/build/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/ulfius/2.7.15
|
||||
SP="$(cd "$(dirname "$0")" && pwd)"
|
||||
"$TC/recipe-sysroot-native/usr/bin/arm-fslc-linux-gnueabi/arm-fslc-linux-gnueabi-gcc" \
|
||||
--sysroot="$TC/recipe-sysroot" \
|
||||
-mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 \
|
||||
-O2 -Wall -Wextra -o "$SP/feeder" "$SP/feeder.c"
|
||||
echo FEEDER-OK
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 3.2 software check: laser PWM carrier frequency from PWM2 registers.
|
||||
|
||||
Expected with the fsl,extra-prescale=<13> fix: PWMCR prescaler divider = 13,
|
||||
PWMPR ~125 (127 counts - 2), effective carrier = perclk / (13 * 127) ~= 40 kHz.
|
||||
Without the fix: divider 1 -> ~520 kHz (audit M8). Safe: read-only register
|
||||
inspection; the laser PWM output feeds the PSU power input, firing stays gated
|
||||
by the hardware chain. The definitive gate remains the scope on LASER_PWM.
|
||||
"""
|
||||
import mmap, struct
|
||||
|
||||
PWM2_BASE = 0x02084000
|
||||
PERCLK_HZ = 66_000_000 # ipg_high; cross-check against the EPIT rate in dmesg
|
||||
|
||||
with open("/dev/mem", "rb") as f:
|
||||
m = mmap.mmap(f.fileno(), 4096, mmap.MAP_SHARED, mmap.PROT_READ,
|
||||
offset=PWM2_BASE)
|
||||
cr, sr, ir, sar, pr = struct.unpack("<5I", m[:20])
|
||||
m.close()
|
||||
|
||||
prescaler_field = (cr >> 4) & 0xFFF
|
||||
divider = prescaler_field + 1
|
||||
period_counts = pr + 2
|
||||
freq = PERCLK_HZ / (divider * period_counts) if period_counts else 0
|
||||
enabled = cr & 1
|
||||
|
||||
print(f"PWMCR=0x{cr:08x} PWMSAR={sar} PWMPR={pr} enabled={bool(enabled)}")
|
||||
print(f"prescaler divider = {divider} (field {prescaler_field})")
|
||||
print(f"period counts = {period_counts}")
|
||||
print(f"carrier frequency = {freq/1000:.2f} kHz")
|
||||
ok = divider == 13 and 120 <= period_counts <= 135 and 38_000 <= freq <= 42_000
|
||||
print(f"\n{'PASS' if ok else 'FAIL'}: expected divider 13, ~127 counts, ~40 kHz")
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* feeder.c - ForgeFIRM spike step 3: prove no-underrun continuous live
|
||||
* feeding of the glowforge.ko SDMA pulse ring under load (audit 6.3).
|
||||
*
|
||||
* Streams NOP (0x00) pulse bytes to /dev/glowforge, pacing by wall clock to
|
||||
* hold a bounded queue depth (like a real grblHAL backend would - audit M6),
|
||||
* with the deadman flock held. Motors must be locked and the laser latch
|
||||
* locked by the caller (bench_phase6.sh does this).
|
||||
*
|
||||
* Reports: feed statistics, worst scheduling stall, ENOMEM count, and the
|
||||
* final driver state (expect "running" throughout, "underrun" only after
|
||||
* the deliberate starve at the end).
|
||||
*
|
||||
* Usage: feeder <step_freq_hz> <duration_s> <depth_ms>
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sched.h>
|
||||
|
||||
#define DEV "/dev/glowforge"
|
||||
#define CNC "/sys/glowforge/cnc/"
|
||||
|
||||
static double now_s(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec + ts.tv_nsec / 1e9;
|
||||
}
|
||||
|
||||
static int wr_attr(const char *attr, const char *val)
|
||||
{
|
||||
char path[128];
|
||||
int fd, ret;
|
||||
snprintf(path, sizeof path, CNC "%s", attr);
|
||||
fd = open(path, O_WRONLY);
|
||||
if (fd < 0) return -1;
|
||||
ret = (int)write(fd, val, strlen(val));
|
||||
close(fd);
|
||||
return ret < 0 ? -1 : 0;
|
||||
}
|
||||
|
||||
static int rd_attr(const char *attr, char *buf, size_t len)
|
||||
{
|
||||
char path[128];
|
||||
int fd;
|
||||
ssize_t n;
|
||||
snprintf(path, sizeof path, CNC "%s", attr);
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
n = read(fd, buf, len - 1);
|
||||
close(fd);
|
||||
if (n < 0) return -1;
|
||||
while (n > 0 && (buf[n-1] == '\n')) n--;
|
||||
buf[n] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
long freq = argc > 1 ? atol(argv[1]) : 10000;
|
||||
long duration = argc > 2 ? atol(argv[2]) : 60;
|
||||
long depth_ms = argc > 3 ? atol(argv[3]) : 150;
|
||||
static unsigned char chunk[8192]; /* NOP bytes: no step, no laser */
|
||||
char state[32], u0[16], u1[16];
|
||||
long long target, enqueued = 0, enomem = 0, writes = 0;
|
||||
double t0, t_end, last = 0, max_stall = 0, max_wr = 0;
|
||||
long depth_bytes = (long)((double)freq * depth_ms / 1000.0);
|
||||
int fd;
|
||||
|
||||
memset(chunk, 0, sizeof chunk);
|
||||
|
||||
/* SCHED_FIFO like a real feeder; fall back silently if not permitted */
|
||||
struct sched_param sp = { .sched_priority = 10 };
|
||||
sched_setscheduler(0, SCHED_FIFO, &sp);
|
||||
|
||||
fd = open(DEV, O_WRONLY);
|
||||
if (fd < 0) { perror("open " DEV); return 1; }
|
||||
if (flock(fd, LOCK_EX) != 0) { perror("flock"); return 1; }
|
||||
|
||||
char fbuf[16];
|
||||
snprintf(fbuf, sizeof fbuf, "%ld", freq);
|
||||
if (wr_attr("step_freq", fbuf)) { perror("step_freq"); return 1; }
|
||||
wr_attr("streaming", "1");
|
||||
rd_attr("underruns", u0, sizeof u0);
|
||||
|
||||
lseek(fd, 0, SEEK_SET); /* clear data + position */
|
||||
|
||||
/* Prefill one queue depth, then start the run */
|
||||
while (enqueued < depth_bytes) {
|
||||
long n = depth_bytes - enqueued;
|
||||
if (n > (long)sizeof chunk) n = sizeof chunk;
|
||||
if (write(fd, chunk, n) < 0) { perror("prefill"); return 1; }
|
||||
enqueued += n;
|
||||
}
|
||||
if (wr_attr("run", "1")) { perror("run"); return 1; }
|
||||
|
||||
t0 = now_s();
|
||||
t_end = t0 + duration;
|
||||
last = t0;
|
||||
printf("feeding: %ld Hz for %ld s, queue depth %ld ms (%ld bytes)\n",
|
||||
freq, duration, depth_ms, depth_bytes);
|
||||
|
||||
while (1) {
|
||||
double t = now_s();
|
||||
if (t >= t_end) break;
|
||||
if (t - last > max_stall) max_stall = t - last;
|
||||
last = t;
|
||||
|
||||
/* wall-clock pacing: keep enqueued = consumed-so-far + depth */
|
||||
target = (long long)((t - t0) * freq) + depth_bytes;
|
||||
while (enqueued < target) {
|
||||
long n = (long)(target - enqueued);
|
||||
if (n > (long)sizeof chunk) n = sizeof chunk;
|
||||
double w0 = now_s(), w1;
|
||||
if (write(fd, chunk, n) < 0) {
|
||||
if (errno == ENOMEM) { enomem++; break; }
|
||||
perror("write"); return 1;
|
||||
}
|
||||
w1 = now_s();
|
||||
if (w1 - w0 > max_wr) max_wr = w1 - w0;
|
||||
enqueued += n;
|
||||
writes++;
|
||||
}
|
||||
|
||||
struct timespec ts = { 0, 20 * 1000 * 1000 }; /* 20 ms */
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
rd_attr("state", state, sizeof state);
|
||||
printf("after %ld s: state=%s enqueued=%lld writes=%lld enomem=%lld\n",
|
||||
duration, state, enqueued, writes, enomem);
|
||||
printf("max loop stall: %.1f ms, max write latency: %.1f ms\n",
|
||||
max_stall * 1e3, max_wr * 1e3);
|
||||
int fed_ok = (strcmp(state, "running") == 0) && enomem == 0;
|
||||
|
||||
/* Deliberate starve: stop feeding, expect a clean underrun */
|
||||
double ts0 = now_s();
|
||||
do {
|
||||
struct timespec ts = { 0, 50 * 1000 * 1000 };
|
||||
nanosleep(&ts, NULL);
|
||||
rd_attr("state", state, sizeof state);
|
||||
} while (strcmp(state, "running") == 0 && now_s() - ts0 < 10 + depth_ms / 1000.0);
|
||||
rd_attr("underruns", u1, sizeof u1);
|
||||
printf("after starve: state=%s underruns %s -> %s\n", state, u0, u1);
|
||||
int starve_ok = strcmp(state, "underrun") == 0;
|
||||
|
||||
wr_attr("stop", "1"); /* acknowledge */
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
wr_attr("streaming", "0");
|
||||
flock(fd, LOCK_UN);
|
||||
close(fd);
|
||||
|
||||
printf("%s: feed %s, starve->underrun %s\n",
|
||||
(fed_ok && starve_ok) ? "PASS" : "FAIL",
|
||||
fed_ok ? "clean" : "FAILED", starve_ok ? "detected" : "MISSED");
|
||||
return (fed_ok && starve_ok) ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user