bench: crash-probe drill run, coexist proven, detector is forgectrl-only

The de-risk drill ran on the bench: the IG registers program and poll
over i2c-dev (I2C_SLAVE_FORCE) with st_accel bound, raw reads intact,
so the crash detector is forgectrl-only (BRINGUP item 6, CAMPAIGN-LOG
2026-08-31). The drill found the IG needs a running ODR: st_accel
leaves the part in power-down between one-shot reads. The script now
saves CTRL1, runs the window at 800 Hz, and restores it on exit; its
old ODR test checked the axis-enable bits instead of the ODR bits.

No catalog consequence: the change is a bench drill script, its bench
page text, and the docs record of the drill; no shipped behavior
changes.
This commit is contained in:
ScottW514
2026-08-31 18:21:08 -04:00
parent 765521455d
commit 61b2913e3a
4 changed files with 71 additions and 24 deletions
+23 -15
View File
@@ -1043,7 +1043,16 @@ is committed.
and neither the factory DTS (head I²C `status = "disabled"`, the accel
driven from userspace) nor ForgeFIRM's DTS gives the accel an `interrupts`
property. ForgeFIRM neither arms the generator nor reads b1; it only polls
raw samples.
raw samples. `st_accel` leaves the part in **power-down between one-shot
reads** (CTRL1 ODR bits 0) and the interrupt generator only samples at a
running ODR, so an armed detector must set the ODR itself and re-assert it
after any liveness read (each one-shot powers the part down again). The IG
registers coexist with the bound driver over i2c-dev (I2C_SLAVE_FORCE):
IG_SRC1 polls at ~166 Hz from Python with `st_accel` raw reads intact. At
the factory +/-2 g full scale the threshold LSB is ~15.6 mg, so 1 g ~= 64;
gravity rides Z on the head (raw ~-16916), so Z trips any sub-1 g
threshold at rest while X and Y stay silent at rest and through a jog at
threshold 40 (~0.62 g).
- **Beam detect in the head MCU.** PTE16 into ADC0 gives reg 0x16, the raw
analog level (`beam_detect_analog`, a head sysfs attr): near 1834 dark, 2600
to 2890 during S300/S400 fire (the acceptance suite's mark witness),
@@ -1225,20 +1234,19 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
The rail-contact signature in the facts bank sets the first threshold in
register units; a pause on contact is the first use. ForgeFIRM already
reads the head accel raw for liveness, so this shares the bus, not new
hardware. The open decision is where the IG programming lives, and it
turns on a bench fact: the IG registers (0x30 to 0x35) are ones
`st_accel` never touches, so if they can be reached over i2c-dev
(I2C_SLAVE_FORCE) while the driver stays bound, the detector is
forgectrl-only with the liveness path untouched; if the two collide, the
accel moves under `glowforge.ko`. The de-risk drill
(`scripts/bench/accel_crash_probe.py`, on the bench page) settles that
first: it arms IG1 in coexist mode, provokes a strike, confirms IG_SRC1
latches the expected axis at a factory-derived threshold, and checks that
`st_accel`'s raw reads keep working through the run. Keeping the factory
full scale (no CTRL4 write) is what keeps the coexistence clean. Owed
after the drill: the chosen readout path, per-state (idle/run) thresholds
seeded from a captured cut header's HA* values, and the two tiers wired
into the existing feed-hold and stop-plus-latch paths.
hardware. The readout path is settled by the de-risk drill
(`scripts/bench/accel_crash_probe.py`, on the bench page; the record is
in CAMPAIGN-LOG): the detector is **forgectrl-only**. The IG registers
(0x30 to 0x35) program and poll over i2c-dev (I2C_SLAVE_FORCE) while
`st_accel` stays bound, and the liveness raw reads keep working through
an armed window. Keeping the factory full scale (no CTRL4 write) keeps
the coexistence clean, and the armed detector must own the ODR: the
facts bank ("The head accelerometer") has the power-down behavior. The
gravity axis rules the thresholds: X and Y arm below 1 g, a Z threshold
must sit above 1 g plus margin. Owed: the forgectrl detector itself,
per-state (idle/run) thresholds seeded from a captured cut header's HA*
values, and the two tiers wired into the existing feed-hold and
stop-plus-latch paths.
Owed for the head IRQ, only if a coarse hardware interrupt is wanted
instead of the poll: arm the accel bit in the head MCU (reg 0x03/0x04),
+33
View File
@@ -6795,6 +6795,39 @@ enables SEGGER RTT telemetry; regs 0x3c-0x3f read a debug capture ring
(the previously-unexplained `i2cget 0x47 0x02`, `0x0f`, `0x3c`
commands).
## 2026-08-31: crash-detector de-risk drill - coexist proven, detector is forgectrl-only
The de-risk drill for the head-accelerometer crash detector
(`scripts/bench/accel_crash_probe.py`, bench page `accel-crash-probe`) ran
three coexist windows on dev 20260831204710 with forgectrl and grblHAL up
and idle: a rest window on xyz, a rest window on xy, and an xy window with
one gentle jog (`$J=G91 X5 F1000`, +X first). No emission, no unbind.
- **Coexist PROVEN.** The IG registers (0x30-0x35) program and poll over
i2c-dev with I2C_SLAVE_FORCE while `st_accel` stays bound; IG_SRC1
polled at ~166 Hz from Python, and `st_accel` raw reads kept working
through and after every window. The detector is **forgectrl-only**: no
kernel change, the liveness path untouched. This was the item's one
open design decision.
- **Latch and per-axis source report work.** At threshold 40 (~0.62 g at
the factory +/-2 g full scale) the gravity axis Z (raw -16916, about
-1.03 g) latched IG_SRC1 on every poll; X and Y stayed silent at rest
and through the jog. So the shipped detector arms X and Y below 1 g and
a Z threshold must sit above 1 g plus margin.
- **New fact: the IG needs a running ODR.** The first window returned no
trips at all because `st_accel` leaves the part in power-down between
one-shot reads (CTRL1 ODR bits 0) and the interrupt generator only
samples at a running ODR. The armed detector must set the ODR and
re-assert it after any liveness read (each one-shot powers the part
down again). The drill script now saves CTRL1, runs the window at
800 Hz, and restores the saved value on exit; its old ODR test checked
the axis-enable bits (0x07) instead of the ODR bits (0x70), fixed in
the same change.
- **No strike was provoked, none owed.** The rail-contact signature from
the retired homing spike (29-42 k counts within ~4 ms) already fixes
the strike magnitude, 3x and more over a threshold that gravity
already trips; a physical tap would add nothing the design needs.
## Reference notes
### Head-IRQ source validation — the beam-emission hypothesis
+2 -1
View File
@@ -86,7 +86,8 @@ TOOLS = [
_arg("jog", "str", None, "optional one jog at t=2 s, e.g. $J=G91X5F1000 (+X only)", flag="--jog")],
"desc": "Arms the head LIS2HH12's on-chip interrupt generator (IG1) and polls IG_SRC1 for a latched strike. "
"coexist mode reaches the IG registers over i2c-dev with st_accel still bound (the forgectrl-only "
"path proof); provoke a trip by hand or with --jog. Touches only the IG registers, no emission, "
"path proof); provoke a trip by hand or with --jog. Touches the IG registers and CTRL1 "
"(saved, run at 800 Hz, restored; the IG only samples at a running ODR), no emission, "
"no full-scale change. CSV to /tmp/accel_crash.csv."},
# -- board-side, takeover / scope ------------------------------------------
{"id": "pwm-sweep", "title": "LASER_PWM scope sweep", "script": "pwm_sweep.py",
+13 -8
View File
@@ -20,11 +20,13 @@ rides on, before any kernel or forgectrl work:
can be set in register units against the rail-contact signature
(facts bank: 20-40x over creep within ~4 ms on a fast strike).
The drill touches ONLY the IG registers plus the IG-latch bit of CTRL7,
which st_accel never writes; it does not change the full scale (CTRL4)
or the ODR (CTRL1), so st_accel's raw scaling is undisturbed. Default is
coexist mode with forgectrl left running: no emission, no commanded
motion. Provoke a trip by hand (a firm tap or nudge on the head) or pass
The drill touches the IG registers plus the IG-latch bit of CTRL7,
which st_accel never writes, and CTRL1: st_accel powers the part down
between one-shot reads (ODR=0), and the IG generator only samples at a
running ODR, so the drill saves CTRL1, sets 800 Hz for the window and
restores the saved value on exit. The full scale (CTRL4) is never
changed, so st_accel's raw scaling is undisturbed. Default is coexist
mode with forgectrl left running: no emission, no commanded motion. Provoke a trip by hand (a firm tap or nudge on the head) or pass
--jog to send one gentle grblHAL jog; a jog needs the controller free,
so run it from the bench page's takeover or stop the controller first.
@@ -163,11 +165,12 @@ def main():
bind_ctl('bind')
return 1
ctrl1 = rd(fd, CTRL1)
orig_ctrl1 = rd(fd, CTRL1)
ctrl1 = orig_ctrl1
ctrl4 = rd(fd, CTRL4)
fs = FS_G.get((ctrl4 >> 4) & 0b11, '?')
if unbind and (ctrl1 & 0x07) == 0:
wr(fd, CTRL1, 0x6F) # 800 Hz, BDU, XYZ on; st_accel does this itself
if (ctrl1 & 0x70) == 0: # ODR bits 6:4 zero = power-down; IG needs
wr(fd, CTRL1, 0x6F) # a running ODR: 800 Hz, BDU, XYZ on
ctrl1 = rd(fd, CTRL1)
lsb_mg = (fs * 1000.0 / 128.0) if isinstance(fs, int) else 0
print('WHO_AM_I=0x%02x CTRL1=0x%02x CTRL4=0x%02x FS=+/-%sg '
@@ -238,6 +241,8 @@ def main():
# Disarm and restore.
wr(fd, IG_CFG1, 0x00)
wr(fd, CTRL7, ctrl7)
if ctrl1 != orig_ctrl1:
wr(fd, CTRL1, orig_ctrl1)
if sock:
try:
sock.recv(4096)