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 theCART_INSERTED/CART_REMOVEDpayloads of theEVENTframe (0xE0). The0x10–0x1Ftype range is vacated; types in this range MUST NOT be used by conforming Pico firmware (F2) or Pi userspace daemon (F3). Pico implementations MAY returnERR_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 othercartridge-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), theCART_INSERTED/CART_REMOVEDevent 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/adr/ADR-0017-realtime-io-coprocessor.md (Approved 2026-04-24; cart-bus role partially superseded by ADR-0019 the same day).
Related: docs/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/adr/ADR-0017-realtime-io-coprocessor.md (parent decision; partially superseded by ADR-0019), docs/adr/ADR-0015-cipher-line-auxiliary-display.md (CIPHER-LINE layout — drives the OLED frame types), docs/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/adr/ADR-0011-device-firmware-update-system.md (firmware update path), docs/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.
1. Link Parameters
Section titled “1. Link Parameters”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.
2. Frame Envelope
Section titled “2. Frame Envelope”Every frame on the wire follows the structure committed by ADR-0017 §4:
+--------+--------+------+-----+----------+----------+----------+| len_lo | len_hi | type | seq | payload | crc16_lo | crc16_hi |+--------+--------+------+-----+----------+----------+----------+| Field | Width | Semantics |
|---|---|---|
len | 2 B (LE) | Total frame length in bytes, envelope + payload. Min 6 (zero-byte payload), max 1024 (§2.1). |
type | 1 B | Frame type identifier (§3). |
seq | 1 B | Sequence number, 0–255 (§2.2). |
payload | 0–1018 B | Type-specific (§4). |
crc16 | 2 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.
2.1. Maximum frame size
Section titled “2.1. Maximum frame size”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.
2.2. Sequence number semantics
Section titled “2.2. Sequence number semantics”- Pi-originated requests carry
seq ∈ [1, 255], monotonically incremented modulo 256 (skipping 0). The Pi MUST NOT reuse aseqwhile its request is outstanding (§5.1 timeouts). - Pico responses echo the request’s
seqexactly. This is how the Pi correlatesCART_READ_BANK_DATAchunks to the originating request. - Pi-originated fire-and-forget (
PSG_*,OLED_*) carryseq = 0. No ack from the Pico. Errors surface asynchronously asERROR(seq=0)orEVENT(BUFFER_OVERFLOW). - Pico-originated events (
EVENT) carryseq = 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.
2.3. CRC-16/CCITT-FALSE
Section titled “2.3. CRC-16/CCITT-FALSE”| Parameter | Value |
|---|---|
| Polynomial | 0x1021 |
| Initial value | 0xFFFF |
| Input/output reflection | none (process MSB first) |
| Final XOR | 0x0000 |
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.
2.4. Frame parsing state machine
Section titled “2.4. Frame parsing state machine”- IDLE: read
len_lo, advance. - LEN_HI: read
len_hi, computelen. Iflen < 6orlen > 1024, raiseERR_MALFORMED_FRAME(Pico-side: emitERROR(seq=0); Pi-side: log and drop) and return to IDLE. - HEADER: read
typeandseq. - PAYLOAD: read
len - 6bytes. - CRC: read
crc16_lo,crc16_hi. Compute expected CRC; on mismatch raiseERR_CRC_MISMATCHand 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.
3. Frame Type Allocation
Section titled “3. Frame Type Allocation”| Type | Name | Direction | Sequence semantics |
|---|---|---|---|
0x01 | HELLO | bidirectional | request + response (echo seq); also reused for heartbeat (§5.2) |
0x03 | VERSION_QUERY | Pi→Pico | request (response echoes seq) |
0x04 | VERSION_RESPONSE | Pico→Pi | response only |
0x10 | CART_DETECT | — | Obsolete per ADR-0019. Type byte vacated. |
0x11 | CART_READ_BANK | — | Obsolete per ADR-0019. Type byte vacated. |
0x12 | CART_READ_BANK_DATA | — | Obsolete per ADR-0019. Type byte vacated. |
0x13 | CART_READ_SRAM | — | Obsolete per ADR-0019. Type byte vacated. |
0x14 | CART_WRITE_SRAM | — | Obsolete per ADR-0019. Type byte vacated. |
0x15 | CART_RESET | — | Obsolete per ADR-0019. Type byte vacated. |
0x16 | CART_READ_SRAM_DATA | — | Obsolete per ADR-0019. Type byte vacated. |
0x20 | PSG_REG_WRITE | Pi→Pico | fire-and-forget (seq=0) |
0x21 | PSG_RESET | Pi→Pico | fire-and-forget (seq=0) |
0x22 | PSG_BULK_WRITE | Pi→Pico | fire-and-forget (seq=0) |
0x30 | OLED_SET_ROW | Pi→Pico | fire-and-forget (seq=0) |
0x31 | OLED_SCROLL_ROW | Pi→Pico | fire-and-forget (seq=0) |
0x32 | OLED_FILL | Pi→Pico | fire-and-forget (seq=0) |
0x33 | OLED_CLEAR | Pi→Pico | fire-and-forget (seq=0) |
0xE0 | EVENT | Pico→Pi | unsolicited (seq=0); cart-related event payloads (CART_INSERTED, CART_REMOVED) obsolete per ADR-0019 — non-cart payloads remain canonical |
0xF0 | ERROR | Pico→Pi | response (seq echoes offending request, or 0 if no correlation) |
Reserved ranges (post-ADR-0019, including the vacated 0x10–0x16 cart-bus block): 0x00, 0x05–0x0F, 0x10–0x1F (the entire cart-bus type range, vacated by ADR-0019), 0x23–0x2F, 0x34–0x3F, 0x40–0xDF, 0xE1–0xEF, 0xF1–0xFF. 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 (0x20–0x22), OLED (0x30–0x33), session control (HELLO / VERSION_QUERY / VERSION_RESPONSE / EVENT non-cart payloads / ERROR).
4. Frame Payload Layouts
Section titled “4. Frame Payload Layouts”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.
4.1. HELLO (0x01) — bidirectional
Section titled “4.1. HELLO (0x01) — bidirectional”Payload (6 B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | role | 0x01 = Pi, 0x02 = Pico. |
| 1 | 1 | flags | bit 0 = HANDSHAKE (1 = initial wake, 0 = heartbeat); bits 1–7 reserved (must be 0). |
| 2 | 4 | nonce | LE 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 withflags = 0x01,role = 0x02, nonce echoed, sameseq. - Heartbeat: Pi sends
flags = 0x00every 5 s. Pico echoes (§5.2).
Errors: ERR_MALFORMED_FRAME (reserved flag bits set); ERR_OUT_OF_RANGE (role not in {0x01, 0x02}).
4.2. VERSION_QUERY (0x03) — Pi→Pico
Section titled “4.2. VERSION_QUERY (0x03) — Pi→Pico”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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | proto_major | Coprocessor protocol major. v0.1 ships 0. |
| 1 | 1 | proto_minor | Protocol minor. v0.1 ships 1. |
| 2 | 1 | fw_major | Pico firmware semver major. |
| 3 | 1 | fw_minor | Pico firmware semver minor. |
| 4 | 1 | fw_patch | Pico firmware semver patch. |
| 5 | 4 | build_id | LE uint32. Lower 32 bits of the Pico firmware’s git commit hash. Opaque to the Pi; used in bug reports. |
| 9 | 2 | caps | LE 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.
4.4. CART_DETECT (0x10) — bidirectional
Section titled “4.4. CART_DETECT (0x10) — bidirectional”⚠ Obsolete per ADR-0019. The Pico no longer drives the cartridge bus; cartridge presence is now a
udevmass-storage device-arrival event on the Pi side. Type byte0x10is 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | present | 0x00 = empty slot; 0x01 = cartridge detected. If 0x00, all subsequent fields are zero-filled. |
| 1 | 1 | mapper | 0x00 = ROM-only; 0x01 = MBC1; 0x03 = MBC3; 0x05 = MBC5; 0xFF = unknown. Per ADR-0013 first-party carts are MBC5. |
| 2 | 2 | rom_banks | LE uint16. 16 KB ROM banks. Range 2 (32 KB) to 512 (8 MB). |
| 4 | 1 | sram_banks | 8 KB SRAM banks. Range 0 to 16 (128 KB). |
| 5 | 2 | header_checksum | LE uint16. The cart’s own header checksum at DMG offset 0x14E. Used by the Pi to disambiguate carts with identical titles. |
| 7 | 16 | title | DMG 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.
4.5. CART_READ_BANK (0x11) — Pi→Pico
Section titled “4.5. CART_READ_BANK (0x11) — Pi→Pico”⚠ 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 byte0x11is vacated and MUST NOT be used by conforming v0.2 firmware. Section retained for design history.
Payload (6 B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 2 | bank | LE uint16. 0–511. Bank 0 = 0x0000–0x3FFF; banks 1+ at 0x4000–0x7FFF via MBC5. |
| 2 | 2 | offset | LE uint16. Byte offset within the bank. 0–16383. |
| 4 | 2 | length | LE 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 byte0x12MUST NOT be used by conforming v0.2 firmware. Section retained for design history.
Payload (8 + N B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 2 | bank | Echoes request. |
| 2 | 2 | chunk_index | LE uint16. Zero-based. |
| 4 | 2 | total_chunks | LE uint16. Constant across all chunks of one request. |
| 6 | 2 | data_len | LE uint16. Valid bytes in data. 1–256. Only the final chunk may carry data_len < 256. |
| 8 | N | data | N = 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 standardread(), not over UART. Type byte0x13MUST NOT be used by conforming v0.2 firmware. Section retained for design history.
Request payload (5 B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | bank | SRAM bank. 0–15 (max 16 banks of 8 KB = 128 KB per ADR-0013). |
| 1 | 2 | offset | LE uint16. 0–8191. |
| 3 | 2 | length | LE uint16. 1–8192. offset + length ≤ 8192. |
Sum: 1+2+2 = 5 B. Frame total: 11 B.
Response shape depends on length:
length ≤ 1013: single-frameCART_READ_SRAMresponse with the layout below.length > 1013: chunked response viaCART_READ_SRAM_DATA(§4.18) frames, 256 B per chunk, all sharing the originatingseq.
Single-frame response payload (5 + N B), used when length ≤ 1013:
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | bank | Echoes request. |
| 1 | 2 | offset | Echoes request. |
| 3 | 2 | data_len | LE uint16. = request length on success. |
| 5 | N | data | SRAM 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 byte0x14MUST NOT be used by conforming v0.2 firmware. Section retained for design history.
Request payload (5 + N B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | bank | 0–15. |
| 1 | 2 | offset | LE uint16. 0–8191. |
| 3 | 2 | data_len | LE uint16. 1–1013. |
| 5 | N | data | Bytes to write. |
Sum: 1+2+2+N = 5 + N B. Max frame total: 1024.
Response payload (6 B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | bank | Echoes request. |
| 1 | 2 | offset | Echoes request. |
| 3 | 2 | bytes_written | LE uint16. = request data_len on success; partial count on partial-write failure. |
| 5 | 1 | status | 0x00 = 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | reg | YM2149 register index. 0–13 (14 registers per CLAUDE.md spec). 14–15 reserved → ERR_OUT_OF_RANGE. |
| 1 | 1 | value | New 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | row | Logical row 1, 2, 3, or 4 per ADR-0015 §2 layout. 0 and 5+ → ERR_OUT_OF_RANGE. |
| 1 | 1 | col_start | Starting column. 0–31 (CIPHER-LINE is 32 logical columns wide). |
| 2 | 1 | text_len | Glyphs that follow. 0–32. col_start + text_len ≤ 32. |
| 3 | N | text | N = 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | row | Logical row 1–4. |
| 1 | 1 | direction | 0x00 = scroll left (rightmost cells fill with space); 0x01 = scroll right (leftmost cells fill with space). |
| 2 | 1 | cells | 1–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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | row | Logical row 1–4. |
| 1 | 1 | glyph | Glyph 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | row | Logical 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | event_code | Event class (table below). |
| 1 | 1 | payload_len | Bytes that follow. Per-class. |
| 2 | N | event_payload | N = payload_len. |
Sum: 1+1+N = 2 + N B.
event_code | Name | payload_len | event_payload layout |
|---|---|---|---|
0x01 | CART_INSERTED | — | Obsolete 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. |
0x02 | CART_REMOVED | — | Obsolete per ADR-0019. Cart removal is a udev device-disappear event on the Pi side. Event code vacated. |
0x03 | BUFFER_OVERFLOW | 3 | [subsystem:1][dropped_count:2]. subsystem: 0x01 = PSG queue, 0x02 = OLED queue. dropped_count: LE uint16 since previous overflow event for the same subsystem. |
0x04 | INTERNAL_ERROR | 2 + 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.
: (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 CART_INSERTED / CART_REMOVEDudev 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | error_code | §6 taxonomy. |
| 1 | 1 | offending_type | Type byte of the offending request, or 0x00 if no specific request can be correlated. |
| 2 | 1 | diag_len | 0–64. |
| 3 | N | diag | N = 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.)
4.17. CART_RESET (0x15) — bidirectional
Section titled “4.17. CART_RESET (0x15) — bidirectional”⚠ Obsolete per ADR-0019. No cart bus on the Pico means no cart-bus reset path on the Pico. Type byte
0x15MUST NOT be used by conforming v0.2 firmware. Section retained for design history.
Request: empty payload. Frame total: 6 B.
Response payload (1 B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | status | 0x00 = 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 byte0x16MUST NOT be used by conforming v0.2 firmware. Section retained for design history.
Payload (7 + N B):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 1 | bank | Echoes the originating CART_READ_SRAM request. |
| 1 | 2 | chunk_index | LE uint16. Zero-based. |
| 3 | 2 | total_chunks | LE uint16. Constant across all chunks of one request. |
| 5 | 2 | data_len | LE uint16. Valid bytes in data. 1–256. Only the final chunk may carry data_len < 256. |
| 7 | N | data | N = 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):
| Offset | W | Field | Semantics |
|---|---|---|---|
| 0 | 14 | registers | 14 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.
5. Session Lifecycle
Section titled “5. Session Lifecycle”5.1. Request/response timeouts
Section titled “5.1. Request/response timeouts”| Frame type | Timeout | Retry behaviour |
|---|---|---|
HELLO (handshake) | 200 ms | Pi retries up to 3 times with fresh nonces; on 3rd timeout, hard fail per §5.3. |
HELLO (heartbeat) | 200 ms | See §5.2. |
VERSION_QUERY | 200 ms | Pi retries up to 2 times; on failure, hard fail. |
CART_DETECT | 100 ms | Pi retries once; on second failure, treats slot as empty and proceeds. |
CART_READ_BANK | 500 ms whole-stream; abort if any chunk gap > 50 ms. | Pi reissues with a fresh seq. |
CART_READ_SRAM | 100 ms | Pi retries once. |
CART_WRITE_SRAM | 200 ms | Pi retries once; on second failure raises a save-failure flag to nOSh; cartridge save is degraded. |
Fire-and-forget (PSG_*, OLED_*) | n/a | n/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.
5.2. Heartbeat and watchdog
Section titled “5.2. Heartbeat and watchdog”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_DEGRADEDto nOSh; displayCOPROCESSOR LINK DEGRADEDon 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-issueVERSION_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.
5.3. Bootstrap sequence
Section titled “5.3. Bootstrap sequence”Canonical link-bring-up flow at Pi userspace daemon start (or after detected Pico reboot):
- Pi →
HELLO(role=Pi, flags=HANDSHAKE, nonce=N1), seq=1. Wait ≤ 200 ms. - Pico →
HELLO(role=Pico, flags=HANDSHAKE, nonce=N1), seq=1. Pi validates the echoed nonce. - Pi →
VERSION_QUERY, seq=2. Wait ≤ 200 ms. - Pico →
VERSION_RESPONSE, seq=2. Pi validatesproto_major(hard fail otherwise per §4.3). - Pi →
PSG_RESET, seq=0. Defensive cleanup. - Pi →
OLED_CLEAR(row=0xFF), seq=0. Defensive cleanup. Pi →(Step 7 obsolete per ADR-0019.) Cart slot state is now sourced fromCART_DETECT, seq=3. Pi ingests slot state.udevmass-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.- 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.
6. Error Code Taxonomy
Section titled “6. Error Code Taxonomy”| Code | Name | Class | Description |
|---|---|---|---|
0x10 | ERR_MALFORMED_FRAME | framing | len out of range, or reserved bit set in a flag byte. |
0x11 | ERR_CRC_MISMATCH | framing | CRC mismatch. |
0x12 | ERR_UNKNOWN_TYPE | framing | type byte not in §3 (including reserved ranges). |
0x13 | ERR_PAYLOAD_LENGTH_MISMATCH | framing | len disagrees with per-type expected length (e.g., PSG_REG_WRITE with payload != 2 B). |
0x14 | ERR_SEQUENCE_CONFLICT | framing | Pi-local: response seq already reaped from outstanding-request table. Logged, not propagated. |
0x20 | ERR_OUT_OF_RANGE | parameter | Per-command parameter outside its declared range. Diag carries the offending field name and value. |
0x21 | ERR_VERSION_MISMATCH | parameter | proto_major mismatch. Diag: proto-mismatch:pi=<v>:pico=<v>. |
0x40 | ERR_CART_NOT_PRESENT | — | Obsolete per ADR-0019. Cart presence is a udev event on the Pi; not surfaced over UART. Code byte vacated. |
0x41 | ERR_SRAM_WRITE_PROTECTED | — | Obsolete per ADR-0019. Per-cartridge save is a Pi-side filesystem write; SRAM write-protect is no longer a category. Code byte vacated. |
0x42 | ERR_CART_BUS_ERROR | — | Obsolete per ADR-0019. No cart bus on the Pico; no MBC5 timing concern. Code byte vacated. |
0x50 | ERR_OLED_BUFFER_OVERFLOW | OLED | CIPHER-LINE command queue overflowed. Reported via EVENT(BUFFER_OVERFLOW) preferentially; stand-alone ERROR only on a session-level OLED failure (e.g., SPI hang). |
0x60 | ERR_PSG_QUEUE_OVERFLOW | PSG | PSG register-write queue overflowed. Same reporting discipline as 0x50. |
0x70 | ERR_INTERNAL_PICO | internal | A Pico-side condition with no more specific code. Diag carries the failure mode (e.g., i2s-underrun, spi-timeout-ssd1322). |
0x71 | ERR_PICO_REBOOTING | internal | Pico is mid-reboot; sent only via the unsolicited HELLO(handshake) mechanism in §5.2. Listed for taxonomy completeness; not a direct ERROR frame. |
0x80 | ERR_LINK_DEGRADED | session | Pi-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 0x10–0x21, 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.
7. Audio Latency Budget Validation
Section titled “7. Audio Latency Budget Validation”ADR-0017 §Known Unknowns #5 sets a <30 ms target for PSG-write-to-audible-tone. Walking the budget:
| Stage | Time | Notes |
|---|---|---|
Pi userspace serialises PSG_REG_WRITE | ≤ 50 µs | 8-byte memcpy + CRC compute on a 1 GHz Cortex-A53. Negligible. |
| Pi UART TX queue → wire | ≤ 80 µs | 8 bytes × 10 µs/B at 1 Mbps 8N1. |
| Pico UART RX → frame parse → CRC check | ≤ 200 µs | Single-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 µs | Immediate store; emulator reads on next sample. |
| Audio sample period (PSG → I2S sample) | 22.7 µs | 1 / 44100. |
| I2S output buffer drain | 5–12 ms | At 256-sample double-buffer: half-buffer ≈ 5.8 ms; full ≈ 11.6 ms. |
| MAX98357A → speaker propagation | < 1 ms | Class-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.
8. Reserved and Future Type Bytes
Section titled “8. Reserved and Future Type Bytes”The following type-byte ranges are reserved for forward compatibility and MUST be rejected with ERR_UNKNOWN_TYPE (0x12) by both endpoints:
| Range | Purpose |
|---|---|
0x00, 0xFF | Reserved sentinels. MUST never appear as a type. |
0x05–0x0F | Future session-control frames. |
0x10–0x16 | Vacated 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. |
0x17–0x1F | Reserved (was: future cartridge frames; cart frames moved off the Pico per ADR-0019). |
0x23–0x2F | Future PSG frames. |
0x34–0x3F | Future OLED frames (e.g., OLED_GET_STATE — deferred to v0.2 per §10.2). |
0x40–0xDF | Future subsystems not yet defined. |
0xE1–0xEF | Additional event-shaped frames. |
0xF1–0xFE | Additional 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.
9. Implementation Conformance
Section titled “9. Implementation Conformance”A conforming Pi userspace daemon and Pico firmware MUST:
- Implement the §2 envelope exactly, including LE byte order and CRC-16/CCITT-FALSE.
- Pass the §2.3 reference test vector (
"123456789"→0x29B1) at boot before raising the link to ready. - Implement every frame type in §4 with the byte layouts as written.
- Implement the §6 error taxonomy with the Pico-side emission rules.
- Implement the §5.3 bootstrap and §5.2 heartbeat (5 s period, 200 ms timeout, 3-strike degraded transition).
- Reject unknown frame types with
ERR_UNKNOWN_TYPEper §8. - Cap
MAX_FRAME_LENat 1024 bytes and validatelenon 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 0x34–0x3F 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.