Skip to content

KN-86 Coprocessor Protocol — Pi ↔ Pico UART Wire Format

⚠ CART-BUS FRAME TYPES OBSOLETED by ADR-0019 on 2026-04-24.

ADR-0019 partially supersedes ADR-0017 by removing the DMG cartridge bus responsibility from the Pi Pico 2. Cartridges are now a full-size SD card carried in a custom two-piece clamshell sled, read by the Pi as USB mass storage through a card reader bridge IC on the internal hub planned in ADR-0018. The Pi Pico 2 no longer touches the cartridge bus; its remaining responsibilities are YM2149 PSG synthesis with I2S out to MAX98357A and the SSD1322 CIPHER-LINE OLED driver.

Concrete consequences for this spec:

  • Obsolete frame types (cart-bus): CART_DETECT (0x10), CART_READ_BANK (0x11), CART_READ_BANK_DATA (0x12), CART_READ_SRAM (0x13), CART_WRITE_SRAM (0x14), CART_RESET (0x15), CART_READ_SRAM_DATA (0x16), and the CART_INSERTED / CART_REMOVED payloads of the EVENT frame (0xE0). The 0x100x1F type range is vacated; types in this range MUST NOT be used by conforming Pico firmware (F2) or Pi userspace daemon (F3). Pico implementations MAY return ERR_UNKNOWN_TYPE (§6) if a cart-related type byte is received, for the same reason any vacated type byte triggers it.
  • Canonical frame types (PSG + OLED + session control, all unaffected): HELLO (0x01), VERSION_QUERY (0x03), VERSION_RESPONSE (0x04), PSG_REG_WRITE (0x20), PSG_RESET (0x21), PSG_BULK_WRITE (0x22), OLED_SET_ROW (0x30), OLED_SCROLL_ROW (0x31), OLED_FILL (0x32), OLED_CLEAR (0x33), EVENT (0xE0, non-cart payloads only), ERROR (0xF0).
  • Obsolete error codes (cart-bus): ERR_CART_BUS_ERROR (0x42) and any other cartridge-domain entries in §6 are vacated; reserved for future use.
  • Obsolete §s in the body: §4.4–§4.8 (CART_DETECT / CART_READ_BANK / CART_READ_BANK_DATA / CART_READ_SRAM / CART_WRITE_SRAM), §4.17 (CART_RESET), §4.18 (CART_READ_SRAM_DATA), the CART_INSERTED / CART_REMOVED event payload tables in §4.15, and any cart-related rows in §3 frame allocation, §5.3 bootstrap, §6 error codes, §7 latency budget, and §10 gap resolution log.

The body below is retained as design history and as the source of truth for the still-canonical PSG + OLED + session-control frame types. Cart-bus sections carry per-section obsolete banners; do not cite them as current; follow ADR-0019’s udev + USB-MSC model for cartridge mount / unmount events instead. End-to-end cartridge bring-up is described in ADR-0019 CART-01 through CART-07 (mounted SD as a standard block device; cartridge loader = read() against the mounted filesystem).

Parent decision: docs/architecture/adr/ADR-0017-realtime-io-coprocessor.md (Approved 2026-04-24; cart-bus role partially superseded by ADR-0019 the same day). Related: docs/architecture/adr/ADR-0019-cartridge-storage-and-form-factor.md (cartridge form factor — full-size SD via USB-MSC; supersedes ADR-0013 and removes the cart-bus role from this spec), docs/architecture/adr/ADR-0017-realtime-io-coprocessor.md (parent decision; partially superseded by ADR-0019), docs/architecture/adr/ADR-0015-cipher-line-auxiliary-display.md (CIPHER-LINE layout — drives the OLED frame types), docs/architecture/adr/ADR-0018-custom-mechanical-keyboard-build.md (internal USB hub topology — the SD card reader bridge IC sits next to the keyboard controller, downstream of the same hub), docs/architecture/adr/ADR-0011-pi-zero-firmware-update-system.md (firmware update path), docs/architecture/adr/ADR-0013-cartridge-physical-format.md (superseded by ADR-0019; retained for design history), CLAUDE.md Canonical Hardware Specification.

This document is the wire-level specification for the UART link between the Raspberry Pi Zero 2 W (host) and the Raspberry Pi Pico 2 (realtime I/O coprocessor). ADR-0017 commits the link’s role, bus parameters, and the shape of the frame format. This spec commits the bytes: every payload layout, every error code, every timeout, every recovery flow. Pico firmware (F2) and Pi userspace daemon (F3) must conform to every canonical clause; cart-bus clauses (called out per-section by the banners below) are obsolete per ADR-0019 and MUST NOT be implemented.

Canonical hardware values (UART pins, processors, audio sample rate, OLED layout, cartridge mapper) are NOT restated here. See CLAUDE.md Canonical Hardware Specification per Spec Hygiene Rule 1.


Bus parameters below are committed by ADR-0017 §4 and reproduced here only as operating context.

  • Bus: UART, 3-wire (TX, RX, common ground). Full duplex, software framing only (no RTS/CTS).
  • Baud: 1,000,000 (1 Mbps), 8N1. Effective throughput 100,000 B/s; each octet costs 10 µs of transit time.
  • Endianness: little-endian for every multi-byte integer in this spec.

Design rationale — endianness. LE matches both endpoints’ native CPU layout, avoiding byte-swapping in the Pico’s hot path (PSG register dispatch at 44.1 kHz cadence). Big-endian “network byte order” was rejected — this is a private bus, not Internet traffic.


Every frame on the wire follows the structure committed by ADR-0017 §4:

+--------+--------+------+-----+----------+----------+----------+
| len_lo | len_hi | type | seq | payload | crc16_lo | crc16_hi |
+--------+--------+------+-----+----------+----------+----------+
FieldWidthSemantics
len2 B (LE)Total frame length in bytes, envelope + payload. Min 6 (zero-byte payload), max 1024 (§2.1).
type1 BFrame type identifier (§3).
seq1 BSequence number, 0–255 (§2.2).
payload0–1018 BType-specific (§4).
crc162 B (LE)CRC-16/CCITT-FALSE over [type, seq, payload] (§2.3). The CRC does NOT cover len — the receiver uses len to know how many bytes follow before validating.

Envelope overhead = 2+1+1+2 = 6 bytes.

