Skip to content

ADR-0023: Accelerometer Integration for Ambient CRT Glitches

The KN-86’s gameplay surface is evolving toward ambient CRT-character — pre-rendered Clip animations, algorithmic art, intentional signal-imperfection on what is otherwise a crisp HDMI panel. A 2026-04-27 design conversation produced a stronger proposal than any of the cartridge-authored effects considered to date: physical jostling of the case triggers brief, transient terminal glitches — scanline jitter, brightness flicker, single-row tracking wobble. The diegetic read is “the device is old and the CRT is misbehaving”; the engineering read is a 3-axis MEMS accelerometer feeding a small motion-event pipeline that nOSh probabilistically translates into the glitch catalogue.

This is a hardware-spec change. There is no accelerometer in the current canonical spec, and adding one ripples into BOM, mechanical mount, the keyboard MCU’s USB HID descriptor, and the nOSh runtime’s render path. Per Spec Hygiene Rule 3, the change requires an ADR with a Canonical Hardware Specification table delta and a project-wide grep sweep for stale claims; this ADR is that vehicle.

The decision is intentionally narrow. v1 is firmware-only. Cartridge authors get nothing. Gestures (tilt-to-scroll, shake-to-clear, orientation hints) are explicitly deferred. Cart-readable motion (a future (motion-vector) / (motion-event) FFI surface) is explicitly deferred. The ambient-glitch system is the only public producer in v1, and it has one sensor source (motion) and one private firmware-internal source (nosh_internal).

  1. Industrial design and BOM are about to commit. The Pelican 1170 inset-panel design and the v0.1 BOM are both in the bring-up window. Adding a sensor after the inset panels are cut, or after components are ordered, costs a redesign cycle. Now is when the sensor lands cheaply.
  2. The keyboard-MCU host slot is open. ADR-0018 names the keyboard controller as a QMK-compatible MCU (Pro Micro family, RP2040-class) reachable over the internal USB hub. Its I²C bus has no other claimant, and its USB HID descriptor has unclaimed Usage Page space for vendor-defined motion reports. This is the cheapest moment to fold the sensor into that controller — before the keyboard PCB is fabbed.
  3. Marty Glitch ships in the launch slate. The Gameplay Design review flagged that broadcast-corruption is Marty’s identity. If we ship motion-glitches without the carve-out for Marty, we erode the most distinctive cartridge in the launch slate. The carve-out has to be wired in from the start, not bolted on later.
  • v1 firmware-only. No NoshAPI FFI primitives for motion. The cart authoring surface (ADR-0005) is unchanged.
  • v1 pure ambient. No tilt, no shake, no orientation gestures. The motion path produces glitches and nothing else.
  • Pre-locked sensor host: keyboard MCU (Pro Micro / QMK-compatible controller). Same architectural argument as the keyboard matrix per docs/device/hardware/build-specification.md §“Why a USB HID keyboard over direct GPIO matrix”: real-time peripheral I/O off Linux userspace, reuse the existing USB HID transport, free Pi I²C / I²S pins for current and future peripherals. Symmetric pattern with the existing build. The Pi Zero 2 W is not the sensor host. The Pico 2 coprocessor (ADR-0017) is not the sensor host either — its three responsibilities (PSG synthesis, MAX98357A I²S audio, SSD1322 OLED ticker) are tempo-critical at sub-millisecond timescales; adding a 25–50 Hz sensor poll would require routing power and I²C to a third location and grant nothing the keyboard MCU isn’t already positioned to do.
  • Power envelope. CLAUDE.md battery row currently shows 145–235 mA typical (post-coprocessor band). Any active draw added by the sensor must be quantified at every Off/Low/Medium/High setting and accepted explicitly.
  • CIPHER-LINE invariant unchanged. The accelerometer never drives the CIPHER-LINE OLED. CIPHER glyphs do not glitch. ADR-0015’s CIPHER-OLED-exclusivity rule is preserved.
  • Marty Glitch identity protection. The broadcast-corruption visual layer (docs/software/cartridges/modules/marty-glitch-broadcast-piracy.md) belongs to Marty diegetically. Motion-glitches must hard-clamp to Off when Marty is the active cartridge regardless of user setting.
  • Tempo-critical phase protection. ICE Breaker (HUNTER pursuit), Drift (intercept window), Shellfire (extraction countdown). Motion-glitches must suppress during these phases regardless of user setting.
  • Audio frame budget protection. Drift’s audio-only navigation depends on stable PSG output. Glitch fires must not jitter the YM2149 PSG callback path. The Pico 2 coprocessor (ADR-0017) already isolates the audio callback from the Pi’s main loop, so this is structurally satisfied — but the ADR makes the requirement explicit and the firmware-design memo (Sub-task 3) verifies it.
  • Spec Hygiene Rule 3. This ADR’s Documentation Updates section is the authoritative checklist for stale-reference cleanup. Every doc that mentions hardware peripherals or current-draw bands must be reviewed in the same PR.

