mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
Pin forgectrl: the crash watch; its catalog case and the drill's latch bit
forgectrl 161686f adds the head-accelerometer crash watch (two tiers on the LIS2HH12's interrupt generators, armed inside the laser's armed window). The catalog grows cooling.crash-watch-plumbing - the unarmed half of the watch provable without a button press: accel_watch stays at watch through an unarmed session, three zeros read as the three crash gates off, an out-of-range threshold is refused - and _COOL_COVERS widens to src/accel.*. The armed-tier bench drill is a BRINGUP next-work item. The de-risk drill script's CTRL7 write moves from bit 0 (4D_IG1) to bit 2 (LIR1), the latch the datasheet actually puts there; the drill's findings did not depend on latching (a continuous gravity signal).
This commit is contained in:
+6
-30
@@ -1225,36 +1225,12 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
|||||||
5. **Update system Phase 5 — recovery refresh.** The remaining phase of
|
5. **Update system Phase 5 — recovery refresh.** The remaining phase of
|
||||||
`docs/UPDATE-SYSTEM.md` (a refreshed recovery image in boot0); Phases 0–4
|
`docs/UPDATE-SYSTEM.md` (a refreshed recovery image in boot0); Phases 0–4
|
||||||
are done.
|
are done.
|
||||||
6. **Head accelerometer crash detector, and the head IRQ (planned;
|
6. **Crash-watch bench commissioning, and the head IRQ (exploratory).**
|
||||||
exploratory).** The factory's head crash detector is the head
|
Commission the crash watch on the bench, on the next image: a knock
|
||||||
accelerometer's own on-chip interrupt generator, armed per job from the
|
inside an armed window must walk both tiers (`BUMP` holds and
|
||||||
HA* header thresholds and read by polling the sensor; the head IRQ is the
|
releases after the quiet polls; `CRASH` stops motion and locks the
|
||||||
coarse aggregate path to that event and the others (hall, beam). Both are
|
latch for the session), and judge the shipped thresholds against
|
||||||
one mechanism (the facts bank, "The head MCU flag register and HEAD_IRQ"
|
that knock.
|
||||||
and "The head accelerometer"), which is why these two items are one. The
|
|
||||||
detector is adopted, not measured from scratch: the earlier plan to
|
|
||||||
bench-measure a filter is moot now that the HA* thresholds are known to be
|
|
||||||
LIS2HH12 register values at a known full scale.
|
|
||||||
|
|
||||||
The detector arms the LIS2HH12 interrupt generator over i2c-3 (per-axis
|
|
||||||
threshold, duration) and polls IG_SRC1 for a latched per-axis trip, on
|
|
||||||
the factory's two-tier shape (an alert that pauses, an abort that fails).
|
|
||||||
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 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
|
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),
|
instead of the poll: arm the accel bit in the head MCU (reg 0x03/0x04),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .. import hw
|
|||||||
|
|
||||||
_COOL_COVERS = [("forgectrl", "src/cool.*"), ("forgectrl", "src/coolfmt.*"), ("forgectrl", "src/diag.*"),
|
_COOL_COVERS = [("forgectrl", "src/cool.*"), ("forgectrl", "src/coolfmt.*"), ("forgectrl", "src/diag.*"),
|
||||||
("forgectrl", "src/gates.*"), ("forgectrl", "src/airflow.*"),
|
("forgectrl", "src/gates.*"), ("forgectrl", "src/airflow.*"),
|
||||||
|
("forgectrl", "src/accel.*"),
|
||||||
("forgectrl", "src/settings.*"),
|
("forgectrl", "src/settings.*"),
|
||||||
("forgectrl", "src/status.*"), ("forgectrl", "src/ui/**"),
|
("forgectrl", "src/status.*"), ("forgectrl", "src/ui/**"),
|
||||||
("grblhal-glowforge", "src/glowforge_cooling.*"),
|
("grblhal-glowforge", "src/glowforge_cooling.*"),
|
||||||
@@ -836,6 +837,84 @@ def fire_watch_tiers(ctx):
|
|||||||
ctx.check(fc.wait_idle(60, abort=ctx.aborted), "machine did not return to idle")
|
ctx.check(fc.wait_idle(60, abort=ctx.aborted), "machine did not return to idle")
|
||||||
|
|
||||||
|
|
||||||
|
CRASH_KEYS = ("cool_accel_x_alert", "cool_accel_y_alert", "cool_accel_abort")
|
||||||
|
CRASH_GATES = ["crash_abort", "crash_x_alert", "crash_y_alert"]
|
||||||
|
|
||||||
|
|
||||||
|
@test("cooling.crash-watch-plumbing", title="The crash watch stays off the accelerometer unarmed and its gates say off",
|
||||||
|
subsystem="cooling", kind="auto", mode="grbl", est_min=3,
|
||||||
|
covers=_COOL_COVERS, requires=["kernel.latch-locked-idle"],
|
||||||
|
steps=["Machine idle, lid closed. The test writes the three crash thresholds and restores "
|
||||||
|
"them; two M8/M9 sessions, no fire, no motion."],
|
||||||
|
description="The head-accelerometer crash watch's plumbing, provable without an armed "
|
||||||
|
"window: /cool/status carries accel_watch at watch with the factory-seeded "
|
||||||
|
"thresholds standing; an unarmed run session never arms the watch (the "
|
||||||
|
"coexistence contract: liveness and homing own the accel outside the armed "
|
||||||
|
"window), so accel_watch reads watch through it; the three thresholds at 0 "
|
||||||
|
"read as the three crash gates off by value (gates_off names them) with the "
|
||||||
|
"session OK; a threshold past the register range is refused; restored, the "
|
||||||
|
"gates read ok. The tiers themselves (BUMP, CRASH) trip only on a physical "
|
||||||
|
"knock inside an armed window and are commissioned at the bench, not here.")
|
||||||
|
def crash_watch_plumbing(ctx):
|
||||||
|
fc = ctx.forgectrl
|
||||||
|
ev = ctx.evidence
|
||||||
|
before = fc.settings()
|
||||||
|
orig = {k: before.get(k, "") for k in CRASH_KEYS}
|
||||||
|
ev["orig"] = orig
|
||||||
|
ctx.log("original: %s", orig)
|
||||||
|
gates = (before.get("gates") or {})
|
||||||
|
for k in CRASH_KEYS:
|
||||||
|
g = gates.get(k) or {}
|
||||||
|
ctx.check(g.get("gate", "").startswith("crash_"), "settings gates has no %s row: %s", k, g)
|
||||||
|
c0 = _cool(fc)
|
||||||
|
ctx.check(c0.get("accel_watch") == "watch",
|
||||||
|
"accel_watch %r at idle with thresholds set, expected watch", c0.get("accel_watch"))
|
||||||
|
|
||||||
|
st, body = fc.post("/settings", params={"cool_accel_x_alert": "300"})
|
||||||
|
ctx.check(st != 200 or body.get("cool_accel_x_alert") != "300",
|
||||||
|
"a threshold past the register range (300) was accepted")
|
||||||
|
|
||||||
|
restored = False
|
||||||
|
with ctx.grbl() as grbl:
|
||||||
|
ctx.check(grbl.status_report()["state"].startswith("Idle"), "controller is not idle")
|
||||||
|
try:
|
||||||
|
# Leg 1: an unarmed session never arms the watch.
|
||||||
|
c = _run_session(ctx, grbl, fc, lambda c: c.get("phase") == "run", "unarmed session")
|
||||||
|
ev["unarmed"] = c
|
||||||
|
ctx.check(c.get("accel_watch") == "watch",
|
||||||
|
"accel_watch %r in an unarmed run session, expected watch", c.get("accel_watch"))
|
||||||
|
ctx.check(c.get("verdict") == "OK", "unarmed session not OK: %s", c)
|
||||||
|
|
||||||
|
# Leg 2: the three thresholds at zero are the gates off.
|
||||||
|
_set_gates(ctx, fc, {k: "0" for k in CRASH_KEYS})
|
||||||
|
c = _run_session(ctx, grbl, fc,
|
||||||
|
lambda c: c.get("verdict") == "OK" and
|
||||||
|
set(CRASH_GATES) <= set(c.get("gates_off") or []),
|
||||||
|
"watch off")
|
||||||
|
ev["off"] = c
|
||||||
|
ctx.check(set(CRASH_GATES) <= set(c.get("gates_off") or []),
|
||||||
|
"gates_off %s does not name the three crash gates", c.get("gates_off"))
|
||||||
|
ctx.check(c.get("verdict") == "OK", "the watch at zero did not read OK: %s", c)
|
||||||
|
|
||||||
|
# Restore, and prove the gates read ok again.
|
||||||
|
_set_gates(ctx, fc, orig)
|
||||||
|
restored = True
|
||||||
|
c = _run_session(ctx, grbl, fc,
|
||||||
|
lambda c: not (set(CRASH_GATES) & set(c.get("gates_off") or [])),
|
||||||
|
"restored")
|
||||||
|
ev["restored"] = c
|
||||||
|
ctx.check(not (set(CRASH_GATES) & set(c.get("gates_off") or [])),
|
||||||
|
"the crash gates stayed off after the restore: %s", c.get("gates_off"))
|
||||||
|
finally:
|
||||||
|
if not restored:
|
||||||
|
st, body = fc.post("/settings", params=orig)
|
||||||
|
ctx.log("restore on failure: POST /settings %s -> %s", orig, st)
|
||||||
|
after = fc.settings()
|
||||||
|
ctx.check(all(after.get(k, "") == orig[k] for k in CRASH_KEYS),
|
||||||
|
"settings not restored: %s", {k: after.get(k) for k in CRASH_KEYS})
|
||||||
|
ctx.check(fc.wait_idle(60, abort=ctx.aborted), "machine did not return to idle")
|
||||||
|
|
||||||
|
|
||||||
@test("cooling.critical-tier", title="The coolant critical line is a fault above the ceiling's pause",
|
@test("cooling.critical-tier", title="The coolant critical line is a fault above the ceiling's pause",
|
||||||
subsystem="cooling", kind="auto", mode="grbl", est_min=3,
|
subsystem="cooling", kind="auto", mode="grbl", est_min=3,
|
||||||
covers=_COOL_COVERS + [("forgectrl", "src/main.c")], requires=["cooling.gate-off"],
|
covers=_COOL_COVERS + [("forgectrl", "src/main.c")], requires=["cooling.gate-off"],
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# only SRCREV and PV here - the image manifest leaves *-pin.inc out of the
|
# only SRCREV and PV here - the image manifest leaves *-pin.inc out of the
|
||||||
# layer content hash because the component entry already identifies the
|
# layer content hash because the component entry already identifies the
|
||||||
# pinned source (forgefirm-image-manifest.bbclass).
|
# pinned source (forgefirm-image-manifest.bbclass).
|
||||||
SRCREV = "c77f040b954e531ca01b460d5567e23d811737a8"
|
SRCREV = "161686fddab446270c63bd3c4cfec169f1b14277"
|
||||||
PV = "0.1.0"
|
PV = "0.1.0"
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ WHO_AM_I = 0x0F
|
|||||||
WHO_AM_I_LIS2HH12 = 0x41
|
WHO_AM_I_LIS2HH12 = 0x41
|
||||||
CTRL1 = 0x20
|
CTRL1 = 0x20
|
||||||
CTRL4 = 0x23 # bits 5:4 = FS (00=+/-2g, 10=+/-4g, 11=+/-8g)
|
CTRL4 = 0x23 # bits 5:4 = FS (00=+/-2g, 10=+/-4g, 11=+/-8g)
|
||||||
CTRL7 = 0x26 # bit 0 = LIR1 (latch IG_SRC1, read-to-clear)
|
CTRL7 = 0x26 # bit 2 = LIR1 (latch IG_SRC1, read-to-clear;
|
||||||
|
# bit 3 = LIR2, bits 1:0 are the 4D enables)
|
||||||
OUT_X_L = 0x28
|
OUT_X_L = 0x28
|
||||||
IG_CFG1 = 0x30 # AOI,6D,ZHIE,ZLIE,YHIE,YLIE,XHIE,XLIE
|
IG_CFG1 = 0x30 # AOI,6D,ZHIE,ZLIE,YHIE,YLIE,XHIE,XLIE
|
||||||
IG_SRC1 = 0x31 # IA(6),ZH,ZL,YH,YL,XH,XL
|
IG_SRC1 = 0x31 # IA(6),ZH,ZL,YH,YL,XH,XL
|
||||||
@@ -182,7 +183,7 @@ def main():
|
|||||||
# Arm IG1: latch the source (CTRL7 LIR1), set the thresholds and
|
# Arm IG1: latch the source (CTRL7 LIR1), set the thresholds and
|
||||||
# duration, then enable the axes last. Preserve CTRL7's other bits.
|
# duration, then enable the axes last. Preserve CTRL7's other bits.
|
||||||
ctrl7 = rd(fd, CTRL7)
|
ctrl7 = rd(fd, CTRL7)
|
||||||
wr(fd, CTRL7, ctrl7 | 0x01)
|
wr(fd, CTRL7, ctrl7 | 0x04)
|
||||||
wr(fd, IG_THS_X1, ths)
|
wr(fd, IG_THS_X1, ths)
|
||||||
wr(fd, IG_THS_Y1, ths)
|
wr(fd, IG_THS_Y1, ths)
|
||||||
wr(fd, IG_THS_Z1, ths)
|
wr(fd, IG_THS_Z1, ths)
|
||||||
|
|||||||
Reference in New Issue
Block a user