MAX_FRAME_LEN = 1024. Caps payload at 1018 bytes. Drivers: Pico SRAM budget (1 KB ≪ 264 KB), and tail-latency variance (10.24 ms transit at the cap is already a meaningful slice of the Pi’s render-loop budget). The largest payload-bearing frame is CART_READ_BANK_DATA at ~270 bytes; no other frame approaches the cap.

  • Pi-originated requests carry seq ∈ [1, 255], monotonically incremented modulo 256 (skipping 0). The Pi MUST NOT reuse a seq while its request is outstanding (§5.1 timeouts).
  • Pico responses echo the request’s seq exactly. This is how the Pi correlates CART_READ_BANK_DATA chunks to the originating request.
  • Pi-originated fire-and-forget (PSG_*, OLED_*) carry seq = 0. No ack from the Pico. Errors surface asynchronously as ERROR(seq=0) or EVENT(BUFFER_OVERFLOW).
  • Pico-originated events (EVENT) carry seq = 0.
  • Heartbeat replies echo the heartbeat request’s seq (§5.2).

The Pi maintains a 256-entry table indexed by seq mapping outstanding requests to type and timeout deadline. Responses with unknown seq are dropped and logged.

ParameterValue
Polynomial0x1021
Initial value0xFFFF
Input/output reflectionnone (process MSB first)
Final XOR0x0000

Reference test vector: ASCII "123456789" (9 bytes) → 0x29B1. Both endpoints MUST validate this at boot before raising the link to ready.

The CRC covers [type, seq, payload] — every byte between the length field and the CRC field. CRC bytes are appended LE (crc16_lo = crc & 0xFF, crc16_hi = (crc >> 8) & 0xFF).

Design rationale — CRC-16/CCITT-FALSE vs Fletcher-16. CRC-16 catches all single-bit errors, all double-bit errors within a 4096-bit frame, all odd-bit-count errors, and all burst errors of length ≤ 16 bits. UART noise tends to come in bursts (clock drift, power-rail transients), so burst-error coverage wins. Fletcher-16 is faster but has weaker burst properties. Both endpoints have CPU headroom for a 256-entry table-driven implementation.

  1. IDLE: read len_lo, advance.
  2. LEN_HI: read len_hi, compute len. If len < 6 or len > 1024, raise ERR_MALFORMED_FRAME (Pico-side: emit ERROR(seq=0); Pi-side: log and drop) and return to IDLE.
  3. HEADER: read type and seq.
  4. PAYLOAD: read len - 6 bytes.
  5. CRC: read crc16_lo, crc16_hi. Compute expected CRC; on mismatch raise ERR_CRC_MISMATCH and return to IDLE. Otherwise dispatch.

Resync on garbage: any state-read with no byte for 10 ms flushes the buffer and returns to IDLE. There is no preamble byte — the link relies on length/CRC validation to reject garbage.

UART BREAK condition (long-zero on RX) is treated as an explicit session reset signal. Both endpoints MUST flush parser state to IDLE, emit nothing in response to the BREAK itself, and on the Pi side trigger §5.3 bootstrap re-handshake. The Pico’s mirror is to wait for the Pi’s HELLO; if no HELLO arrives within 1 s of detecting BREAK, the Pico emits an unsolicited HELLO(role=Pico, flags=HANDSHAKE) per §5.2 to nudge the Pi. BREAK is the only mechanism for explicit cross-side resync outside the heartbeat-degraded path; firmware bring-up and emergency recovery use it. The Pico’s UART driver MUST detect BREAK via the silicon-level framing-error indication, not by software byte-level monitoring.

Design rationale — fixed 16-bit length vs varint. Fixed length lets the parser know immediately how many bytes to read; varint would add a variable-length parse step in the receive ISR. The 16-bit width is sized to the cap, not the typical frame; the average overhead is one extra byte and is amortised against the payload.


TypeNameDirectionSequence semantics
0x01HELLObidirectionalrequest + response (echo seq); also reused for heartbeat (§5.2)
0x03VERSION_QUERYPi→Picorequest (response echoes seq)
0x04VERSION_RESPONSEPico→Piresponse only
0x10CART_DETECTObsolete per ADR-0019. Type byte vacated.
0x11CART_READ_BANKObsolete per ADR-0019. Type byte vacated.
0x12CART_READ_BANK_DATAObsolete per ADR-0019. Type byte vacated.
0x13CART_READ_SRAMObsolete per ADR-0019. Type byte vacated.
0x14CART_WRITE_SRAMObsolete per ADR-0019. Type byte vacated.
0x15CART_RESETObsolete per ADR-0019. Type byte vacated.
0x16CART_READ_SRAM_DATAObsolete per ADR-0019. Type byte vacated.
0x20PSG_REG_WRITEPi→Picofire-and-forget (seq=0)
0x21PSG_RESETPi→Picofire-and-forget (seq=0)
0x22PSG_BULK_WRITEPi→Picofire-and-forget (seq=0)
0x30OLED_SET_ROWPi→Picofire-and-forget (seq=0)
0x31OLED_SCROLL_ROWPi→Picofire-and-forget (seq=0)
0x32OLED_FILLPi→Picofire-and-forget (seq=0)
0x33OLED_CLEARPi→Picofire-and-forget (seq=0)
0xE0EVENTPico→Piunsolicited (seq=0); cart-related event payloads (CART_INSERTED, CART_REMOVED) obsolete per ADR-0019 — non-cart payloads remain canonical
0xF0ERRORPico→Piresponse (seq echoes offending request, or 0 if no correlation)

Reserved ranges (post-ADR-0019, including the vacated 0x100x16 cart-bus block): 0x00, 0x050x0F, 0x100x1F (the entire cart-bus type range, vacated by ADR-0019), 0x230x2F, 0x340x3F, 0x400xDF, 0xE10xEF, 0xF10xFF. Unknown types MUST be rejected with ERR_UNKNOWN_TYPE — never silently dropped (§6).

This was the complete set after applying the v0.1 gap-resolution amendments approved 2026-04-24 (added: CART_RESET, CART_READ_SRAM_DATA, PSG_BULK_WRITE; UART BREAK semantics — see §10). ADR-0019 (Accepted 2026-04-24, the same day as ADR-0017) subsequently removed the cart-bus role from the Pico 2; the cart-related rows above are retained struck-through for design history. The canonical v0.2 surface is the un-struck rows: PSG (0x200x22), OLED (0x300x33), session control (HELLO / VERSION_QUERY / VERSION_RESPONSE / EVENT non-cart payloads / ERROR).