The KN-86 adopts a 3-axis MEMS accelerometer on the keyboard MCU’s I²C bus, sampled at 25 Hz on the keyboard controller, processed into discrete motion events on the controller, reported to the Pi Zero 2 W over the existing USB HID interface as a vendor-defined HID report, and consumed by a new nOSh ambient glitch system. Cartridge authors get nothing in v1; the entire path is firmware-internal.

Update — LIS3DH host pinned to master (LEFT) Sweep half by ADR-0031 §5 (2026-06-06): Under the Ferris Sweep split topology adopted in ADR-0031, “the keyboard MCU” is one of two MCUs. The LIS3DH lives on the master (LEFT) half’s MCU only — the master holds the USB HID interface to the Pi, so motion events reach nOSh without an inter-half TRRS hop. One sensor is sufficient; vibration coupling through the Pelican 1170 inset-panel structure propagates case-jostle events to both halves’ PCBs equally. Sensor selection (§1 LIS3DH), sample rate (§5 25 Hz), threshold detector (§5 0.4 g + 4-bucket classifier), USB HID vendor-report transport (§6), nOSh ambient-glitch system (§7), axis convention (§4), and the Marty Glitch hard-clamp carve-out are all preserved unchanged.

Second update — I²C peripheral moved from I²C0 to I²C1 by ADR-0032 §3 (2026-06-07): With 2× trackpoint added on the Sweep (one per half), I²C0 (D4/D5 = GP4/GP5) on the master half is consumed by the trackpoint’s PS/2 adapter. The LIS3DH moves to I²C1 (D6/D7 = GP6/GP7) on the same master KB2040. The originally-reserved INT1 line on D6 is released — v1 motion handling is polling-only at the 25 Hz ODR per §5 below, so the interrupt line is not currently used. If v2 interrupt-driven motion detection is ever wanted, a new ADR re-allocates a free pin (D8/GP8 is available). All other §5 / §6 / §7 commitments (sample rate, threshold, vendor HID report, nOSh consumer) are unchanged.

Concrete commitments:

1. Sensor selection — STMicroelectronics LIS3DH

Section titled “1. Sensor selection — STMicroelectronics LIS3DH”
  • Part: STMicroelectronics LIS3DH (LGA-16, 3 × 3 × 1.0 mm package). 3-axis MEMS accelerometer, ±2g / ±4g / ±8g / ±16g full-scale ranges (firmware uses ±4g), 16-bit output, I²C and SPI interfaces. The KN-86 uses I²C.
  • Rationale (vs. ADXL345 and LIS2DH12, the other two final candidates — full BOM memo lives on the parent Notion task GWP-308):
    • Power. LIS3DH offers a 2 µA “low-power 1 Hz” mode and an 11 µA active mode at 25 Hz, both materially below ADXL345 (40 µA active typical) and on par with LIS2DH12 (LIS2DH12 is the 2.0 × 2.0 mm shrink of the LIS3DH die — same silicon, smaller package, fewer distributors). At 25 Hz operating duty cycle the LIS3DH adds <0.1 mA to the typical-band; effectively free against the 145–235 mA envelope.
    • Library and toolchain support. LIS3DH has first-class drivers in Adafruit’s CircuitPython, Sparkfun’s Arduino libraries, and stock QMK has a community-contributed accel_lis3dh integration that the keyboard firmware can pull in directly. ADXL345 has equivalent Arduino support but no clean QMK path. LIS2DH12’s library coverage is thinner because the part is newer.
    • Sourcing. Adafruit, SparkFun, Pimoroni, and DigiKey all stock LIS3DH breakouts ($5.95 Adafruit ADA-2809, $9.95 SparkFun SEN-13963) and the bare IC ($1.50–2.20 qty 100 at DigiKey, qty 1 ~$3.50). LIS2DH12 sources mostly from STMicro distributors only — supply concentration risk. ADXL345 is widely sourced but at higher unit cost ($1.95 qty 1, $1.20 qty 100) and higher active current.
    • Form factor. A 3 × 3 × 1.0 mm LGA reflows cleanly onto the keyboard PCB next to the keyboard MCU. The Adafruit breakout is acceptable as a daughterboard fallback during prototype bring-up.
  • Datasheet: STMicroelectronics LIS3DH datasheet (DS6839 Rev 4).
  • Sourcing options (≥2): DigiKey 497-10613-1-ND (bare IC), Mouser 511-LIS3DH (bare IC), Adafruit ADA-2809 (breakout), SparkFun SEN-13963 (breakout).
  • Qty pricing: $3.50 qty 1 / $1.95 qty 100 (bare IC, DigiKey April 2026 reference).

2. Sensor host — keyboard MCU (Pro Micro / QMK-compatible controller per ADR-0018)

