Laser safety write-up (docs/SAFETY.md + diagram); bump pin: forgectrl a451e7c

docs/SAFETY.md documents the control board's laser safing chain as
reverse-engineered and bench-verified - parts, every net with its SoC pin,
Linux exposure and polarity, the logic, and the software layers ForgeFIRM
stacks on it - with docs/img/safety-chain.svg. Linked from the README.

BRINGUP: Next work item 11 records the interlock-latch finding (an open
loop never tripped the latch; the kernel now drives INTERLOCK_LATCH_RESET)
and the charge-pump watchdog readback, both code-complete and pinned,
bench validation on the next image.
This commit is contained in:
ScottW514
2026-08-15 11:03:15 -04:00
parent 43cc540a09
commit 21d3a482c6
5 changed files with 528 additions and 3 deletions
+3 -1
View File
@@ -9,6 +9,7 @@ panel, and a standard Grbl interface.
* [Installation Instructions](https://github.com/ScottW514/forgefirm/blob/master/INSTALL.md)
* [Build Instructions](https://github.com/ScottW514/forgefirm/blob/master/BUILD.md)
* [Connecting LightBurn](https://github.com/ScottW514/forgefirm/blob/master/docs/LIGHTBURN.md)
* [How the laser safing works](https://github.com/ScottW514/forgefirm/blob/master/docs/SAFETY.md)
* [Community Support](https://community.openglow.org)
## What it does
@@ -67,7 +68,8 @@ running job unattended — keep a fire extinguisher within reach. Read
[Before you cut — safety](docs/LIGHTBURN.md#before-you-cut--safety-read-this-first)
before your first job, and the
[Regulatory and legal](INSTALL.md#regulatory-and-legal) section before
installing.
installing. [How the laser safing works](docs/SAFETY.md) describes the hardware
safety chain and the software gates ForgeFIRM stacks on it.
**A very important warning: this is experimental software. Use of this software
could seriously maim or kill you or others, and voids your warranty. It is not
+42 -1
View File
@@ -2091,7 +2091,10 @@ accordingly ("Automatic — AP country, else World").
Interlock-trip recovery was exercised in commissioning runs without
one — the chain recovers when the condition clears. `cnc/laser_latch`
stays write-only (1 = lock), the driver's arm flow unlocks per job,
and `interlock_latch_reset` remains a readback.
and `interlock_latch_reset` remains a readback. **Amended 2026-08-15:**
the *interlock* latch never trips at all in ForgeFIRM — see Next work
item 11; the "recovery" seen in commissioning was the software
safety-door path, not the hardware latch.
**Bench items:** open the lid mid-job (expect `Door` at the sender,
motion parked, resume on close); jog and `$H` with the lid open
(refused); a Pro with an unjumpered interlock connector (expect the
@@ -2351,3 +2354,41 @@ accordingly ("Automatic — AP country, else World").
after an underrun cuts at the stale origin unless homing is
required (GRBL mode permits unhomed cutting; the underrun itself
alarms and unlinks the anchor).
11. **Interlock latch has no hardware trip path in ForgeFIRM (found
2026-08-15, bench-verified).** With the interlock connector unjumpered
at idle: EV_SW `interlock`=1 (loop open), `interlock_latch`=0 (not
tripped), `cnc/interlock_circuit`=13 (b4 INTERLOCK_RESET=0),
`interlock_latch_reset`=0. This matches the safing schematic: the
interlock latch (U23-2, CD4043B) has RESET = loop-closed and
SET = INTERLOCK_RESET (GPIO4_05) — an open loop only *releases* the
reset, and nothing in ForgeFIRM drives INTERLOCK_RESET (the driver
exposes it as a read-only readback, initialized low; the former
`interlock_reset` LED node that let userspace drive it is gone). So on
a machine with a real external lockout (Pro), an open loop does **not**
cut LASER_ON in hardware; enforcement is the GRBL safety-door hold on
switch code 5 and the cloud client's motion gate. Basic/Plus ship the
loop jumpered. **Decision + fix needed:** drive INTERLOCK_RESET high
whenever the loop is open and hold it until the loop closes, so Q2
blocks the LASER_ON gate in hardware (the CD4043B is set-dominant, so
the latch stays blocked until the SoC releases SET *and* the loop is
closed). **IMPLEMENTED 2026-08-15 (kernel-module, code-complete, bench
validation pending; kernel-module 015913b, meta-openglow 92d6e20 DTS +
897c175 pin, forgectrl a451e7c docs, all pushed and pins bumped
2026-08-15):** `src/cnc_interlock.{c,h}` — an in-kernel input
handler on the gpio-keys switch device (no DT change, GPIO stays with
gpio-keys) drives INTERLOCK_RESET high while EV_SW code 5 reads open,
from probe until the switch device attaches, and if it detaches
(unobservable = open); low only while an attached device reports the
loop closed. Pin init changed to `GPIOF_OUT_INIT_HIGH`. Proof so far:
host test `tests/interlock_test.c` (8 cases, `make -C tests check`,
new CI job `host-tests`) green; module cross-compiled clean against
the staged 6.12.20-fslc kernel with `KCFLAGS=-Werror`, MODPOST silent.
Ships with the next image flash (kernel changes are never hot-swapped);
bench re-run of this exact reading then expects `interlock_latch`=1 /
`interlock_circuit` b4=1 with the loop open, both clearing after it is
closed. Same batch: the charge-pump watchdog readback (`cnc/charge_pump_alive`,
`interlock_circuit` b5; GPIO1_08 = inverted one-shot Q, new
`charge-pump-alive-gpio` + GPIO_8 pad in the linux-fslc DTS — kernel
module and DTB must ship together, the pin is required at probe; DTB
compile-checked with cpp+dtc against the staged kernel). Full write-up
of the chain: `docs/SAFETY.md` (+ `docs/img/safety-chain.svg`).
+267
View File
@@ -0,0 +1,267 @@
# Laser safety: how the machine is kept from firing
This document describes the laser-safing design of a stock Glowforge running
ForgeFIRM: the discrete hardware chain on the factory control board, what the
i.MX6 can see and drive, and the software layers ForgeFIRM stacks on top. The
hardware chain is the safety boundary; software only ever adds gates in front
of it and never bypasses it.
Signal names follow the factory board's nets. `GPIOx_yy` is the i.MX6 GPIO;
the Linux name in parentheses is how ForgeFIRM exposes it (device-tree
`gpio-keys` switch, or `glowforge.ko` `/sys/glowforge/cnc` attribute).
---
## 1. Principle
```
lid closed (both switches) ─┐
SoC alive (charge pump) ──┴─▶ HV_ENABLE ─────────────────▶ PSU: HV supply may run
│
FIRE (per-tick stream bit) ─┐ ▼
button latch cleared ──┼─▶ LASER_ON ──▶ PSU: tube fires only when BOTH are true
interlock latch cleared ──┘
```
Two independent hardware outputs go to the laser power supply on J1:
- **HV_ENABLE (J1_16)** — high only while the lid is closed *and* the SoC is
actively retriggering a hardware one-shot ("charge pump"). A hung SoC, a
stuck GPIO or an open lid drops it in hardware.
- **LASER_ON (J1_12)** — the SoC's per-tick FIRE request, AND-gated behind
two hardware latches: the *button latch* (lid state + SoC lock, cleared only
by a physical button press) and the *interlock latch* (set by the SoC,
cleared only while the remote-interlock loop is closed — see §5 for what
that means in ForgeFIRM today).
The SoC cannot fire the tube by driving one pin. It has to keep the one-shot
alive, release its own lock, wait for a human to press the button while the
lid is closed, and then stream FIRE bits — and any of those conditions going
away kills emission in hardware, not in software.
---
## 2. The hardware chain
### 2.1 Parts on the control board
| Ref | Part | Role |
|---|---|---|
| U1 | SN74AHC123A dual retriggerable monostable, R ≈ 499 kΩ / C ≈ 1 µF (t_w ≈ 0.45–0.5 s) | Charge-pump watchdog: Q stays high only while CHG_PUMP keeps arriving; times out ~0.5 s after the last pulse |
| U5, U6 | SN74AHC14 hex Schmitt-trigger inverters | Level inversion / conditioning for every switch line and SoC readback |
| U17 | SN74AHC08 quad 2-input AND | The four gates: DOORS, HV_ENABLE, and the two-stage LASER_ON gate |
| U23 | CD4043B quad R/S latch (NOR type, active-high S/R, output enable tied high) | Latch 1 = button latch, latch 2 = interlock latch |
| U32 | 74AHC1G32 single 2-input OR | Lid-open OR SoC lock → button latch SET |
| U24 | 74AHC1G04 single inverter | E-STOP sense line = ¬HV_ENABLE |
| U18 | i.MX6 Solo | The SoC: drives CHG_PUMP, LATCH_RESET, INTERLOCK_RESET, FIRE; reads everything else |
### 2.2 Inputs
| Net | Source | Conditioning | SoC pin | Linux exposure | Meaning |
|---|---|---|---|---|---|
| DOOR_SW1 (L) | J4_13, lid switch pulled to 3.3 V when closed | U5-1 inverts | GPIO4_14 (ball T6) | `gpio-keys` code 0 `door1`, active low → **active = closed** | Left lid switch |
| DOOR_SW2 (R) | J4_12 | U5-2 inverts | GPIO1_06 (T3) | code 1 `door2`, active low → **active = closed** | Right lid switch |
| DOORS | U17-1 = DOOR_SW1 · DOOR_SW2 | U5-3 inverts | GPIO1_00 (T5) | code 3 `doors`, active low → **active = both closed** | The lid term the chain actually uses |
| BUTTON | J5_5, 12 V through the button, 27 kΩ / 8.7 kΩ divider (≈ 2.9 V when pressed) | U5-5 inverts | GPIO4_09 (U6) | code 2 `button`, active low → **active = pressed** | Big front button. Also the RESET input of the button latch |
| INTERLOCK_SW | J8, 12 V through the remote-interlock loop, 432 Ω / 165 Ω divider (≈ 3.3 V when the loop is closed); factory-jumpered on Basic/Plus, brought out on Pro | U6-2 inverts | GPIO1_09 (T2) | code 5 `interlock`, active high → **active = loop OPEN** | Also the RESET input of the interlock latch |
| CHG_PUMP watchdog Q | U1-1 Q (pin 13): /A = GND, /CLR = 3.3 V, B = CHG_PUMP; each rising edge retriggers | U6-6 inverts | GPIO1_08 (R5) | `cnc/charge_pump_alive` (logical), `interlock_circuit` bit 5 (raw, 0 = alive) | watchdog alive |
| Button latch state | U23-1 Q → U5-6 → U6-1 | double inversion | GPIO1_03 (R7) | `cnc/button_latch`, `interlock_circuit` bit 2 | 1 = latch SET (fire blocked / not armed), 0 = armed |
| Interlock latch state | U23-2 Q → U6-3 → U6-4 | double inversion | GPIO1_02 (T1) | code 6 `interlock_latch`, active high → **active = latch SET** | 1 = interlock latch blocking |
| LASER_ON readback | J1_12 net (U17-3 output) | U6-5 inverts | GPIO1_05 (R4) | `cnc/laser_on`, `laser_on_sampled`, `interlock_circuit` bit 0 (raw, active low) | The gated output — the only software-visible proof of emission permission |
| E-STOP | U24 = ¬HV_ENABLE | — | GPIO4_06 (W5) | code 4 `estop`, active high | Sense line, **not** an input: high whenever HV_ENABLE is low (idle), low while HV_ENABLE is alive |
| LASER_PGOOD | J1_14 (the supply's HV_OK line) | — | GPIO4_21 (P24) | `cnc/laser_pgood`, `laser_pgood_sampled` (active low) | Read as "power good" from the laser supply; what the supply actually signals on it is not fully characterized |
### 2.3 SoC outputs into the chain
| Net | SoC pin | Driven by | Effect |
|---|---|---|---|
| CHG_PUMP | GPIO3_24 (F22, `charge-pump-gpio`) | `glowforge.ko`: one 0→1→0 pulse at run start, then every 200 ms from a soft hrtimer **only while `state == running`**; forced low on stop, disable, unload and kernel panic | Retriggers U1-1 (t_w ≈ 0.45–0.5 s, so a 200 ms feed holds Q solidly high). No edges → Q falls within ~0.5 s → HV_ENABLE drops |
| LATCH_RESET | GPIO1_07 (R3, `latch-reset-gpio`, init HIGH) | `cnc/laser_latch` (1 = lock). Also drives the FIRE line to high impedance while locked | Into U32 with lid-open; SETs the button latch → LASER_ON blocked until the next button press |
| INTERLOCK_RESET | GPIO4_05 (P5, `interlock-latch-reset-gpio`, init HIGH) | `glowforge.ko`: high whenever the remote-interlock loop reads open, or until a switch device reporting the loop has attached; low only while an attached device reports it closed (in-kernel input handler on the gpio-keys switch, EV_SW code 5). Read back as `interlock_latch_reset` / `interlock_circuit` bit 4 | SET input of the interlock latch → LASER_ON blocked in hardware while the loop is open |
| FIRE (LASER_ENABLE) | GPIO2_30 (E22, `laser-enable-gpio`) | The SDMA script, from bit 4 of each pulse byte; Hi-Z whenever the latch is locked or no run is in flight | One input of the final LASER_ON AND gate |
Laser *power* (PWM2 on J1_13) is not part of the chain: it sets the tube
current setpoint and is not gated. Emission permission is FIRE ∧ chain; the
laser-off guarantee rests on FIRE, and the kernel drops FIRE within one tick
on end-of-data or underrun.
### 2.4 Logic
```
DOORS_OK = DOOR_SW1 · DOOR_SW2 (U17-1)
WDOG_ALIVE = U1-1 Q, retriggered by every CHG_PUMP rising edge
HV_ENABLE = DOORS_OK · WDOG_ALIVE (U17-4) → J1_16
E_STOP = ¬HV_ENABLE (U24 inverter) → GPIO4_06
Button latch (U23-1):
SET = ¬DOORS_OK + LATCH_RESET (U32 OR)
RESET = BUTTON pressed
Q1 = 1 → fire blocked; 0 → armed
Interlock latch (U23-2):
SET = INTERLOCK_RESET (SoC: high while the loop reads open or is unobservable)
RESET = interlock loop closed
Q2 = 1 → fire blocked
LASER_ON = FIRE · ¬Q1 · ¬Q2 (U17-2, U17-3) → J1_12
```
The CD4043B is set-dominant: while SET is high the latch cannot be cleared.
That ordering is what makes the button meaningful — a press only arms the
machine when the lid is closed *and* the SoC has already released its lock.
![Laser safing chain: lid switches, charge-pump watchdog, button latch, interlock latch and FIRE combine into HV_ENABLE and LASER_ON](img/safety-chain.svg)
### 2.5 What each condition does, in hardware alone
| Event | HV_ENABLE | LASER_ON | Recovery |
|---|---|---|---|
| Lid opens (either switch) | drops (DOORS_OK low) | drops immediately: ¬DOORS_OK SETs the button latch | close the lid, SoC lock released, **press the button** |
| SoC asserts LATCH_RESET (kernel `laser_latch=1`) | unchanged | blocked: button latch SET; FIRE line is also Hi-Z | `laser_latch=0`, then a button press |
| SoC stops toggling CHG_PUMP (hang, panic, stop, fault, underrun) | drops within one one-shot period | FIRE is parked by the same paths | next run restarts the feed |
| Button pressed with lid closed and lock released | — | armed (Q1 cleared) | — |
| Button pressed while lid open or lock held | — | stays blocked (SET is dominant) | — |
| Remote-interlock loop opens (Pro) | unchanged | blocked: the kernel drives INTERLOCK_RESET high on the switch edge, setting the interlock latch. Opening the loop by itself only releases the latch's RESET — the board has no direct trip path — so this SoC drive is what makes the interlock a hardware cut (see §3.1); software additionally parks the job on `interlock` | close the loop: the kernel releases INTERLOCK_RESET and the closed loop resets the latch |
| Interlock latch already SET | unchanged | blocked | closing the loop clears it |
Note the *E-STOP* line: despite the factory name it is an output of this
chain — the inverse of HV_ENABLE. It reads active on a healthy idle machine and
drops for the whole duration of any kernel run, the window in which the charge
pump is fed and HV_ENABLE is alive. Nothing in ForgeFIRM gates on it unless a
machine settings key opts in for a retrofitted circuit.
---
## 3. Software layers on top
Every layer below sits *in front of* the chain: it can only withhold FIRE, hold
the lock, or starve the charge pump. None can produce emission the hardware
would not allow.
### 3.1 `glowforge.ko` (kernel)
- **Laser latch** (`cnc/laser_latch`, write-only): 1 = lock. Locking drives
LATCH_RESET high (button latch SETs) *and* puts the FIRE line in high
impedance so the SDMA stream physically cannot raise it. **Locked by
default; every close of `/dev/glowforge` relocks.** Unlocking never restores
the FIRE drive while a run or ramp is in flight — only run start and the
resume waypoint do, and only if the latch is unlocked at that moment.
- **Charge pump only while running.** The 200 ms retrigger starts with the
run and the callback returns without rearming as soon as the state leaves
`running` (stop, halt, fault, underrun). Stop/disable/unload pin sets force
CHG_PUMP low.
- **FIRE backstop.** At end-of-data and on underrun the SDMA script drops FIRE
and the step lines within one tick; the FIRE line is parked Hi-Z at every
run end and only a latch unlock plus a new run restores it.
- **Interlock latch drive.** The board's interlock latch is reset by a closed
loop but can only be *set* by the SoC's INTERLOCK_RESET line; an open loop
alone does not trip it. The driver owns that line through an in-kernel
input handler on the gpio-keys switch device: it is high (latch set,
LASER_ON blocked) from probe until the switch device attaches, whenever the
loop reads open, and again if the switch device goes away — an
unobservable loop counts as open. Only an attached device reporting the
loop closed releases it, and the set-dominant latch then clears through
its own RESET. The policy is host-tested (`tests/interlock_test.c`).
- **Dead man's switch.** A feeder holds `/dev/glowforge` open with `flock
LOCK_EX`; if that fd closes while a program runs, the driver performs an
emergency stop, puts the head in its safe state and de-energizes the
thermal-loop heat sources.
- **Panic handler.** On a kernel panic the driver stops the EPIT and drives
the pins safe directly: FIRE Hi-Z, CHG_PUMP low, LATCH_RESET asserted,
steppers de-energized — because SDMA and EPIT would otherwise keep playing
the ring with no kernel alive.
- **Readbacks** (`interlock_circuit` bits 0–5, `laser_on[_sampled]`,
`laser_pgood[_sampled]`, `button_latch`, `charge_pump_alive`,
`interlock_latch_reset`) are
monitoring only; the driver enforces nothing from them. Bits 1, 3 and 4 are
driven outputs read back from the data register — bit 3 says what the
driver *commanded*, `laser_on` says what the chain *did*.
### 3.2 grblHAL controller (`grblHAL-glowforge`)
- **Operator-armed window.** The first laser-on of a job runs the arm flow on
the protocol thread: coolant fire verdict must be OK, a head must be present
(lens, air assist and beam detector live on it and the chain has no head
term), fans go to the run profile, the latch is unlocked, and the controller
then waits for the physical button (`laser_button_timeout_s`, default 300 s;
a timeout or soft reset relocks and aborts). The hardware button latch is
what the press clears — the software wait exists so the job does not start
streaming FIRE bits into a blocked gate.
- **Disarm.** After `laser_disarm_s` (default 60 s) of no laser use, or on
program end/abort, the controller relocks the latch, turns the button LED
off and stands the cooling profile down. The relock waits for the kernel to
finish the queue tail so a controlled stop can never leave FIRE driven.
- **Coolant fire gates.** The armed window requires a fresh `fire_ok` verdict
from the cooling engine (flow verification, over-temperature, lid-IR
emission witness); a stale or failed verdict relocks in-process.
- **Safety door.** `doors` (lid) and `interlock` (loop open) are the core's
safety-door signal: a running job parks and resumes on close. This is a
motion/UX gate; the lid is *also* cut in hardware by the button latch.
- **Head/motion witnesses.** Position counters are not proof of motion (the
step-stream drives are open loop); the head accelerometer is the motion
witness, and `beam_detect_analog` on the head is the live emission witness.
### 3.3 forgectrl (machine services)
- Holds `/dev/glowforge` for its lifetime (pulse-device broker) so controller
handovers never close the device, and **relocks the latch (`cnc/stop` +
`cnc/laser_latch=1`) on every transition out of a running child** —
unexpected death, mode switch, restart.
- The **cooling engine** is the sole owner of the thermal hardware and
publishes the fire verdict the controllers enforce; on a FIRE-class verdict
it writes `cnc/stop` + `cnc/laser_latch=1` itself.
- The **motion-liveness gate** refuses to hand a controller a machine whose
drivers may have wedged (counters running, motors dead) — a laser-safety
corollary of "counters are not motion".
- `/status` reports the switch map and `laser_locked` (`interlock_circuit`
bit 3) for the panel and telemetry; nothing in forgectrl reads the Grbl
socket for machine state.
### 3.4 Cloud mode
The factory-experience client runs behind the same kernel latch, charge-pump,
backstop and dead-man rules; the precomputed pulse file it loads is subject to
the same FIRE gating as the live stream.
---
## 4. What is proven, and how
Verified on the bench with a probe on the PSU-connector LASER_ON pin and the
kernel readbacks (the runbook `BRINGUP.md` holds the drill records):
- Latch **locked**: 40,000 streamed FIRE bits → PSU pin flat, `laser_enable`
0 — the lock severs the FIRE drive entirely.
- Latch **unlocked, chain unarmed** (no button press): `laser_enable` 1
mid-window, PSU pin flat, `laser_on` 0 — the AND gate holds.
- FIRE drop at end-of-data and at true underrun: ≤ 1 tick, both termination
paths.
- A latch unlock inside an acceleration ramp does not restore the FIRE drive
for the in-flight run; a locked latch survives a stop + resume replay.
- Armed kill mid-FIRE: emission tail equals the ring in-flight only
(15–171 ms), the latch relocks, the burn line ends abruptly.
- Switch bits 0–3, 5, 6 verified against physical state; bit 4 (`estop`)
characterized live: high at idle, low through any run.
---
## 5. Not yet established
Present gaps in the hardware picture. None of them changes the safety
argument (every gap is on the readback/sense side or is a "which part" question),
but each is worth closing:
- **`estop` toggles seen inside motion windows.** With the one-shot's
measured R/C (≈ 499 kΩ, ≈ 1 µF → t_w ≈ 0.45–0.5 s against a 200 ms feed)
the one-shot cannot gap during a healthy run, so those toggles are most
likely run boundaries (homing and jogs are several short runs) or a feed
late by more than ~250 ms; `charge_pump_alive` sampled through a run
settles it.
- **Interlock latch drive: bench validation pending.** The board itself has
no trip path from the loop to the latch's SET input (bench-verified with the
connector unjumpered: `interlock` open, `interlock_latch` clear while
INTERLOCK_RESET was held low). The kernel drive described in §3.1 closes
that gap; its host test is green and it ships with the next image, where the
expected reading with the loop open is `interlock_latch` set,
`interlock_circuit` bit 4 set, clearing after the loop is closed.
- **`laser_pgood` (HV_OK, J1_14) semantics** are not fully characterized.
+215
View File
@@ -0,0 +1,215 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1500 860" width="1500" height="860" font-family="Segoe UI, Helvetica, Arial, sans-serif" font-size="13">
<title>Glowforge laser safing chain as run by ForgeFIRM</title>
<desc>Lid switches, the charge-pump watchdog, the button latch, the interlock latch and the FIRE line combine on the factory control board into HV_ENABLE and LASER_ON for the laser power supply; the SoC drives four lines and reads back the rest.</desc>
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#222"/>
</marker>
<style>
.wire { fill: none; stroke: #222; stroke-width: 2; }
.wire-out { fill: none; stroke: #222; stroke-width: 2; marker-end: url(#arr); }
.dot { fill: #222; }
.gate { fill: #ffffff; stroke: #222; stroke-width: 1.6; }
.gate-l { font-weight: 700; font-size: 13px; text-anchor: middle; fill: #111; }
.gate-s { font-size: 10px; text-anchor: middle; fill: #555; }
.pin { font-size: 10px; fill: #444; }
.in-tag { fill: #e9ebee; stroke: #6b7280; stroke-width: 1.2; }
.out-tag { fill: #dbeafe; stroke: #2563eb; stroke-width: 1.2; }
.rb-tag { fill: #fef3c7; stroke: #d97706; stroke-width: 1.2; }
.psu-tag { fill: #fee2e2; stroke: #dc2626; stroke-width: 1.2; }
.tag-t { font-size: 12px; font-weight: 700; fill: #111; }
.tag-s { font-size: 10px; fill: #444; }
.region { fill: #f6f7f9; stroke: #b8bec8; stroke-width: 1; stroke-dasharray: 5 4; }
.region-l { font-size: 12px; font-weight: 700; fill: #4b5563; letter-spacing: 0.4px; }
.net { font-size: 11px; font-style: italic; fill: #333; }
.note { font-size: 12px; fill: #222; }
.title { font-size: 18px; font-weight: 700; fill: #111; }
</style>
</defs>
<rect width="1500" height="860" fill="#ffffff"/>
<text x="20" y="30" class="title">Glowforge control board — laser safing chain, as run by ForgeFIRM</text>
<!-- regions -->
<rect x="20" y="52" width="212" height="640" class="region"/>
<text x="30" y="70" class="region-l">PHYSICAL INPUTS</text>
<rect x="250" y="52" width="1010" height="640" class="region"/>
<text x="260" y="70" class="region-l">CONTROL-BOARD SAFING LOGIC (hardware)</text>
<rect x="1290" y="52" width="192" height="640" class="region"/>
<text x="1300" y="70" class="region-l">LASER POWER SUPPLY (J1)</text>
<!-- ===================== Row A: lid switches ===================== -->
<rect x="40" y="90" width="172" height="28" rx="5" class="in-tag"/>
<text x="50" y="109" class="tag-t">LID_SW1</text><text x="122" y="109" class="tag-s">J4_13 · closed = 1</text>
<rect x="40" y="130" width="172" height="28" rx="5" class="in-tag"/>
<text x="50" y="149" class="tag-t">LID_SW2</text><text x="122" y="149" class="tag-s">J4_12 · closed = 1</text>
<path class="wire" d="M212 104 H330"/>
<path class="wire" d="M212 144 H310 V116 H330"/>
<rect x="330" y="84" width="64" height="44" rx="6" class="gate"/>
<text x="362" y="103" class="gate-l">AND</text><text x="362" y="118" class="gate-s">U17-1</text>
<path class="wire" d="M394 106 H740 V136 H760"/>
<circle cx="470" cy="106" r="3.5" class="dot"/>
<text x="480" y="98" class="net">DOORS_OK (both lid switches closed)</text>
<!-- doors readback -->
<path class="wire" d="M560 106 V126"/>
<rect x="500" y="126" width="150" height="24" rx="5" class="rb-tag"/>
<text x="508" y="142" class="tag-t">doors</text><text x="552" y="142" class="tag-s">EV_SW 3 · GPIO1_00</text>
<!-- ===================== Row B: charge-pump watchdog / HV_ENABLE ===================== -->
<rect x="262" y="176" width="190" height="40" rx="5" class="out-tag"/>
<text x="270" y="192" class="tag-t">CHG_PUMP</text><text x="352" y="192" class="tag-s">GPIO3_24</text>
<text x="270" y="207" class="tag-s">200 ms pulses, only while running</text>
<path class="wire" d="M452 196 H500"/>
<rect x="500" y="174" width="90" height="44" rx="6" class="gate"/>
<text x="545" y="192" class="gate-l">one-shot</text><text x="545" y="207" class="gate-s">U1-1 · t_w ≈ 0.45 s</text>
<path class="wire" d="M590 196 H620 V156 H760"/>
<circle cx="620" cy="196" r="3.5" class="dot"/>
<text x="630" y="190" class="net">WDOG_ALIVE</text>
<!-- charge_pump_alive readback -->
<path class="wire" d="M620 196 V222"/>
<rect x="540" y="222" width="212" height="24" rx="5" class="rb-tag"/>
<text x="548" y="238" class="tag-t">charge_pump_alive</text><text x="672" y="238" class="tag-s">GPIO1_08 (¬Q)</text>
<!-- AND U17-4 -->
<rect x="760" y="124" width="64" height="44" rx="6" class="gate"/>
<text x="792" y="143" class="gate-l">AND</text><text x="792" y="158" class="gate-s">U17-4</text>
<path class="wire-out" d="M824 146 H1300"/>
<circle cx="880" cy="146" r="3.5" class="dot"/>
<text x="900" y="166" class="net">HV_ENABLE</text>
<!-- U24 inverter -> estop -->
<path class="wire" d="M880 146 V88 H930"/>
<rect x="930" y="70" width="64" height="36" rx="6" class="gate"/>
<text x="962" y="86" class="gate-l">NOT</text><text x="962" y="99" class="gate-s">U24</text>
<path class="wire" d="M994 88 H1010"/>
<rect x="1010" y="76" width="236" height="24" rx="5" class="rb-tag"/>
<text x="1018" y="92" class="tag-t">estop</text><text x="1060" y="92" class="tag-s">EV_SW 4 · GPIO4_06 · = ¬HV_ENABLE</text>
<!-- PSU tag HV_ENABLE -->
<rect x="1300" y="132" width="172" height="28" rx="5" class="psu-tag"/>
<text x="1308" y="151" class="tag-t">J1_16</text><text x="1354" y="151" class="tag-s">HV_ENABLE</text>
<text x="1300" y="180" class="note">HV runs only while this</text>
<text x="1300" y="196" class="note">line is alive (pulsed).</text>
<!-- ===================== Row C: button latch ===================== -->
<!-- ¬DOORS_OK -->
<path class="wire" d="M470 106 V190 A6 6 0 0 1 470 202 V288 H500"/>
<rect x="500" y="270" width="64" height="36" rx="6" class="gate"/>
<text x="532" y="286" class="gate-l">NOT</text><text x="532" y="299" class="gate-s">U5-4</text>
<path class="wire" d="M564 288 H584 V280 H600"/>
<text x="565" y="266" class="net">lid open</text>
<!-- LATCH_RESET -->
<rect x="262" y="296" width="190" height="40" rx="5" class="out-tag"/>
<text x="270" y="312" class="tag-t">LATCH_RESET</text><text x="366" y="312" class="tag-s">GPIO1_07</text>
<text x="270" y="327" class="tag-s">cnc/laser_latch: 1 = lock (FIRE also Hi-Z)</text>
<path class="wire" d="M452 316 H584 V300 H600"/>
<!-- OR U32 -->
<rect x="600" y="268" width="64" height="44" rx="6" class="gate"/>
<text x="632" y="287" class="gate-l">OR</text><text x="632" y="302" class="gate-s">U32</text>
<path class="wire" d="M664 290 H720"/>
<!-- button latch -->
<rect x="720" y="272" width="124" height="76" rx="6" class="gate"/>
<text x="782" y="292" class="gate-l">Button latch</text>
<text x="782" y="306" class="gate-s">U23-1 · CD4043B</text>
<text x="782" y="320" class="gate-s">set-dominant</text>
<text x="726" y="294" class="pin">S</text>
<text x="726" y="336" class="pin">R</text>
<text x="832" y="314" class="pin">Q</text>
<!-- BUTTON -->
<rect x="40" y="332" width="172" height="28" rx="5" class="in-tag"/>
<text x="50" y="351" class="tag-t">BUTTON</text><text x="118" y="351" class="tag-s">J5 · pressed = 1</text>
<path class="wire" d="M212 346 H700 V332 H720"/>
<path class="wire" d="M330 346 V356"/>
<rect x="270" y="356" width="150" height="24" rx="5" class="rb-tag"/>
<text x="278" y="372" class="tag-t">button</text><text x="326" y="372" class="tag-s">EV_SW 2 · GPIO4_09</text>
<!-- Q1 -> NOT -> AND2 -->
<path class="wire" d="M844 311 H890"/>
<circle cx="868" cy="311" r="3.5" class="dot"/>
<rect x="890" y="293" width="64" height="36" rx="6" class="gate"/>
<text x="922" y="309" class="gate-l">NOT</text><text x="922" y="322" class="gate-s">U5-6</text>
<path class="wire" d="M954 311 H975 V380 H1000"/>
<text x="958" y="345" class="net">¬Q1</text>
<!-- button_latch readback -->
<path class="wire" d="M868 311 V262"/>
<rect x="770" y="238" width="222" height="24" rx="5" class="rb-tag"/>
<text x="778" y="254" class="tag-t">button_latch</text><text x="864" y="254" class="tag-s">GPIO1_03 · = Q1 · 1 = set</text>
<!-- ===================== Row D: interlock latch ===================== -->
<rect x="262" y="462" width="190" height="40" rx="5" class="out-tag"/>
<text x="270" y="478" class="tag-t">INTERLOCK_RESET</text><text x="386" y="478" class="tag-s">GPIO4_05</text>
<text x="270" y="493" class="tag-s">1 while loop open or unobserved</text>
<path class="wire" d="M452 476 H720"/>
<rect x="720" y="458" width="124" height="76" rx="6" class="gate"/>
<text x="782" y="478" class="gate-l">Interlock latch</text>
<text x="782" y="492" class="gate-s">U23-2 · CD4043B</text>
<text x="782" y="506" class="gate-s">set-dominant</text>
<text x="726" y="480" class="pin">S</text>
<text x="726" y="522" class="pin">R</text>
<text x="832" y="500" class="pin">Q</text>
<!-- INTERLOCK loop -->
<rect x="40" y="504" width="172" height="40" rx="5" class="in-tag"/>
<text x="50" y="520" class="tag-t">INTERLOCK loop</text><text x="160" y="520" class="tag-s">J8</text>
<text x="50" y="535" class="tag-s">jumpered on Basic/Plus · closed = 1</text>
<path class="wire" d="M212 518 H720"/>
<path class="wire" d="M330 518 V540"/>
<rect x="270" y="540" width="262" height="24" rx="5" class="rb-tag"/>
<text x="278" y="556" class="tag-t">interlock</text><text x="340" y="556" class="tag-s">EV_SW 5 · GPIO1_09 · active = open</text>
<!-- Q2 -> NOT -> AND2 -->
<path class="wire" d="M844 497 H890"/>
<circle cx="868" cy="497" r="3.5" class="dot"/>
<rect x="890" y="479" width="64" height="36" rx="6" class="gate"/>
<text x="922" y="495" class="gate-l">NOT</text><text x="922" y="508" class="gate-s">U6-3</text>
<path class="wire" d="M954 497 H985 V400 H1000"/>
<text x="958" y="470" class="net">¬Q2</text>
<!-- interlock_latch readback -->
<path class="wire" d="M868 497 V546"/>
<rect x="770" y="546" width="236" height="24" rx="5" class="rb-tag"/>
<text x="778" y="562" class="tag-t">interlock_latch</text><text x="878" y="562" class="tag-s">EV_SW 6 · GPIO1_02 · = Q2</text>
<!-- ===================== Row E: LASER_ON ===================== -->
<rect x="1000" y="368" width="64" height="44" rx="6" class="gate"/>
<text x="1032" y="387" class="gate-l">AND</text><text x="1032" y="402" class="gate-s">U17-2</text>
<path class="wire" d="M1064 390 H1110"/>
<rect x="1110" y="378" width="64" height="44" rx="6" class="gate"/>
<text x="1142" y="397" class="gate-l">AND</text><text x="1142" y="412" class="gate-s">U17-3</text>
<!-- FIRE -->
<rect x="850" y="600" width="232" height="40" rx="5" class="out-tag"/>
<text x="858" y="616" class="tag-t">FIRE</text><text x="896" y="616" class="tag-s">GPIO2_30 (laser_enable)</text>
<text x="858" y="631" class="tag-s">SDMA pulse-byte bit 4 · Hi-Z when locked or idle</text>
<path class="wire" d="M1082 620 H1094 V410 H1110"/>
<!-- LASER_ON out -->
<path class="wire-out" d="M1174 400 H1300"/>
<circle cx="1210" cy="400" r="3.5" class="dot"/>
<text x="1182" y="392" class="net">LASER_ON</text>
<path class="wire" d="M1210 400 V446"/>
<rect x="1112" y="446" width="146" height="24" rx="5" class="rb-tag"/>
<text x="1120" y="462" class="tag-t">laser_on</text><text x="1182" y="462" class="tag-s">GPIO1_05</text>
<!-- PSU tag LASER_ON -->
<rect x="1300" y="386" width="172" height="28" rx="5" class="psu-tag"/>
<text x="1308" y="405" class="tag-t">J1_12</text><text x="1354" y="405" class="tag-s">LASER_ON</text>
<text x="1300" y="434" class="note">The tube fires only while</text>
<text x="1300" y="450" class="note">this line is high (and HV</text>
<text x="1300" y="466" class="note">is enabled). Power comes</text>
<text x="1300" y="482" class="note">from PWM on J1_13, which</text>
<text x="1300" y="498" class="note">is not part of the chain.</text>
<!-- ===================== equations ===================== -->
<text x="262" y="612" class="note" font-weight="700">HV_ENABLE = DOORS_OK · WDOG_ALIVE</text>
<text x="262" y="630" class="note" font-weight="700">LASER_ON = FIRE · ¬Q1 · ¬Q2</text>
<text x="262" y="652" class="note">Button latch: SET = lid open OR LATCH_RESET; RESET = button pressed. Cleared only by a press while the lid is closed and the lock released.</text>
<text x="262" y="670" class="note">Interlock latch: SET = INTERLOCK_RESET (driven by glowforge.ko while the loop reads open); RESET = loop closed. Both latches are set-dominant.</text>
<!-- ===================== legend ===================== -->
<rect x="20" y="712" width="1462" height="128" rx="6" fill="#ffffff" stroke="#b8bec8"/>
<text x="34" y="734" class="region-l">LEGEND</text>
<rect x="34" y="746" width="26" height="16" rx="3" class="in-tag"/>
<text x="68" y="759" class="note">physical switch or loop, wired to the board</text>
<rect x="34" y="770" width="26" height="16" rx="3" class="out-tag"/>
<text x="68" y="783" class="note">line driven by the SoC (glowforge.ko)</text>
<rect x="34" y="794" width="26" height="16" rx="3" class="rb-tag"/>
<text x="68" y="807" class="note">line read by the SoC — gpio-keys switch (EV_SW code) or /sys/glowforge/cnc attribute; monitoring only</text>
<rect x="34" y="818" width="26" height="16" rx="3" class="psu-tag"/>
<text x="68" y="831" class="note">output to the laser power supply</text>
<text x="760" y="759" class="note">Boxes are logic functions on the control board (part reference below each). Wires carry logic levels: 1 = true as named.</text>
<text x="760" y="777" class="note">Software can only withhold FIRE, hold LATCH_RESET, drive INTERLOCK_RESET, or stop feeding CHG_PUMP —</text>
<text x="760" y="795" class="note">every one of those makes the hardware block emission. Nothing on the SoC side can add an emission path.</text>
<text x="760" y="817" class="note">Also read: door1 / door2 (EV_SW 0 / 1, GPIO4_14 / GPIO1_06), the two lid switches individually;</text>
<text x="760" y="833" class="note">laser_pgood (GPIO4_21), the supply's HV_OK line on J1_14.</text>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

@@ -9,7 +9,7 @@ PV = "0.1.0"
SRC_URI = "git://github.com/ScottW514/forgectrl.git;protocol=https;branch=main"
# Pinned; bump deliberately after pushing forgectrl changes.
SRCREV = "73eda9a5a47c4a94c6df40fa039226129f53d2f9"
SRCREV = "a451e7cd6e6d41cb5ef1d640badca0e8b4b5724b"
S = "${WORKDIR}/git"