Every payload below has been verified by hand for offset/width consistency. Variable-length payloads are stated as K + N where N is a runtime length.

Payload (6 B):

OffsetWFieldSemantics
01role0x01 = Pi, 0x02 = Pico.
11flagsbit 0 = HANDSHAKE (1 = initial wake, 0 = heartbeat); bits 1–7 reserved (must be 0).
24nonceLE uint32. Sender-generated random; receiver echoes verbatim for replay defence + correlation.

Sum: 1+1+4 = 6 B. Frame total: 12 B.

  • Initial handshake: Pi sends flags = 0x01 + fresh nonce. Pico responds with flags = 0x01, role = 0x02, nonce echoed, same seq.
  • Heartbeat: Pi sends flags = 0x00 every 5 s. Pico echoes (§5.2).

Errors: ERR_MALFORMED_FRAME (reserved flag bits set); ERR_OUT_OF_RANGE (role not in {0x01, 0x02}).

Payload: zero bytes. Frame total: 6 B. Pico responds with VERSION_RESPONSE (same seq).

4.3. VERSION_RESPONSE (0x04) — Pico→Pi

Section titled “4.3. VERSION_RESPONSE (0x04) — Pico→Pi”

Payload (11 B):

OffsetWFieldSemantics
01proto_majorCoprocessor protocol major. v0.1 ships 0.
11proto_minorProtocol minor. v0.1 ships 1.
21fw_majorPico firmware semver major.
31fw_minorPico firmware semver minor.
41fw_patchPico firmware semver patch.
54build_idLE uint32. Lower 32 bits of the Pico firmware’s git commit hash. Opaque to the Pi; used in bug reports.
92capsLE uint16 capability bitfield (below).

Sum: 1+1+1+1+1+4+2 = 11 B. Frame total: 17 B.

Capability bits (LSB first): bit 0 = MBC1 read support (obsolete per ADR-0019; bit reserved); bit 1 = MBC3 read support (obsolete per ADR-0019; bit reserved); bit 2 = MBC5 read+write (obsolete per ADR-0019; bit reserved); bit 3 = SSD1322 16-level grayscale; bit 4 = I2S audio output (always set); bits 5–15 reserved (must be 0 in v0.2). Conforming v0.2 Pico firmware MUST clear bits 0–2.

Version mismatch behaviour: the Pi compares proto_major against its expected major. On mismatch the Pi raises a hard fail per ADR-0017 §5: log, display COPROCESSOR PROTOCOL MISMATCH (v<pi> vs v<pico>) on Row 24, refuse to enter the nOSh runtime. The Pico’s mirror is to raise ERR_VERSION_MISMATCH on the next non-VERSION request from the Pi. proto_minor differences are warnings only.

⚠ Obsolete per ADR-0019. The Pico no longer drives the cartridge bus; cartridge presence is now a udev mass-storage device-arrival event on the Pi side. Type byte 0x10 is vacated and MUST NOT be used by conforming v0.2 firmware. Section retained for design history.

Request: empty payload. Frame total: 6 B.

Response payload (23 B):

OffsetWFieldSemantics
01present0x00 = empty slot; 0x01 = cartridge detected. If 0x00, all subsequent fields are zero-filled.
11mapper0x00 = ROM-only; 0x01 = MBC1; 0x03 = MBC3; 0x05 = MBC5; 0xFF = unknown. Per ADR-0013 first-party carts are MBC5.
22rom_banksLE uint16. 16 KB ROM banks. Range 2 (32 KB) to 512 (8 MB).
41sram_banks8 KB SRAM banks. Range 0 to 16 (128 KB).
52header_checksumLE uint16. The cart’s own header checksum at DMG offset 0x14E. Used by the Pi to disambiguate carts with identical titles.
716titleDMG header offset 0x134–0x143. NUL-padded; not necessarily NUL-terminated.

Sum: 1+1+2+1+2+16 = 23 B. Frame total: 29 B.

Errors: if the slot is mid-insertion (mechanical bounce), the Pico waits ≤ 50 ms for stability, then either populates the response or raises ERR_INTERNAL_PICO with diag cart-bus-unstable.

⚠ Obsolete per ADR-0019. The Pico no longer reads cartridge ROM banks; the cartridge is a USB-MSC SD card mounted on the Pi, and reads happen via standard read() against the mounted filesystem. Type byte 0x11 is vacated and MUST NOT be used by conforming v0.2 firmware. Section retained for design history.

Payload (6 B):

OffsetWFieldSemantics
02bankLE uint16. 0–511. Bank 0 = 0x0000–0x3FFF; banks 1+ at 0x4000–0x7FFF via MBC5.
22offsetLE uint16. Byte offset within the bank. 0–16383.
42lengthLE uint16. Bytes to read. 1–16384. offset + length ≤ 16384.

Sum: 2+2+2 = 6 B. Frame total: 12 B.

Pico services the request as a stream of CART_READ_BANK_DATA frames sharing the originating seq, chunked at 256 B. Total chunks = ceil(length / 256); final chunk may be shorter.

Throughput: a full 16 KB bank read at 1 Mbps with 256-B chunks transmits 64 chunks (~270 B each) totalling ~17.3 KB on the wire and ~173 ms transit. This sets the practical ceiling for boot-time cartridge ingestion.

Errors: ERR_OUT_OF_RANGE (bank ≥ rom_banks, offset + length > 16384, length == 0); ERR_CART_NOT_PRESENT.

4.6. CART_READ_BANK_DATA (0x12) — Pico→Pi

Section titled “4.6. CART_READ_BANK_DATA (0x12) — Pico→Pi”

⚠ Obsolete per ADR-0019. Companion response frame to CART_READ_BANK (§4.5); both vacated. Type byte 0x12 MUST NOT be used by conforming v0.2 firmware. Section retained for design history.

Payload (8 + N B):

OffsetWFieldSemantics
02bankEchoes request.
22chunk_indexLE uint16. Zero-based.
42total_chunksLE uint16. Constant across all chunks of one request.
62data_lenLE uint16. Valid bytes in data. 1–256. Only the final chunk may carry data_len < 256.
8NdataN = data_len. Raw cartridge bytes.