Section titled “2. Sensor host — keyboard MCU (Pro Micro / QMK-compatible controller per ADR-0018)”
  • Host: the same MCU that scans the 31-key matrix per ADR-0018 — Pro Micro (ATmega32U4), Sea-Picro / KB2040 (RP2040), or whichever QMK-compatible board the hardware agent picks during bring-up. The accelerometer shares the host MCU; no second microcontroller is added.
  • Bus: I²C on the keyboard MCU. ATmega32U4 has hardware TWI on PD0/PD1; RP2040-class boards have multiple I²C peripherals. Either is sufficient. Address default 0x18 (LIS3DH SA0 = GND) or 0x19 (SA0 = VDD); pick one and document in the keyboard schematic.
  • Why not the Pi Zero 2 W: real-time peripheral I/O off Linux userspace; symmetric with the keyboard-matrix decision (ADR-0018, build-spec §“Why a USB HID keyboard over direct GPIO matrix”); preserves Pi I²C lines for future peripherals; reuses the existing USB HID transport rather than introducing a fresh device-tree overlay and userspace daemon.
  • Why not the Pico 2 coprocessor: the Pico 2 owns sub-millisecond-jitter responsibilities (PSG audio callback, OLED ticker cadence). A 25 Hz accelerometer poll would either share a hot path with audio (risk) or sit on a separate I²C bus the Pico 2 doesn’t currently route (cost). Hosting on the keyboard MCU keeps the Pico 2’s responsibility set unchanged.
  • Memory budget. ATmega32U4 is the tighter target: 32 KB flash, 2.5 KB SRAM. The QMK keymap + matrix scan for 31 keys consumes ~14 KB flash / ~512 B SRAM in stock QMK with our keymap. Adding the LIS3DH driver (community-contributed, ~3 KB flash), a 16-byte sample ring buffer, and the threshold-event state machine (~512 B flash, 64 B SRAM) leaves ~14 KB flash and ~1.9 KB SRAM headroom. RP2040-class controllers have order-of-magnitude more headroom and are a non-issue.

3. Mount — accelerometer reflowed on the keyboard PCB

Section titled “3. Mount — accelerometer reflowed on the keyboard PCB”
  • Mount method: reflow the LIS3DH directly onto the keyboard PCB (ADR-0018 Option B, custom-fab unified 31-key board, preferred path) on the underside of the board near the keyboard MCU. No daughterboard, no breakout, no new PCB. This is the cheapest path: the keyboard PCB is being designed and fabbed anyway; one additional 3 × 3 mm footprint and four tracks (I²C SDA, SCL, VDD, GND) is a trivial layout addition.
  • Fallback for ADR-0018 Option C (split-PCB reconnected): if the split-PCB fallback is taken, mount the Adafruit LIS3DH breakout (ADA-2809) on the interior plate adjacent to the keyboard halves, wired to the keyboard MCU’s I²C pins. Less elegant but still single-MCU, single-USB-HID.
  • Vibration coupling. The keyboard PCB is screwed directly to the keyplate, which is screwed directly to the Pelican-1170 inset panel. The full case-jostle motion path is rigid: shell → inset panel → keyplate → keyboard PCB → sensor. No foam, no gel, no decoupling stage. A keyboard click transmits to the sensor and a hand-shake transmits to the sensor; the firmware threshold (§5) is what distinguishes the two, not the mount. The brief case-jostle motion the design wants to detect is already audible as a faint click on the Pelican shell, so we know the mechanical path conducts at the magnitudes we care about.
  • Constraint compatibility. The 220 × 155 × 32 mm clamshell (CLAUDE.md historical Case row, now superseded by the Pelican 1170 row) is unaffected; the sensor footprint is 9 mm² on a PCB that already exists. The Pelican 1170 shell is unmodified per ADR-0018 invariants.

4. Axis convention (pinned, plain English)

Section titled “4. Axis convention (pinned, plain English)”

The KN-86’s accelerometer axes, defined with the device held in normal operating orientation (clamshell open, primary display facing the operator, keyplate flat):

  • +X — to the operator’s right.
  • +Yup (away from the keyplate, toward the top of the open clamshell).
  • +Zout of the screen toward the operator (the gravity vector when the device is laid keyplate-down on a table is +Z; when held normally it is approximately +Z = horizontal, +Y ≈ +1 g component depending on tilt).

Every downstream document, spec, firmware comment, and Lisp constant — when motion FFI is eventually added in v2 — must use this convention. The keyboard PCB layout assumes this convention and the LIS3DH’s chip-marking dot identifies pin 1 such that the sensor’s native +X aligns with operator-right when reflowed correctly. The hardware agent verifies this at bring-up by tilting the assembled deck operator-right and confirming X reads positive in the keyboard MCU debug stream.

5. Sample rate, threshold, and motion-event extraction (keyboard MCU side)

