Tile electronics: EXO-410 contact board + EXO-420 controller board
The functional tile (v1). The decorative v0 tile (EXO-2xx, SLA with snap cups) is unchanged; this replaces it when
the helmet has to measure something. Source: exo/pcb/ (geometry, circuit as code, placement, checks) and pcb/tile.mjs (tscircuit: build, autoroute,
Gerbers). Generated outputs: out/pcb/.
Status: designed and generated, not fabricated, not powered, nothing measured. Every number below is a design value from a datasheet or a calculation, and says which.
What changed from v0
| v0 (visual) | v1 (this) | |
|---|---|---|
| tile | curved SLA carrier, 64 snap cups | two flat 6-layer PCBs stacked, identical on all 16 tiles |
| contact | 3 mm brass ball snapped in a cup | turned, gold-plated ball-pin with a spring plunger, screwed into an M1.6 SMT nut reflowed on the board |
| contacts per tile | 64 | 60 EEG + 4 laser optical encoders (PAT9125EL, VCSEL) at the corners, in place of the corner pins |
| helmet | 1,024 decorative | 960 EEG channels + 64 laser optical encoders |
| pin length | one | six lengths, 7.5–12.5 mm in 1 mm steps (190 / 334 / 284 / 113 / 35 / 4 per helmet): the flat board is 6.4 mm farther from the scalp at its corners than at its centre |
| shell inner face | 10.2 mm off the scalp | 19.8 mm + the controller board behind it (≈ 22.5 mm). The flat boards cost ≈ 12 mm of helmet thickness |
One design for all 16 tiles. The layout builds every tile frame so the contacts lie on the same regular grid in the tile's own x/y, so the PCBs are identical. Only the pin lengths differ, and the shell's standoffs carry each board at its tile's position.
Architecture
scalp
│ ball-pin (spring plunger, Ø3 gold ball) ── M1.6 SMT nut (F, scalp side of EXO-410)
│ 100 kΩ ── 100 kΩ (F, between the nuts) single fault: 3.3 V / 200 kΩ = 16.5 µA
│ ── via ── MCP6V31 follower (B, behind the pin) active electrode: the high-Z node is ~2 mm long
│ 1 µF ── AIN ── 2.2 MΩ to GND AC coupling: dry-contact DC offsets never reach the PGA
│ ADS131M08 × 8 (B): gain 32, OSR 2048, 2.048 MHz CLKIN → 500 SPS, AINxN = buffered, high-passed REF
│ 4 corners (F): PAT9125EL laser optical encoder (lateral slip; lift from its image-quality registers)
│ │ 4 × 10-pin 0.4 mm DF40 pairs, one per corner (the only nut-hole-free spots on the back)
└─ EXO-420 controller: RP2040 + 16 Mbit flash + 12 MHz crystal, digital LDO, 12-pin FPC to the hub
FPC: 3.6 V, GND, REF (driven), 2.048 MHz CLK, SYNC, UART 3 Mbaud TX/RX
hub (one per helmet, next document): driven-right-leg + reference contact, the clock and SYNC for every converter,
16 UART receivers, battery, host link (exocortex-2026-05-A head-bridge protocol)
Why a buffer behind every pin. A dry contact is 100 kΩ–1 MΩ+. The ADS131M08's input impedance is 1 MΩ at gain ≥ 8 (datasheet), so without a buffer every contact would be a variable voltage divider and the common-mode rejection would follow the contact quality. The ADS1299 avoids this with ~1 GΩ inputs. A first-principles chain buys the same thing with a 21 µA zero-drift follower per pin, and gets an active electrode in the bargain: the high-impedance trace is the 2 mm from the nut to the follower, not a wire across the board.
Why AC coupling. At gain 32 the ADS131M08's range is ±37.5 mV. Dry contacts sit at offsets of hundreds of mV that drift with sweat and pressure. A 1 µF / 2.2 MΩ high-pass (0.07 Hz; ≈ 0.23 Hz with the converter's input impedance in parallel) removes them, and the AINxN side gets the same high-passed reference through its own follower, so each channel is HP(electrode − REF).
Why two boards. A follower, coupling capacitor and bleed resistor per pin, plus 8 converters, fill the contact board's back (the placer reports 63 % courtyard fill with 0201 passives, 58 % on the front). The MCU, flash, crystal, digital LDO and FPC do not fit as well. Splitting gives an analog-only board facing the scalp, shielded by its ground planes from a digital board behind it. The link is four 10-pin DF40 pairs, one per corner: with a Ø2.5 pilot hole at every pin, the corners are the only places on the back with room for a connector, and four pairs also hold the stack.
What came from CommandAGI-EEG-001 (CommandAGI/commandagi, seed/drive/platform/user-commandagi/hardware/CommandAGI-EEG-001):
- 2 × 100 kΩ 0.1 % in series on every signal and reference contact (theirs: ee/bank.circuit.ts).
- One shared converter clock (2.048 MHz, the same frequency) and one SYNC for every converter on the head.
- One bias drive and one reference contact per head, not per tile.
- The data frame (32-byte header with EEG! magic, sequence, µs timestamp, flags; CRC32; latched fault causes), the
128 ms bounded queue, and "overflow is a fault, never a silent drop".
- The faults check (walk every contact to its first semiconductor pin and fail the build if the series resistance is
short of the table), ported to exo/pcb/check.py.
- What not to copy: its leakage argument compares a DC current with the 100 µA AC limit. The DC limits for a type
BF applied part are lower (from memory of IEC 60601-1 table 3: 10 µA normal, 50 µA single fault; verify against the
standard). Here the single-fault bound is 3.3 V / 200 kΩ = 16.5 µA.
Noise budget (0.5–100 Hz, per channel)
| source | density | in-band rms | basis |
|---|---|---|---|
| dry contact, 1 MΩ (thermal) | 129 nV/√Hz | 1.29 µV | √(4kTR), R typical for dry gold on hair-covered scalp; the contact dominates |
| 2 × 100 kΩ protection | 57 nV/√Hz | 0.57 µV | √(4kTR) |
| MCP6V31 voltage noise | ~50 nV/√Hz | 0.50 µV | datasheet class value (confirm: datasheet agent) |
| MCP6V31 current noise × 1 MΩ | small (chopper ~fA/√Hz) | < 0.1 µV | confirm |
| ADS131M08, gain 32, OSR 2048 | — | 1.00 µV over its 250 Hz band, ≈ 0.63 µV in 100 Hz | datasheet table 7-1 |
| total | ≈ 1.6 µVrms | the same chain with an ADS1299 would be ≈ 1.4 µVrms: the contact sets the floor either way |
Power budget (per tile)
| current at 3.3 V | basis | |
|---|---|---|
| 8 × ADS131M08, very-low-power mode | 8 × (1.9 + 0.18) mA = 16.6 mA | datasheet: noise depends only on OSR and gain, not on the power mode |
| 62 × MCP6V31 | 1.3 mA | 21 µA each |
| RP2040 at 48 MHz | ≈ 15 mA | typical |
| 4 × PAT9125EL, run mode | 4 × 0.7 mA | datasheet v1.2 |
| tile | ≈ 36 mA ≈ 120 mW | 16 tiles ≈ 1.9 W plus the hub |
The same 960 channels on ADS1299s would draw ≈ 5 W for the converters alone. 2.4 W spread over the head is warm, not hot; the thermal check needs the built helmet.
Optical corners: what they can and cannot do
Each corner has a PAT9125EL: a laser (850 nm VCSEL) optical encoder with no lens, 1–30 mm working distance, 0.7 mA. It reports lateral slip (Δx, Δy) of whatever surface it sees, and its image-quality / shutter registers drop when the surface lifts away, a distance proxy. Four per tile give reference signals for adaptive motion-artifact cancellation (the "closed loop" is a filter, not an actuator). A separate ToF/proximity part was dropped: the corner has room for one sensor, and the VCNL36826S is marked not for new designs.
- Through hair they track the hair, not the scalp. They work where the corner sits over skin or thin hair; elsewhere they report hair motion, which still correlates with cap slip. Nothing here is proven: bench it on a head first.
- Skin oils change the contact's electrical impedance, which optics cannot see. The right sensor for that is the contact itself: inject a small known current at a frequency outside the EEG band through the reference/bias path and measure each channel's response (the ADS131M08 has no built-in lead-off; the hub's bias driver injects, the tiles measure). That is the oil/contact-quality compensation; the optics handle motion.
Pins and nuts
- Nut: M1.6 SMT nut, hex 3.18 A/F, 1.5 mm high (Sinhoo SMTSO1615MTJ, LCSC C2928168; PEM SMTSO-M1.6 class), Ø4.2 pad, Ø2.5 unplated pilot hole through the board (PEM drawing). These 60 holes are why the back is laid out in 45° cells.
- Pin (
EXO-5LLL, CAD inexo/pcb/pin.py, STEP inout/pcb/pins/): M1.6 × 0.35 thread into the nut, 2.5 A/F hex collar, Ø2.0 barrel (≥ 2.35 mm for the spring), Ø1.3 plunger with 1.5 mm travel, Ø3.0 ball head; brass, gold over nickel. Six lengths, 7.5–12.5 mm. A custom part from a pogo-pin or turned-part shop (JLCCNC does not list brass turning; quote PCBWay CNC and pogo-pin makers).
Toolchain
Circuit, placement and checks are Python (exo/pcb/); the boards are built and exported with tscircuit in plain Node
(pcb/tile.mjs, no KiCad install). EXO-420 is autorouted there. EXO-410 is not: tscircuit's autorouter runs out of
iterations on its 1,295 pads at every effort level, even with GND and the supply left out, so tile.mjs builds it with
routing off and exo/pcb/route.py routes it with Freerouting (local, headless): placement → Specctra DSN → SES →
traces and vias back into circuit.json, pours recomputed around them, then its own DRC (every net one piece of
copper, no two nets closer than the clearance) and the Gerbers through pcb/export.mjs. GND and supply pads that can
take one get a filled, capped via in the pad (164 of them), so they reach their plane without a fanout.
exo/pcb/check.py runs the faults walk, the cross-board connector check, reads both boards' route.json and writes
the JLCPCB BOM/CPL; exo/pcb/render.py draws the routed copper and checks that the stacked connectors' pads coincide.
uv run python -m exo.pcb.geom && uv run python -m exo.pcb.circuit && uv run python -m exo.pcb.footprints
uv run python -m exo.pcb.place # ~1.5 min
cd pcb && npm install --allow-git=all --allow-remote=all && cd .. # npm 12 blocks tscircuit's git/tarball deps
node pcb/tile.mjs # EXO-420 build + autoroute (~1 min), EXO-410 build (~3 min)
uv run python -m exo.pcb.route EXO-410 # Freerouting: hours; needs Java 25 + freerouting-2.4.1.jar (JAVA, FREEROUTING)
uv run python -m exo.pcb.render && uv run python -m exo.pcb.check
route.py reads its rules from the environment: TRACE, CLEAR (mm), VIA (pad/drill), SIG4=1 (inner4 as a
fifth signal layer with the supply as a fill), FANOUT, PASSES, FR_ARGS (passed to Freerouting). Freerouting's
router uses one core, so variants run side by side.
Board stack-up (JLCPCB 6-layer, 1.0 mm, ENIG)
| layer | contact board | controller board |
|---|---|---|
| L1 F.Cu | nuts, protection resistors (0201), optics (faces the scalp) | 4 corner DF40 headers |
| L2 | GND plane (one solid ground) | GND |
| L3 | signal | signal |
| L4 | signal | signal |
| L5 | +3V3A plane | +3V3D plane |
| L6 B.Cu | followers, coupling (0201), converters at 45°, analog LDO, 4 corner DF40 receptacles; GND pour | RP2040, flash, crystal, LDO, FPC, debug pads; GND pour |
Rules: tscircuit's jlcpcb_standard preset; 0.1 mm default track. Plugged and capped vias in pads are free on 6 layers
at JLCPCB.
Still open (needs a bench, not more design)
Contact impedance and noise on a real head; motion artefacts and whether the corner optics help; pin force and comfort (60 springs per tile); leakage and fault injection; clock skew across 16 tiles; thermal; EMC; the hub board.