diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 385eecd..80b59cf 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -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), diff --git a/docs/CAMPAIGN-LOG.md b/docs/CAMPAIGN-LOG.md index 20f2e09..6e24b7b 100644 --- a/docs/CAMPAIGN-LOG.md +++ b/docs/CAMPAIGN-LOG.md @@ -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 diff --git a/forgetest/forgetest/bench.py b/forgetest/forgetest/bench.py index 05b4bf6..38ac9ca 100644 --- a/forgetest/forgetest/bench.py +++ b/forgetest/forgetest/bench.py @@ -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", diff --git a/scripts/bench/accel_crash_probe.py b/scripts/bench/accel_crash_probe.py index c9a7683..2e3a72e 100644 --- a/scripts/bench/accel_crash_probe.py +++ b/scripts/bench/accel_crash_probe.py @@ -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)