Section titled “5. Sample rate, threshold, and motion-event extraction (keyboard MCU side)”
  • Sample rate: 25 Hz (LIS3DH ODR = 25 Hz mode).
    • Rationale: motion events of interest are case-jostle on the order of 50–500 ms. 25 Hz Nyquist (12.5 Hz) covers this comfortably. Higher rates buy nothing useful and burn power.
    • Power at 25 Hz: 11 µA per LIS3DH datasheet table 9 (low-power mode at 25 Hz ODR). Effectively zero against the 145–235 mA envelope.
  • Buffering: an 8-sample ring buffer on the MCU. Each sample is the L2 magnitude of (X, Y, Z) minus the running 1-second moving average (gravity removed). 320 ms history at 25 Hz.
  • Threshold detector: a peak-magnitude threshold of 0.4 g (configurable in keyboard firmware constant). When any sample in the ring buffer exceeds threshold, the MCU emits a single motion-event HID report. After firing, the MCU enters a 200 ms hardware-side cooldown to prevent floods on the wire (separate from nOSh’s per-class cooldowns in §7).
  • Magnitude bucket: the MCU classifies the peak into 4 coarse buckets (light, medium, heavy, extreme) at thresholds 0.4 g / 0.8 g / 1.4 g / 2.5 g. The Pi never sees raw samples.
  • What the Pi sees: discrete motion-event HID reports plus the bucket. Not a raw sample stream. This keeps the link bandwidth in single-digit reports per second under nominal use, and makes the Pi side of the pipeline trivially synchronous.

6. USB HID transport — vendor-defined HID report on the existing keyboard interface

Section titled “6. USB HID transport — vendor-defined HID report on the existing keyboard interface”
  • Transport choice: extend the keyboard’s existing USB HID interface with a vendor-defined report on a Vendor Usage Page (Usage Page 0xFF00, Usage 0x01). Not a separate USB device, not a composite second interface, not a Game Controller HID page. Keep the keyboard a keyboard from Linux’s perspective; the additional report co-exists on the same interface.
    • Why not composite (HID keyboard + HID gamepad): introduces a second /dev/input/eventN node, complicates the early-boot key-scan gate from ADR-0011 (which expects a single keyboard event device), and gains nothing — the motion data isn’t a gamepad axis.
    • Why not Game Controller HID page: semantically wrong (motion is not a control axis), and Linux’s evdev would synthesize ABS_X/ABS_Y/ABS_Z events that nOSh would have to filter out.
    • Why vendor-defined: clean. nOSh opens /dev/hidraw* for the keyboard, reads the vendor report, decodes the 4-byte payload ({event_id, bucket, peak_x, peak_y, peak_z} — payload is informational, only event_id and bucket drive the glitch system in v1). Keyboard input continues to flow through evdev exactly as before.
  • Report layout (v1): 6 bytes total. Byte 0: report ID (0x10). Byte 1: bucket (0=light, 1=medium, 2=heavy, 3=extreme). Bytes 2–5: signed-16-bit peak_x, peak_y peaks in milli-g, little-endian (4 bytes — the peak vector for the event, in the axis convention above; reserved for future use, not consumed by v1 nOSh).
  • Report rate cap. The MCU’s hardware-side 200 ms cooldown caps motion reports at 5/sec maximum on the wire. nOSh’s per-class quotas (§7) drop this further on the consumer side.

7. nOSh ambient glitch system (Pi Zero 2 W side, firmware-internal)

Section titled “7. nOSh ambient glitch system (Pi Zero 2 W side, firmware-internal)”

7.1 Setting model — Off / Low / Medium / High

Section titled “7.1 Setting model — Off / Low / Medium / High”

The user’s ambient-glitch setting lives in a new nOSh setting, persisted on the device microSD in the firmware-config store at /home/shared/nosh-config.toml (a new file owned by nOSh). Not in Universal Deck State. Universal Deck State is reserved for cross-cartridge gameplay fields per CLAUDE.md (“operator handle, credits, reputation, cartridge history bitfield, variable-length phase chain”); a UI preference does not belong there. The firmware-config store is a separate, smaller persistence target with the same on-microSD survival as deck state but a different audience (firmware) and write cadence (rare — only when the user changes the setting).

The four levels in concrete numbers:

SettingBaseline glitches/min (no motion)Peak glitches/min (during motion)Max single-glitch durationFade in / outWire-cap motion events used
Off00(sensor still polled; events dropped)
Low0up to 480 ms60 / 120 msup to 1 of every 5 motion events fires
Medium0.2 (~1 every 5 min)up to 8120 ms60 / 120 msup to 1 of every 3 motion events fires
High0.5 (~1 every 2 min)up to 16180 ms80 / 200 msup to 1 of every 2 motion events fires

Baseline glitches at Medium and High come from nosh_internal sources (low-battery, cart-insert / cart-eject, boot, sleep-wake) on a slow Poisson schedule. Motion is the only public producer; nosh_internal is firmware-only and not accessible to anyone outside nOSh.