Sum: 2+2+2+2+N = 8 + N B. With N = 256, payload = 264 B; frame total = 270 B.

The Pi reassembles by writing data at chunk_index × 256 within its receive buffer, validating that total_chunks is consistent and that all chunks 0..total_chunks-1 arrive within the timeout window (§5.1). On timeout the Pi may reissue the original CART_READ_BANK with a fresh seq — the Pico is stateless across requests.

Pico-side back-pressure: if the TX FIFO is consistently full, the Pico inserts a 1 ms idle gap between chunks. The Pi userspace daemon drains UART at >100 KB/s sustained, so back-pressure rarely fires.

Errors: none on the data frames themselves. A mid-stream cart-bus error becomes a final ERROR(seq=originating) frame with ERR_INTERNAL_PICO (diag cart-bus-error-mid-read); the Pi treats this as a request abort.

4.7. CART_READ_SRAM (0x13) — bidirectional

Section titled “4.7. CART_READ_SRAM (0x13) — bidirectional”

⚠ Obsolete per ADR-0019. Per-cartridge save state lives as a file on the cartridge’s SD filesystem (/save/<cart_id>.sav) per ADR-0019; reads happen on the Pi via standard read(), not over UART. Type byte 0x13 MUST NOT be used by conforming v0.2 firmware. Section retained for design history.

Request payload (5 B):

OffsetWFieldSemantics
01bankSRAM bank. 0–15 (max 16 banks of 8 KB = 128 KB per ADR-0013).
12offsetLE uint16. 0–8191.
32lengthLE uint16. 1–8192. offset + length ≤ 8192.

Sum: 1+2+2 = 5 B. Frame total: 11 B.

Response shape depends on length:

  • length ≤ 1013: single-frame CART_READ_SRAM response with the layout below.
  • length > 1013: chunked response via CART_READ_SRAM_DATA (§4.18) frames, 256 B per chunk, all sharing the originating seq.

Single-frame response payload (5 + N B), used when length ≤ 1013:

OffsetWFieldSemantics
01bankEchoes request.
12offsetEchoes request.
32data_lenLE uint16. = request length on success.
5NdataSRAM bytes.

Sum: 1+2+2+N = 5 + N B. With max N = 1013, frame total = 1024 (= MAX_FRAME_LEN).

Errors: ERR_OUT_OF_RANGE (bank ≥ sram_banks, offset + length > 8192, length outside 1..8192); ERR_CART_NOT_PRESENT.

4.8. CART_WRITE_SRAM (0x14) — bidirectional

Section titled “4.8. CART_WRITE_SRAM (0x14) — bidirectional”

⚠ Obsolete per ADR-0019. Per-cartridge save writes are filesystem write() calls on the Pi against the mounted SD; not a Pico responsibility. Type byte 0x14 MUST NOT be used by conforming v0.2 firmware. Section retained for design history.

Request payload (5 + N B):

OffsetWFieldSemantics
01bank0–15.
12offsetLE uint16. 0–8191.
32data_lenLE uint16. 1–1013.
5NdataBytes to write.

Sum: 1+2+2+N = 5 + N B. Max frame total: 1024.

Response payload (6 B):

OffsetWFieldSemantics
01bankEchoes request.
12offsetEchoes request.
32bytes_writtenLE uint16. = request data_len on success; partial count on partial-write failure.
51status0x00 = OK; 0x01 = write-protected; 0x02 = range error (caught at validation; bytes_written = 0).

Sum: 1+2+2+1 = 6 B. Frame total: 12 B.

The ack carries write-protection information via status = 0x01; the Pico does NOT additionally emit a stand-alone ERROR frame (avoids double-reporting). ERR_OUT_OF_RANGE and ERR_CART_NOT_PRESENT apply at request validation. If the cart is removed mid-write, ack carries bytes_written = (count before removal) and status = 0x02.

4.9. PSG_REG_WRITE (0x20) — Pi→Pico, fire-and-forget

Section titled “4.9. PSG_REG_WRITE (0x20) — Pi→Pico, fire-and-forget”

Payload (2 B):

OffsetWFieldSemantics
01regYM2149 register index. 0–13 (14 registers per CLAUDE.md spec). 14–15 reserved → ERR_OUT_OF_RANGE.
11valueNew register value. The Pico’s PSG emulator applies datasheet bit-masks for unused bits.

Sum: 1+1 = 2 B. Frame total: 8 B.

The most-frequent frame on the link in normal operation. Pico writes immediately to emulator state; takes effect on the next 22.7 µs sample period. See §7 for end-to-end audio latency.

Errors: ERR_OUT_OF_RANGE → asynchronous ERROR(seq=0) with offending register in diag.

4.10. PSG_RESET (0x21) — Pi→Pico, fire-and-forget

Section titled “4.10. PSG_RESET (0x21) — Pi→Pico, fire-and-forget”

Empty payload. Frame total: 6 B. Pico zeros all 14 PSG registers and silences I2S. Used at boot, on cartridge unload, and as defensive cleanup in the Pi’s audio panic path.

4.11. OLED_SET_ROW (0x30) — Pi→Pico, fire-and-forget

Section titled “4.11. OLED_SET_ROW (0x30) — Pi→Pico, fire-and-forget”

Payload (3 + N B):

OffsetWFieldSemantics
01rowLogical row 1, 2, 3, or 4 per ADR-0015 §2 layout. 0 and 5+ → ERR_OUT_OF_RANGE.
11col_startStarting column. 0–31 (CIPHER-LINE is 32 logical columns wide).
21text_lenGlyphs that follow. 0–32. col_start + text_len ≤ 32.
3NtextN = text_len. Press Start 2P glyphs at SSD1322 native 8×8 (per ADR-0015 §2). 0x20–0x7E printable; 0x00–0x1F box-drawing per the KN-86 Code Page (CLAUDE.md Font row); 0x7F+ reserved.

Sum: 1+1+1+N = 3 + N B. Max frame total (N=32): 41 B.