Defined in v1 even though only motion and nosh_internal are public producers:

typedef struct {
glitch_source_t source; /* MOTION | NOSH_INTERNAL */
uint8_t intensity; /* 0–3, maps to LIS3DH bucket for motion-source */
uint16_t duration_ms; /* 80–180 ms per setting table above */
bool suppressed_by_modal; /* true → suppressed during PROMPTS / Lisp REPL / nEmacs */
bool gameplay_critical; /* true → suppressed during nosh_set_tempo_critical(true) */
} glitch_event_t;

This taxonomy is the v1 foundation. v2 cart-FFI exposure (a future ADR) adds CART as a third source; the cart-side primitive becomes a one-line registration call against this same struct, not a refactor. The firmware-design memo (Sub-task 3) names the file as kn86-emulator/src/nosh_glitch.h (and the on-device equivalent on the Pi) for the canonical definition.

7.3 Effect catalogue (v1) — three types, no exceptions

Section titled “7.3 Effect catalogue (v1) — three types, no exceptions”
  1. Brightness flicker. Whole-frame brightness drop to ~70% for the configured duration, then restore. Implemented as a one-frame composite alpha pass at the SDL layer; on the Pi the SDL surface composites against a translucent black overlay. Cheap, safe.
  2. Single-row scanline shift. One horizontal cell-row from rows 1–23 (cartridge content area; never row 0 or row 24 — the firmware rows are inviolable per CLAUDE.md Spec Hygiene Rule 5) shifts left or right by 1–3 columns for the duration, then snaps back. The row is chosen uniformly at random.
  3. Brief tracking wobble. A 2 cell-row vertical wobble that displaces the rendered framebuffer up by 1 row for half the duration, down by 1 row for the remaining half. Visually reads as VHS tracking, not a glitch storm.

Snow (full-screen noise) and full-screen sync rolls are explicitly deferred to v2 diegetic events — they are too large a region to fire during tempo-critical OODA cycles, and their visual identity belongs to Marty Glitch (broadcast-piracy module).

7.4 Trigger function (motion → glitch fire)

Section titled “7.4 Trigger function (motion → glitch fire)”

For each motion-event report received from the keyboard MCU, nOSh runs the following decision sequence:

  1. Modal suppressor check. If a PROMPTS modal, the Lisp REPL, or nEmacs is the focused surface, drop the event silently. Reading and typing precision is non-negotiable.
  2. Tempo-critical suppressor check. If nosh_set_tempo_critical(true) has been raised by the cell runtime (current firmware-callable, not exposed to cart Lisp in v1), drop the event silently.
  3. Marty-active suppressor check. If the active cartridge identifies as Marty Glitch (cart-id matches the Marty manifest entry), drop the event silently regardless of setting. Hard-clamp to Off; no override.
  4. Per-class quota check. Each glitch type carries its own quota:
    • Brightness flicker: minimum 4 s gap between fires of this class.
    • Scanline shift: minimum 8 s gap between fires of this class.
    • Tracking wobble: minimum 6 s gap between fires of this class. If the chosen class is in cooldown, drop the event silently. (Cooldown by event-class quotas, not a single global cap. A hand-shake won’t cascade into a glitch storm even within an overall cap.)
  5. Setting-level filter. Apply the wire-cap-motion-events ratio from the setting table (1:5 / 1:3 / 1:2 for Low / Medium / High; 0 for Off).
  6. Perception-stance latency (Option A — “device is old”). Schedule the glitch fire 200–400 ms after the motion event (uniform random within that window). If a key-down event arrives in nOSh’s input queue between the motion event and the scheduled fire, suppress the fire entirely — never glitch on the keypress immediately following motion. This breaks the causal loop the operator’s brain would otherwise close.
  7. Threshold jitter. Apply ±20% jitter to the firing magnitude bucket (light may behave like medium or stay light; never escalate above heavy). This compounds with the latency to make the glitch-fire feel discontinuous from the operator’s input.
  8. Fire. Pick a glitch class uniformly weighted by its remaining quota budget, queue the SDL composite pass.

The same gate is applied to nosh_internal events with steps 1–2 + 4–8 (Marty suppression doesn’t apply — Marty doesn’t generate nosh_internal events; only nOSh does).

nOSh’s render loop is event-driven with a 20 fps animation cap (CLAUDE.md Emulator Reference §“Event-driven redraw”). Glitch fires must NOT force continuous redraw — that would melt the battery for no gameplay benefit. Mechanism:

  • A glitch fire enqueues a one-shot composite pass on the next render tick.
  • The render loop schedules itself for one additional tick at fire+duration to clear the effect.
  • Between fire+duration and the next event, the render loop returns to idle. No animation cap pressure.

This adds at most 2 extra frames per glitch fire to the render path. At High setting peak (16 glitches/min), that’s ~32 extra frames/min above the idle floor — negligible.

The Pico 2 coprocessor (ADR-0017) owns YM2149 PSG synthesis with I²S DMA out. The Pi side of the audio path is psg-reg-write UART commands at register-change rate (typically <100 Hz). Glitch fires are SDL composite operations on the Pi; they have no path to the audio callback. By construction, glitch fires cannot jitter the PSG audio callback. Drift’s audio-only navigation requirement is structurally satisfied — no firmware-level guard is needed beyond keeping the glitch system out of the UART path (it has no business there in v1).

8. Carve-outs (mandatory; not setting-dependent)

Section titled “8. Carve-outs (mandatory; not setting-dependent)”
  • Marty Glitch (SETEC ASTRONOMY) — hard-clamp to Off. When the active cartridge is Marty Glitch, motion-glitches are forced to Off regardless of the user’s nOSh setting. The Marty cartridge identifies itself to nOSh on cart-load via the standard cart-manifest cart-id field; nOSh’s glitch system reads the cart-id at the trigger-function step 3 above and drops the event. No status-bar indicator, no modal — silent suppression. Marty owns the broadcast-corruption visual layer; the firmware does not compete with it.
  • Tempo-critical phases — suppress via nosh_set_tempo_critical(bool). ICE Breaker (HUNTER pursuit phase), Drift (intercept window), Shellfire (extraction countdown). The cell runtime raises nosh_set_tempo_critical(true) when these phases activate and lowers it on phase exit. The glitch system’s trigger-function step 2 drops events while raised. Other modules (CipherGarden, Threshold, BlackLedger, Pathfinder, etc.) do not raise this flag and see motion-glitches normally.
  • Modal focus — suppress. PROMPTS modals, Lisp REPL, nEmacs. Reading and typing precision is non-negotiable; trigger-function step 1.

9. Cart-FFI exposure: explicitly out of scope (v1)

Section titled “9. Cart-FFI exposure: explicitly out of scope (v1)”

Cartridges get nothing from the motion path in v1. No (motion-vector), no (motion-event), no (set-glitch). The taxonomy of §7.2 is firmware-internal. The future cart-FFI exposure that exposes a CART source on glitch_event_t.source and a small primitive set for cart-authored glitches is deferred to a future ADR — call it ADR-XXXX-cart-motion-ffi-v1, to be opened only after the v1 ambient-glitch system has shipped and burned in.

10. Beyond-glitches gestures: explicitly out of scope (v1)

Section titled “10. Beyond-glitches gestures: explicitly out of scope (v1)”

Tilt-to-scroll, shake-to-clear, orientation hints, and any motion-driven UX outside the ambient-glitch path are deferred to a future ADR. The v1 sensor data path produces glitches and nothing else.


Option A: Sensor on keyboard MCU, USB HID vendor report, three-effect catalogue (ACCEPTED)

Section titled “Option A: Sensor on keyboard MCU, USB HID vendor report, three-effect catalogue (ACCEPTED)”

Described above. Cheapest hardware integration, symmetric with the keyboard-matrix architecture, single MCU, single USB HID interface, no new device-tree work, predictable power impact, clean Marty / tempo-critical / modal carve-outs.

Option B: Sensor on the Pi Zero 2 W’s I²C0 bus

Section titled “Option B: Sensor on the Pi Zero 2 W’s I²C0 bus”

Wire LIS3DH directly to Pi GPIO2/3 (I²C0). Userspace daemon polls and forwards to nOSh.

Rejected because: introduces a new userspace daemon, a new device-tree overlay, and a userspace-scheduling latency band on the motion-event extraction path that the keyboard MCU simply doesn’t have. Also burns Pi I²C0, which the Pi-Zero Build Spec keeps free for future peripherals (haptic driver, RTC). Asymmetric with the keyboard-matrix decision (ADR-0018) — Josh’s pre-locked choice was specifically to keep this architecture symmetric.

Option C: Sensor on the Pico 2 coprocessor (ADR-0017)

Section titled “Option C: Sensor on the Pico 2 coprocessor (ADR-0017)”

Wire LIS3DH to a Pico 2 GPIO pair as a third I²C bus. Pico forwards motion-events over the existing UART command protocol.

Rejected because: the Pico 2’s three responsibilities (PSG synthesis, MAX98357A I²S audio, SSD1322 OLED ticker) are all sub-millisecond-jitter-sensitive. Adding a 25 Hz polling loop to the Pico’s main loop would require careful interrupt-priority work to not perturb the audio callback. The keyboard MCU has no such constraints — it scans a key matrix at ~1 kHz and otherwise idles. Net: hosting on the Pico costs us complexity that the keyboard MCU absorbs for free.

Option D: Cart-author FFI surface in v1 (motion exposed to cart Lisp directly)