Partial-row write — cells outside [col_start, col_start + text_len) are not modified. To clear the rest, send OLED_FILL or OLED_SET_ROW with spaces. The Pico maintains the SSD1322 framebuffer and pushes only dirty rows to the panel (per ADR-0015 Known Unknown #5).

4.12. OLED_SCROLL_ROW (0x31) — Pi→Pico, fire-and-forget

Section titled “4.12. OLED_SCROLL_ROW (0x31) — Pi→Pico, fire-and-forget”

Payload (3 B):

OffsetWFieldSemantics
01rowLogical row 1–4.
11direction0x00 = scroll left (rightmost cells fill with space); 0x01 = scroll right (leftmost cells fill with space).
21cells1–32. cells = 32 clears the row.

Sum: 1+1+1 = 3 B. Frame total: 9 B.

Used for the CIPHER scrollback animation: Row 2 → Row 3 between utterances (combined with a follow-up OLED_SET_ROW on Row 2 for the new fragment).

4.13. OLED_FILL (0x32) — Pi→Pico, fire-and-forget

Section titled “4.13. OLED_FILL (0x32) — Pi→Pico, fire-and-forget”

Payload (2 B):

OffsetWFieldSemantics
01rowLogical row 1–4.
11glyphGlyph byte to fill all 32 cells. Typically 0x20 (space) or a block character.

Sum: 1+1 = 2 B. Frame total: 8 B.

4.14. OLED_CLEAR (0x33) — Pi→Pico, fire-and-forget

Section titled “4.14. OLED_CLEAR (0x33) — Pi→Pico, fire-and-forget”

Payload (1 B):

OffsetWFieldSemantics
01rowLogical row 1–4 to clear, OR 0xFF to clear all four rows.

Sum: 1 B. Frame total: 7 B. Equivalent to OLED_FILL with glyph = 0x20 for the specified row(s); shorthand for the common case.

4.15. EVENT (0xE0) — Pico→Pi, unsolicited (seq = 0)

Section titled “4.15. EVENT (0xE0) — Pico→Pi, unsolicited (seq = 0)”

Payload (2 + N B):

OffsetWFieldSemantics
01event_codeEvent class (table below).
11payload_lenBytes that follow. Per-class.
2Nevent_payloadN = payload_len.

Sum: 1+1+N = 2 + N B.

event_codeNamepayload_lenevent_payload layout
0x01CART_INSERTEDObsolete per ADR-0019. Cart insertion is a udev mass-storage device-arrival event on the Pi side; the Pico does not see the cart. Event code vacated.
0x02CART_REMOVEDObsolete per ADR-0019. Cart removal is a udev device-disappear event on the Pi side. Event code vacated.
0x03BUFFER_OVERFLOW3[subsystem:1][dropped_count:2]. subsystem: 0x01 = PSG queue, 0x02 = OLED queue. dropped_count: LE uint16 since previous overflow event for the same subsystem.
0x04INTERNAL_ERROR2 + M[error_class:1][diag_len:1][diag:M]. error_class = §6 error code; diag ≤ 32 ASCII chars.

Frame totals (canonical events only post-ADR-0019): BUFFER_OVERFLOW = 11 B; INTERNAL_ERROR (max diag) = 42 B. Cart-related event payloads above are obsolete per ADR-0019; their frame totals are retained struck-through for design history only.

CART_INSERTED / CART_REMOVED: (Obsolete per ADR-0019.) The Pico does not observe cart insertion or removal post-ADR-0019. Cart presence is signalled to the nOSh runtime via udev mass-storage device-arrival / disappear events on the Pi USB stack; ADR-0019 §“Hot-swap semantics” defines the contract. The 50 ms cart-detect-line debounce described here applied to the ADR-0013 / ADR-0017 cart-bus path and is retained as design history only.

BUFFER_OVERFLOW recovery flow: the Pico maintains a 32-frame command queue per fire-and-forget subsystem (PSG, OLED). When the queue is full and another command arrives, the Pico drops the oldest queued command and increments its overflow counter. After a 100 ms quiescent window with no further drops, the Pico emits EVENT(BUFFER_OVERFLOW) with the cumulative drop count and resets the counter. The Pi MUST log the overflow, raise an internal degraded-state flag, and rate-limit its outgoing fire-and-forget traffic to 80% of nominal until the link recovers (no further overflow events for 5 seconds).

INTERNAL_ERROR: any unrecoverable Pico-side condition with no more specific code (SSD1322 SPI timeout, I2S underrun, PIO state-machine fault). Post-ADR-0019 the cart-bus glitch class no longer applies — the Pico does not touch the cart bus. The Pi logs and, if the failure indicates a hard subsystem outage, falls back to a degraded state.

4.16. ERROR (0xF0) — Pico→Pi, response

Section titled “4.16. ERROR (0xF0) — Pico→Pi, response”

Payload (3 + N B):

OffsetWFieldSemantics
01error_code§6 taxonomy.
11offending_typeType byte of the offending request, or 0x00 if no specific request can be correlated.
21diag_len0–64.
3NdiagN = diag_len. ASCII diagnostic, e.g., psg-reg-out-of-range:14. Not NUL-terminated.

Sum: 1+1+1+N = 3 + N B. Max frame (N=64): 73 B.

seq echoes the offending request when correlation is possible; otherwise seq = 0. The Pi MUST NOT emit ERROR frames over the wire — Pi-side errors are local-only diagnostics. (The Pico is the I/O peripheral; the Pi has no useful complaint to relay to it.)

⚠ Obsolete per ADR-0019. No cart bus on the Pico means no cart-bus reset path on the Pico. Type byte 0x15 MUST NOT be used by conforming v0.2 firmware. Section retained for design history.

Request: empty payload. Frame total: 6 B.

Response payload (1 B):

OffsetWFieldSemantics
01status0x00 = OK, cartridge bus healthy after reset; 0x01 = cart still unresponsive (reset issued but post-reset header read failed).

Sum: 1 B. Frame total: 7 B.

The Pico drives the cartridge slot’s /RESET line low for 100 ms, releases, waits 50 ms for power-on stabilisation, then reads the cart header to confirm the bus has recovered. On success the response carries status = 0x00; on persistent failure, status = 0x01 and the Pi treats the slot as degraded (subsequent CART_* requests will return ERR_CART_BUS_ERROR until the cart is physically removed and reinserted).

Used by the Pi to recover from a hung MBC5 mapper without rebooting the Pico. The full-Pico-reboot fallback (§5.2 watchdog, ~30 s) remains available; CART_RESET is the fast path for transient bus glitches and saves the audio + OLED state on the Pico.

Errors: ERR_CART_NOT_PRESENT if the slot is empty; ERR_INTERNAL_PICO if the Pico’s reset GPIO is not configured.

4.18. CART_READ_SRAM_DATA (0x16) — Pico→Pi

Section titled “4.18. CART_READ_SRAM_DATA (0x16) — Pico→Pi”

⚠ Obsolete per ADR-0019. Companion to CART_READ_SRAM (§4.7); both vacated. Type byte 0x16 MUST NOT be used by conforming v0.2 firmware. Section retained for design history.

Payload (7 + N B):

OffsetWFieldSemantics
01bankEchoes the originating CART_READ_SRAM request.
12chunk_indexLE uint16. Zero-based.
32total_chunksLE uint16. Constant across all chunks of one request.
52data_lenLE uint16. Valid bytes in data. 1–256. Only the final chunk may carry data_len < 256.
7NdataN = data_len. Raw SRAM bytes.

Sum: 1+2+2+2+N = 7 + N B. With N = 256, payload = 263 B; frame total = 269 B.

Used as the chunked response form of CART_READ_SRAM (§4.7) when the requested length > 1013. Pico chunks at 256 B for symmetry with CART_READ_BANK_DATA (§4.6); total_chunks = ceil(length / 256). The Pi reassembles by writing data at chunk_index × 256 within its receive buffer, validating that all chunks 0..total_chunks-1 arrive within the §5.1 timeout window.

Throughput: a full 8 KB SRAM bank read at 1 Mbps with 256-B chunks transmits 32 chunks (~270 B each) totalling ~8.6 KB on the wire and ~86 ms transit. Acceptable for cart-load (one-shot at insertion).

Errors: none on the data frames themselves; mid-stream cart-bus errors surface as a final ERROR(seq=originating) with ERR_CART_BUS_ERROR, mirroring CART_READ_BANK_DATA behaviour.

4.19. PSG_BULK_WRITE (0x22) — Pi→Pico, fire-and-forget

Section titled “4.19. PSG_BULK_WRITE (0x22) — Pi→Pico, fire-and-forget”

Payload (14 B):

OffsetWFieldSemantics
014registers14 bytes, one per YM2149 register (registers[0] = reg 0, …, registers[13] = reg 13). The Pico applies all 14 atomically before generating the next sample.

Sum: 14 B. Frame total: 20 B.

Compresses 14 sequential PSG_REG_WRITE frames (~110 ms wire time + 14× dispatch overhead) into a single frame (~200 µs wire + one dispatch). Used at cartridge boot (initial PSG state from cart vocabulary), audio-state restore after PSG_RESET, and recovery flows after Pico crash + watchdog re-handshake (§5.2). Fires the same per-register bit-mask validation as PSG_REG_WRITE; an out-of-range value in any register byte raises ERR_OUT_OF_RANGE (asynchronous ERROR(seq=0) with offending register index in diag) but does NOT abort the bulk write — every byte is applied.


Frame typeTimeoutRetry behaviour
HELLO (handshake)200 msPi retries up to 3 times with fresh nonces; on 3rd timeout, hard fail per §5.3.
HELLO (heartbeat)200 msSee §5.2.
VERSION_QUERY200 msPi retries up to 2 times; on failure, hard fail.
CART_DETECT100 msPi retries once; on second failure, treats slot as empty and proceeds.
CART_READ_BANK500 ms whole-stream; abort if any chunk gap > 50 ms.Pi reissues with a fresh seq.
CART_READ_SRAM100 msPi retries once.
CART_WRITE_SRAM200 msPi retries once; on second failure raises a save-failure flag to nOSh; cartridge save is degraded.
Fire-and-forget (PSG_*, OLED_*)n/an/a (no response expected)

The Pi’s outstanding-request table (§2.2) ages out entries at the per-type timeout; aged entries are reaped and logged.

Per ADR-0017 §Implementation Notes: 5-second heartbeat ping with three-missed-pings = degraded.

Mechanism: every 5 s, the Pi sends HELLO(flags=0x00, fresh nonce) with an incremented seq. The Pico echoes (same seq, same nonce, flags=0x00). Timeout 200 ms.

Pi side:

  • On a missed heartbeat, increment missed_heartbeats. The next scheduled heartbeat fires on schedule.
  • After 3 consecutive misses (~15 s of unresponsive Pico), the Pi enters degraded state: raise KN86_LINK_DEGRADED to nOSh; display COPROCESSOR LINK DEGRADED on Row 24; silence audio (PSG state unreachable); suspend CIPHER-LINE writes. (Pre-ADR-0019 also disabled cartridge-bus operations on the Pico; obsolete — cart access is on the Pi USB-MSC path now and is unaffected by the Pi↔Pico link health.) Continue heartbeats every 5 s.
  • On a successful response while degraded, reset missed_heartbeats = 0, clear the flag, re-issue VERSION_QUERY (verify the Pico did not reboot into a different firmware), and replay deck-state-critical commands (e.g., re-init OLED with current frame contents).

Pico side: does not initiate heartbeats. Maintains its own watchdog: if it receives no frame at all for 30 s, it resets its UART subsystem and emits an unsolicited HELLO(role=Pico, flags=HANDSHAKE) to nudge the Pi into re-handshake.

Crash semantics: if the Pico crashes and reboots, its first frame after boot is an unsolicited HELLO(role=Pico, flags=HANDSHAKE, fresh nonce). The Pi treats this as “the Pico just rebooted” and re-runs §5.3.

Canonical link-bring-up flow at Pi userspace daemon start (or after detected Pico reboot):

  1. Pi → HELLO(role=Pi, flags=HANDSHAKE, nonce=N1), seq=1. Wait ≤ 200 ms.
  2. Pico → HELLO(role=Pico, flags=HANDSHAKE, nonce=N1), seq=1. Pi validates the echoed nonce.
  3. Pi → VERSION_QUERY, seq=2. Wait ≤ 200 ms.
  4. Pico → VERSION_RESPONSE, seq=2. Pi validates proto_major (hard fail otherwise per §4.3).
  5. Pi → PSG_RESET, seq=0. Defensive cleanup.
  6. Pi → OLED_CLEAR(row=0xFF), seq=0. Defensive cleanup.
  7. Pi → CART_DETECT, seq=3. Pi ingests slot state. (Step 7 obsolete per ADR-0019.) Cart slot state is now sourced from udev mass-storage device events on the Pi side, not from the Pico. The Pi reads the SD-mounted cartridge filesystem directly when a cart is present; no Pico round-trip required during bootstrap.
  8. Link is operational. Pi begins periodic heartbeats per §5.2.

If any step times out per §5.1, fall into hard-fail: log, display error on Row 24, refuse to start nOSh.


CodeNameClassDescription
0x10ERR_MALFORMED_FRAMEframinglen out of range, or reserved bit set in a flag byte.
0x11ERR_CRC_MISMATCHframingCRC mismatch.
0x12ERR_UNKNOWN_TYPEframingtype byte not in §3 (including reserved ranges).
0x13ERR_PAYLOAD_LENGTH_MISMATCHframinglen disagrees with per-type expected length (e.g., PSG_REG_WRITE with payload != 2 B).
0x14ERR_SEQUENCE_CONFLICTframingPi-local: response seq already reaped from outstanding-request table. Logged, not propagated.
0x20ERR_OUT_OF_RANGEparameterPer-command parameter outside its declared range. Diag carries the offending field name and value.
0x21ERR_VERSION_MISMATCHparameterproto_major mismatch. Diag: proto-mismatch:pi=<v>:pico=<v>.
0x40ERR_CART_NOT_PRESENTObsolete per ADR-0019. Cart presence is a udev event on the Pi; not surfaced over UART. Code byte vacated.
0x41ERR_SRAM_WRITE_PROTECTEDObsolete per ADR-0019. Per-cartridge save is a Pi-side filesystem write; SRAM write-protect is no longer a category. Code byte vacated.
0x42ERR_CART_BUS_ERRORObsolete per ADR-0019. No cart bus on the Pico; no MBC5 timing concern. Code byte vacated.
0x50ERR_OLED_BUFFER_OVERFLOWOLEDCIPHER-LINE command queue overflowed. Reported via EVENT(BUFFER_OVERFLOW) preferentially; stand-alone ERROR only on a session-level OLED failure (e.g., SPI hang).
0x60ERR_PSG_QUEUE_OVERFLOWPSGPSG register-write queue overflowed. Same reporting discipline as 0x50.
0x70ERR_INTERNAL_PICOinternalA Pico-side condition with no more specific code. Diag carries the failure mode (e.g., i2s-underrun, spi-timeout-ssd1322).
0x71ERR_PICO_REBOOTINGinternalPico is mid-reboot; sent only via the unsolicited HELLO(handshake) mechanism in §5.2. Listed for taxonomy completeness; not a direct ERROR frame.
0x80ERR_LINK_DEGRADEDsessionPi-local: a userspace caller attempted a request while the Pi is in degraded state. Not sent over the wire.

The Pico MUST emit an ERROR frame for every wire-level error condition. Post-ADR-0019 the canonical surface is 0x100x21, 0x50, 0x60, 0x70 (cart-related codes 0x40/0x41/0x42 are vacated; conforming v0.2 firmware MUST NOT emit them). For fire-and-forget origins, seq = 0.


ADR-0017 §Known Unknowns #5 sets a <30 ms target for PSG-write-to-audible-tone. Walking the budget:

StageTimeNotes
Pi userspace serialises PSG_REG_WRITE≤ 50 µs8-byte memcpy + CRC compute on a 1 GHz Cortex-A53. Negligible.
Pi UART TX queue → wire≤ 80 µs8 bytes × 10 µs/B at 1 Mbps 8N1.
Pico UART RX → frame parse → CRC check≤ 200 µsSingle-frame buffer, table-driven CRC, dispatch is a switch on type. RP2040 at 125 MHz has ample headroom.
Pico writes value to PSG register state≤ 1 µsImmediate store; emulator reads on next sample.
Audio sample period (PSG → I2S sample)22.7 µs1 / 44100.
I2S output buffer drain5–12 msAt 256-sample double-buffer: half-buffer ≈ 5.8 ms; full ≈ 11.6 ms.
MAX98357A → speaker propagation< 1 msClass-D transient response; effectively instantaneous.

Sum (typical): ~9 ms. Sum (worst-case I2S buffering): ~12.5 ms. Headroom against 30 ms target: ~17 ms.

No mitigations required at the protocol layer. The Pico firmware (F2) MUST size its I2S buffer at 256 samples × 2-deep or smaller; deeper buffering pushes the budget without protocol changes. If bring-up measures actual latency above 20 ms, investigate the I2S buffer depth or the Pico’s UART RX ISR latency — the protocol itself does not need amendment to support that investigation.


The following type-byte ranges are reserved for forward compatibility and MUST be rejected with ERR_UNKNOWN_TYPE (0x12) by both endpoints:

RangePurpose
0x00, 0xFFReserved sentinels. MUST never appear as a type.
0x050x0FFuture session-control frames.
0x100x16Vacated by ADR-0019 — the entire cart-bus frame block (CART_DETECT, CART_READ_BANK, CART_READ_BANK_DATA, CART_READ_SRAM, CART_WRITE_SRAM, CART_RESET, CART_READ_SRAM_DATA). Reserved; MUST NOT be reused without a fresh ADR.
0x170x1FReserved (was: future cartridge frames; cart frames moved off the Pico per ADR-0019).
0x230x2FFuture PSG frames.
0x340x3FFuture OLED frames (e.g., OLED_GET_STATE — deferred to v0.2 per §10.2).
0x400xDFFuture subsystems not yet defined.
0xE10xEFAdditional event-shaped frames.
0xF10xFEAdditional control frames.

Forward-compatibility rule: unknown types MUST be rejected — never silently dropped. Silent drops mask version drift and produce intermittent bugs that are nightmarish to debug. New frame types land via this spec’s §3 allocation and are gated on a proto_minor bump and the §4.3 capability bits.


A conforming Pi userspace daemon and Pico firmware MUST:

  1. Implement the §2 envelope exactly, including LE byte order and CRC-16/CCITT-FALSE.
  2. Pass the §2.3 reference test vector ("123456789"0x29B1) at boot before raising the link to ready.
  3. Implement every frame type in §4 with the byte layouts as written.
  4. Implement the §6 error taxonomy with the Pico-side emission rules.
  5. Implement the §5.3 bootstrap and §5.2 heartbeat (5 s period, 200 ms timeout, 3-strike degraded transition).
  6. Reject unknown frame types with ERR_UNKNOWN_TYPE per §8.
  7. Cap MAX_FRAME_LEN at 1024 bytes and validate len on every frame.

Conforming implementations MAY use DMA on the Pico’s UART RX/TX, inline-cache the CRC table, and coalesce adjacent OLED_SET_ROW calls before pushing the framebuffer.

Conforming implementations MUST NOT emit frames with len < 6 or len > 1024, reuse a seq with an outstanding response, process a frame with a CRC mismatch, or define new frame types or error codes outside this spec without an ADR.


10. Gap Resolution Log (v0.1 amendments approved 2026-04-24)

Section titled “10. Gap Resolution Log (v0.1 amendments approved 2026-04-24)”

The original drafting surfaced ten items needing explicit resolution before F2 (Pico firmware) implementation begins. Josh’s decisions, taken 2026-04-24, are recorded here. ADDED items landed in the same PR as this spec; CONFIRMED items ratified the spec’s existing commitment; DEFERRED items are queued for a future protocol version.

10.1. CART_RESET frame — ADDED (Obsolete per ADR-0019.)

Section titled “10.1. CART_RESET frame — ADDED (Obsolete per ADR-0019.)”

Added 2026-04-24 as type 0x15 (§4.17), then vacated 2026-04-24 by ADR-0019 along with the entire cart-bus frame block. Section retained for design history.

10.2. OLED_GET_STATE for diagnostics — DEFERRED to v0.2

Section titled “10.2. OLED_GET_STATE for diagnostics — DEFERRED to v0.2”

Pi shadow buffer is the v0.1 source of truth. If a drift bug surfaces during bring-up, add the primitive then. Type-byte range 0x340x3F is reserved for it.

10.3. Bulk PSG register snapshot — ADDED

Section titled “10.3. Bulk PSG register snapshot — ADDED”

Added as PSG_BULK_WRITE type 0x22 (§4.19). 14-byte payload, one byte per YM2149 register, applied atomically before the next sample. Compresses cartridge boot / audio-state-restore from ~110 ms (14 sequential frames) to ~200 µs (single frame).

10.4. CART_READ_SRAM_DATA chunked-response variant — ADDED (Obsolete per ADR-0019.)

Section titled “10.4. CART_READ_SRAM_DATA chunked-response variant — ADDED (Obsolete per ADR-0019.)”

Added 2026-04-24 as type 0x16 (§4.18), then vacated 2026-04-24 by ADR-0019 along with CART_READ_SRAM (§4.7) and the entire cart-bus frame block. Section retained for design history.

10.5. Pi-side ERROR emission — CONFIRMED Pico-only

Section titled “10.5. Pi-side ERROR emission — CONFIRMED Pico-only”

ERROR frames are emitted by the Pico only. The Pi has no useful complaint to relay over the wire to its I/O peripheral; Pi-side errors stay local diagnostics. ADR-0017 §4 is amended in the same PR to make the direction explicit.

10.6. EVENT(CART_INSERTED) payload — CONFIRMED full-data (Obsolete per ADR-0019.)

Section titled “10.6. EVENT(CART_INSERTED) payload — CONFIRMED full-data (Obsolete per ADR-0019.)”

The full-data CART_INSERTED payload was an optimisation against the (now obsolete) Pico-driven cart-detect path. Post-ADR-0019 cart insertion is signalled to the nOSh runtime via udev mass-storage events on the Pi side; no UART event is emitted. Section retained for design history.

10.7. Heartbeat type — CONFIRMED reuse-HELLO-with-flag

Section titled “10.7. Heartbeat type — CONFIRMED reuse-HELLO-with-flag”

Heartbeat reuses HELLO (0x01) with flags.HANDSHAKE = 0 (§4.1). Avoids type-byte proliferation; log-readability cost is a 1-line annotation in the Pi daemon’s parser.

10.8. Frame size cap at 1024 bytes — CONFIRMED

Section titled “10.8. Frame size cap at 1024 bytes — CONFIRMED”

MAX_FRAME_LEN = 1024 (§2.1). Originally sized so the largest single-frame command (CART_READ_SRAM/CART_WRITE_SRAM with 1013-B payload) fit at exactly the cap; post-ADR-0019 those frames are obsolete, but the cap is retained as it leaves comfortable headroom for chunked variants in the still-canonical PSG/OLED/event surface. A future high-throughput frame type would land via a proto_minor bump and an amended MAX_FRAME_LEN.

10.9. UART BREAK / line-idle handling — ADDED (BREAK = session reset)

Section titled “10.9. UART BREAK / line-idle handling — ADDED (BREAK = session reset)”

§2.4 now specifies BREAK as an explicit session reset signal: both endpoints flush parser state to IDLE, emit nothing in response to the BREAK itself, and the Pi triggers §5.3 bootstrap re-handshake. The Pico’s mirror is to wait 1 s for HELLO before emitting an unsolicited handshake. Used by firmware bring-up and emergency recovery.

10.10. build_id format — CONFIRMED lower 32 bits of git commit hash

Section titled “10.10. build_id format — CONFIRMED lower 32 bits of git commit hash”

§4.3’s build_id is the lower 32 bits of the Pico firmware’s git commit hash, LE uint32. Opaque to the Pi; used in bug reports and firmware-update telemetry.


Appendix A — Reference CRC-16/CCITT-FALSE Implementation (informative)

Section titled “Appendix A — Reference CRC-16/CCITT-FALSE Implementation (informative)”
/* CRC-16/CCITT-FALSE: poly=0x1021, init=0xFFFF, no reflection, xorout=0x0000.
* Reference test vector: crc16_ccitt_false("123456789", 9) == 0x29B1.
*/
uint16_t crc16_ccitt_false(const uint8_t *data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= ((uint16_t)data[i]) << 8;
for (int b = 0; b < 8; b++) {
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
}
}
return crc;
}

A table-driven version is the recommended production implementation; the bit-serial form above is given for unambiguous spec reference.