Section titled “Option D: Cart-author FFI surface in v1 (motion exposed to cart Lisp directly)”

Expose (motion-vector) and (motion-event) to cart Lisp, let cart authors implement their own glitch effects.

Rejected because: Josh’s pre-locked v1 scope is firmware-only ambient glitches. Pulling cart-FFI forward would invite per-cartridge motion designs that we haven’t reviewed; any one of them could compete with Marty’s identity, sabotage Drift’s audio-only navigation, or trigger glitch storms during tempo-critical phases. The right path is firmware-only v1, ship the carve-outs, prove the integration is solid, then open the cart-FFI surface in a follow-up ADR. The taxonomy of §7.2 is built so that follow-up is a small additive change, not a rewrite.

Option E: Bigger effect catalogue (snow, full sync rolls, signal-loss frames) in v1

Section titled “Option E: Bigger effect catalogue (snow, full sync rolls, signal-loss frames) in v1”

Ship the full broadcast-corruption catalogue in the firmware ambient-glitch system from day one.

Rejected because: large-region effects can flip a decision in tempo-critical OODA cycles (the Gameplay Design review flagged this explicitly). The ambient-glitch system needs to read as small ambient anomalies, not visual interference. Also: the broadcast-corruption visual identity belongs to Marty Glitch. Two systems competing on the same vocabulary dilutes both. Snow and sync rolls are deferred to v2 diegetic events.


Against Option B (Pi-side host), Option A wins on architectural symmetry (matches the keyboard-matrix decision), latency (motion event extraction runs on a real-time MCU, not Linux userspace), and pin budget (Pi I²C0 stays free). The cost is a slightly more complex keyboard firmware footprint — ~3.5 KB additional flash and ~600 B SRAM on the ATmega32U4, both within budget. Acceptable.

Against Option C (Pico 2 host), Option A wins on Pico-side simplicity. The Pico 2 already runs at sub-millisecond jitter budgets for audio and OLED; a 25 Hz polling loop is small but the risk of perturbing the audio callback is real, and the gain is zero. The keyboard MCU has spare cycles; spending them on the sensor poll is the right allocation.

Against Option D (cart-FFI in v1), Option A wins on review cycle. v1 firmware-only is a single review surface (this ADR). v1 with cart-FFI is N+1 review surfaces (this ADR plus every cart that uses it). The Marty identity-protection problem is much easier to solve when there’s one producer (firmware) than when there’s a producer-per-cartridge.

Against Option E (bigger effect catalogue), Option A wins on tempo-critical safety. Three small, contained effects — flicker, single-row shift, single-row wobble — never disturb the operator’s read of a tempo-critical screen. Snow, sync rolls, and signal-loss frames cover too much screen area to share that property.

The cost of Option A — the chosen path:

  • One additional 3 × 3 mm chip footprint on the keyboard PCB. Trivial.
  • ~3.5 KB additional QMK firmware flash, ~600 B SRAM. In budget on ATmega32U4 with 14 KB / 1.9 KB headroom.
  • ~0.1 mA additional active current at 25 Hz. Effectively zero against the 145–235 mA envelope. Battery runtime impact: -0.1 mA at all Off/L/M/H levels for the sensor itself; +0–2 mA peak for the Pi-side glitch composites at High setting (negligible against the typical-band variance). At High setting the worst-case envelope is ~145.2–237.2 mA typical; runtime band remains 13–17 h. The accelerometer does not move the runtime needle in any setting.

  • Cheapest possible hardware integration. Reuses an MCU, an I²C bus, a USB HID interface, and a PCB that all already exist. One additional chip footprint, no new connectors, no new daemons, no new device-tree work.
  • Diegetic fit. “Old hardware acting up” is on-brand for the KN-86 retro-cyberpunk aesthetic and matches the device’s fiction without authoring effort per cartridge.
  • Marty Glitch identity protected. The hard-clamp carve-out preserves Marty as the cartridge that owns broadcast-corruption visuals.
  • Tempo-critical safety preserved. ICE Breaker / Drift / Shellfire suppression keeps the OODA-cycle modules clean.
  • v2 cart-FFI is a one-line registration, not a refactor. The taxonomy of §7.2 is forward-compatible.
  • Power envelope unchanged in any meaningful sense. The accelerometer is invisible to the runtime band.
  • Adds a sensor to the keyboard PCB. Hardware agent’s keyboard PCB design now includes the LIS3DH footprint and four I²C tracks. Trivial layout addition; small mental-overhead cost during bring-up.
  • Adds ~3.5 KB to keyboard firmware. Within ATmega32U4 budget; non-issue on RP2040 controllers.
  • Adds a vendor HID report to the keyboard descriptor. nOSh now reads /dev/hidraw* for the keyboard in addition to /dev/input/event* for matrix input. Two read paths from one device; not difficult, but a small new surface.
  • Adds a firmware-config store at /home/shared/nosh-config.toml. New persistence target with its own write/migrate cycle. Small; deck-state writer pattern can be reused.
  • Adds a small carve-out matrix to nOSh. Three carve-outs (Marty, tempo-critical, modal) all live in the trigger-function path; readable but a non-trivial slice of conditional logic.
  • F1 — Hardware: keyboard PCB layout adds LIS3DH footprint. Folded into the in-flight ADR-0018 PCB design cycle; no new task. Verify reflow + I²C continuity at bring-up.
  • F2 — Keyboard firmware: integrate LIS3DH driver + threshold detector + USB HID vendor report. Pulled from QMK community module + threshold state machine. Concrete sub-task to spawn at the start of the keyboard-firmware bring-up.
  • F3 — nOSh runtime: implement glitch_event_t, the trigger function, the three effects, the per-class quotas, and the carve-out matrix. New module nosh_glitch.c/.h. TDD per the standard project pattern.
  • F4 — nOSh setting: add Off/L/M/H to the settings screen, persist to /home/shared/nosh-config.toml. Settings-screen UI work + new config-store module.
  • F5 — Marty Glitch cart-id check. Verify the cart-manifest cart-id field is exposed to nOSh at cart-load time; if not, add the read path. (Likely already there via cart-manifest plumbing; verify.)
  • F6 — Tempo-critical hook. Confirm nosh_set_tempo_critical(bool) exists in the cell runtime; if not, add. Wire ICE Breaker, Drift, and Shellfire phase handlers to raise/lower it.
  • F7 — v2 ADR for cart-motion-FFI. Open after F2–F6 ship and burn in. Out of scope for this ADR.
  • F8 — v2 ADR for diegetic snow + sync rolls. Open after F2–F6 ship and burn in. Out of scope for this ADR.
  • F9 — Build-spec Accelerometer subsection. Added in this PR; see Documentation Updates.

Documentation Updates (REQUIRED — part of the decision, not aspirational)

Section titled “Documentation Updates (REQUIRED — part of the decision, not aspirational)”
  • CLAUDE.md — Canonical Hardware Specification table: insert new Accelerometer row between Audio and Speaker (alphabetic-by-domain placement adjacent to other I/O peripherals); reference this ADR.
  • docs/device/hardware/build-specification.md — add a §“Accelerometer” subsection under §2 Hardware Topology referencing this ADR; update §2 subsystem-roles table to add an Accelerometer row hosted on the keyboard MCU.
  • docs/architecture/KN-86-Haptic-Feedback-Addendum.md — §2D paragraph “If additional I2C peripherals are added later (IMU, RTC, external EEPROM)” — IMU is no longer hypothetical; reword to acknowledge ADR-0023 and clarify the Pi I²C bus is still free because the accelerometer is hosted on the keyboard MCU, not the Pi.
  • docs/adr/README.md — append entry for ADR-0023.
  • Project-wide grep sweep for stale “no accelerometer”, “no IMU”, “no motion sensor”, “without (any) sensor” claims — verified clean as of this PR (no matches outside the haptic-addendum line above, which is updated).
  • docs/_meta/definitive-articles.md — no change (this ADR is a working decision, not a new DEFINITIVE document).
  • kn86-emulator/src/types.h — no change in this PR. Future F3 work adds glitch_event_t to a new nosh_glitch.h; this ADR does not touch types.h directly.

A PR that lands this ADR without ticking these boxes fails review.


We started this thread asking whether a CRT effect could read as “the device is old” without leaning on cartridge authors to author per-cartridge glitches. The cheapest answer turned out to be a real motion sensor — a $3 MEMS accelerometer on the keyboard PCB — wired into a tiny ambient-glitch system in nOSh. The brief that drove this ADR pre-locked three unusually clean architectural choices: host the sensor on the keyboard MCU (symmetric with the matrix-on-Pro-Micro pattern), keep v1 firmware-only (no cart-FFI for now), and make the perception stance “device is old” rather than “I shook it and it glitched” (latency + threshold jitter + suppress-on-keypress break the causal loop). The Gameplay Design review added the load-bearing carve-outs: Marty Glitch hard-clamps to Off because broadcast-corruption is its identity, and tempo-critical phases (ICE Breaker pursuit, Drift intercept, Shellfire extraction) suppress because a glitch on the wrong frame can flip a decision. What changed: we now have a sanctioned motion-data path on the device, hosted on the same MCU as the keyboard matrix, with a forward-compatible event taxonomy that lets v2 add cart-FFI as a one-line registration. What stays the same: the Pi side of the runtime, the Pico 2’s audio path, the CIPHER-LINE OLED, and the cart authoring surface — all untouched. A future reader should care because this is the first sensor on the device, and it sets the pattern for anything that comes next: real-time peripheral, MCU-hosted, USB HID transport, firmware-internal first, cart-FFI later. Same shape as the keyboard. Same shape as future input modalities, if any.