Coprocessor Firmware
The Pico-side firmware for the realtime I/O coprocessor: architecture, the YM2149 PSG emulation algorithm, the MAX98357A I2S output, the SSD1322 OLED driver, versioning, and the build/flash workflow. Read this if you are modifying the Pico firmware, debugging an audio glitch, debugging an OLED render artifact, or bringing up a fresh Pico 2 board.
../../software/api-reference/grammars/coprocessor-protocol.md— the Pi↔Pico UART wire contract this firmware implements.../../adr/ADR-0017-realtime-io-coprocessor.md— coprocessor architecture decision (and ADR-0019 partial-supersession note for the cart-bus role).../../adr/ADR-0015-cipher-line-auxiliary-display.md— CIPHER-LINE 4-row layout the OLED driver renders.device-tree-overlays.md— Pi-side UART0 overlay this firmware talks over.power-idle.md— dormancy signaling that the Pico’s main loop honors.../hardware/build-specification.md§4 Stage 1c — physical bring-up steps for the coprocessor.
Firmware architecture
Section titled “Firmware architecture”The Pico 2 firmware lives at kn86-pico/ in this repo (deliverable F2 of ADR-0017). It is a Pico-SDK + CMake project with three concurrent subsystems running on the dual-core M33:
+--------------------------------------------------------------+| kn86-pico/ || || Core 0: UART command handler (main loop) || +--> Frame parser (state machine per coproc-protocol §2.4)|| +--> Dispatch table (coproc-protocol §3 frame types) || +--> Heartbeat tracker (§5.2; degraded after 3 missed) || +--> Session state (HELLO/VERSION handshake; §5.3) || || Core 1: Subsystem workers || +--> PSG emulator (YM2149 register set + sample gen) || +--> I2S DMA pump (MAX98357A output, 44.1 kHz) || +--> SSD1322 OLED driver (SPI, framebuffer, ticker) || || PIO state machines (programmable I/O blocks): || +--> SM 0: I2S BCK/LRCK/DOUT to MAX98357A || +--> SM 1: SPI to SSD1322 (or hardware SPI; TBD F2) || +--> SM 2-11: free; reserved for future protocols |+--------------------------------------------------------------+Core 0 owns the UART link end-to-end. Frames arrive byte-at-a-time via interrupt; a small ring buffer feeds the parser; valid frames dispatch to Core-1 workers via lock-free single-producer/single-consumer queues per subsystem. Core 0 never blocks on a worker — fire-and-forget commands (PSG_*, OLED_*) are queued and acknowledged only via async EVENT(BUFFER_OVERFLOW) if the queue is saturated.
Core 1 owns the workers. The I2S DMA pump fires every ~22 µs (per 44.1 kHz sample) and is timing-critical; the OLED driver runs on a soft timer at 30 Hz for ticker scroll updates (CIPHER-LINE Row 2/3 typewriter cadence, ADR-0015). Both worker subsystems pull commands from their queues at queue-poll cadence and apply them.
The Pico-SDK multicore_* API handles the inter-core handoff. There is no RTOS — the firmware is a bare-metal Pico-SDK build with cooperative loops on each core.
power-on -> RP2350 stage-1 boot ROM -> jumps to kn86-pico flash image -> Core 0: init UART0 @ 1 Mbps, init dispatch table, send unsolicited HELLO if no Pi HELLO arrives within 1 s of boot (§5.3 fallback) -> Core 1: init PSG state to all zeros, init I2S DMA pump in idle (no samples), init SSD1322 driver (panel init sequence), display "KN-86 BOOT" splash on OLED -> Both cores enter their main loops; firmware is operationalBoot completes in <100 ms. The Pi typically boots Linux in 5–10 s, so the Pico is always ready by the time the Pi’s kn86-coprocessor.service (boot-and-systemd.md) sends its HELLO.
YM2149 PSG emulation
Section titled “YM2149 PSG emulation”The YM2149 is the General Instruments AY-3-8910-family PSG used in the Atari ST, Amiga, MSX, and ZX Spectrum 128. The Pico firmware emulates it in software at sample-accurate cadence and pumps the samples to the MAX98357A over I2S.
Spec values are canonical in CLAUDE.md (Audio row): 3 tone channels + 1 noise channel, envelope generator, 14 registers, 44.1 kHz mono PCM output. This doc covers the algorithm at a high level; for the exact register layout see the upstream YM2149 datasheet referenced in ADR-0017 and the kn86-pico/src/psg/ source comments.
State machine (one tick per output sample)
Section titled “State machine (one tick per output sample)”1. For each tone channel (A, B, C): - period = (regs[2k+1] << 8) | regs[2k] # 12-bit tone period - tone_counter[k] -= 1 - if tone_counter[k] == 0: tone_phase[k] ^= 1 # square wave flip tone_counter[k] = period - tone_output[k] = tone_phase[k]
2. Noise channel: - noise_period = regs[6] & 0x1F # 5-bit noise period - noise_counter -= 1 - if noise_counter == 0: lfsr_step() # 17-bit LFSR per AY-3-8910 spec noise_counter = noise_period - noise_output = lfsr & 1
3. Mixer (regs[7]): - For each channel: mixed[k] = (tone_output[k] | tone_disable[k]) & (noise_output | noise_disable[k])
4. Envelope generator (regs[11..13]): - Run the 16-step envelope state machine per the regs[13] shape bits - env_amplitude in [0, 15]
5. Per-channel amplitude (regs[8..10]): - if regs[k] & 0x10: amp[k] = env_amplitude - else: amp[k] = regs[k] & 0x0F
6. Sample mix: - sample = sum_k(mixed[k] * volume_lut[amp[k]]) - sample is signed 16-bit, mono
7. Push sample to I2S DMA buffer.The volume_lut is the standard YM2149 logarithmic 16-step volume table. State table size is small (~64 bytes); per-sample compute fits comfortably in the M33 budget at 44.1 kHz.
Register writes
Section titled “Register writes”Pi sends PSG_REG_WRITE(reg, value) (frame type 0x20, coprocessor-protocol.md §4 — exact payload TBD against the spec body). The Pico’s UART handler queues the write to Core 1; Core 1 applies the write to its register file at the next sample-boundary so a register update never lands mid-sample. PSG_BULK_WRITE (0x22) batches multiple register updates atomically — useful for a phase-coherent retrigger.
PSG_RESET (0x21) zeros the register file (effectively silence) and is the path used by power-idle.md to throttle the synth on idle.
I2S out to MAX98357A
Section titled “I2S out to MAX98357A”The MAX98357A is a class-D mono I2S DAC + amplifier. It accepts standard I2S timing (BCLK + LRCLK + DIN) and outputs directly to the speaker. No I²C config required — the chip strap-pins set the gain and channel.
Pico GPIO (TBD F2 bring-up) -BCLK-> MAX98357A BCLKPico GPIO (TBD F2 bring-up) -LRCK-> MAX98357A LRCPico GPIO (TBD F2 bring-up) -DOUT-> MAX98357A DIN | +--> 28mm 8Ω 2W speakerThe Pico’s PIO state machine is the I2S generator: 32-bit frames at 1.4112 MHz BCLK (32× 44.1 kHz). DMA double-buffers the sample stream so the PSG worker writes into one buffer while the PIO drains the other; the swap is interrupt-driven on DMA completion.
Latency from a PSG_REG_WRITE arriving on UART to the corresponding tone change being audible is ~5–20 ms (dominated by the I2S buffer depth). The buffer depth is configurable; the budget per coprocessor-protocol.md §7 (audio latency) is <30 ms end-to-end, leaving comfortable headroom.
SSD1322 OLED driver
Section titled “SSD1322 OLED driver”The SSD1322 controller drives the 256×64 yellow OLED that carries CIPHER-LINE (ADR-0015). It speaks 4-wire SPI: SCLK, MOSI (the SSD1322 input), DC (data/command select), and CS (chip select). RST is a separate GPIO held high in normal operation, pulled low briefly to reset.
Pico SPI (TBD F2 bring-up) -SCLK-> SSD1322 SCLKPico SPI (TBD F2 bring-up) -MOSI-> SSD1322 SDINPico GPIO (TBD F2 bring-up) -CS -> SSD1322 CS#Pico GPIO (TBD F2 bring-up) -DC -> SSD1322 DCPico GPIO (TBD F2 bring-up) -RST -> SSD1322 RST#Init sequence
Section titled “Init sequence”On Pico boot the OLED driver runs the SSD1322 power-on init sequence (the chip’s datasheet is the authoritative source; the Pico firmware’s kn86-pico/src/oled/init.c carries the exact byte sequence):
- Pull RST low for 10 ms, release, wait 100 ms.
- Send
0xFD0x12(unlock command) so subsequent commands are accepted. - Send
0xAE(display off) before reconfiguring. - Set front-clock divider, MUX ratio, display offset, segment remap (the panel’s physical pixel layout has the SEG/COM lines mapped through a remap table — exact values per the datasheet’s panel-orientation guidance).
- Set GPIO/IREF/precharge/VCOMH per panel datasheet recommendations.
- Set contrast (greyscale level — start at mid-range; per-mission brightness tuning is a
power-idle.mdconcern). - Send
0xAF(display on).
After init, the OLED driver maintains a 256×64 1-bit framebuffer in Pico SRAM (~2 KB) and pushes dirty rows to the panel on demand.
4-row CIPHER-LINE layout
Section titled “4-row CIPHER-LINE layout”The framebuffer is logically partitioned into four 256×16-pixel rows per ADR-0015 §2 + CLAUDE.md (Auxiliary Display row):
| Logical row | Pixel rows | Owner / content |
|---|---|---|
| Row 1 | 0–15 | Status strip — battery / timer / mode / TERM hint |
| Row 2 | 16–31 | CIPHER scrollback — current fragment |
| Row 3 | 32–47 | CIPHER scrollback — previous echo (fading) |
| Row 4 | 48–63 | Contextual — seed capture / sub-timer / mission meta |
UART command frames map to the layout:
OLED_SET_ROW(row, "string")(0x30) — replaces the contents ofrow(1–4) withstring. Press Start 2P 8×8 source bitmap, rendered 1× horizontal / 2× vertical to fill the 16-pixel row height.OLED_SCROLL_ROW(row, direction, px_per_sec)(0x31) — animates a row’s existing content scrolling. The Pico runs the scroll on its own 30 Hz timer; the Pi sends the command once and forgets, immune to Pi rendering load (one of the load-bearing reasons for ADR-0017).OLED_FILL(rect, mode)(0x32) — fills a sub-rect with a grayscale level; used for dim policy inpower-idle.md.OLED_CLEAR()(0x33) — blanks the entire panel.
Dirty-rect tracking limits SPI traffic — only changed rows get pushed. A full-panel refresh is ~2 KB at the SPI clock the Pico drives, well under 1 ms.
Versioning
Section titled “Versioning”Firmware semver lives in kn86-pico/include/kn86_pico_version.h:
#define KN86_PICO_FW_MAJOR 0#define KN86_PICO_FW_MINOR 2#define KN86_PICO_FW_PATCH 0#define KN86_PICO_PROTO_MAJOR 0 /* matches coproc-protocol §4.3 */#define KN86_PICO_PROTO_MINOR 2These are reported back to the Pi in the VERSION_RESPONSE frame (coprocessor-protocol.md §4.3) and also embedded in the boot banner that the firmware writes to the OLED at boot — KN-86 PICO v0.2.0 on row 2 of the splash. The git commit hash (lower 32 bits) is included in VERSION_RESPONSE.build_id for bug reports.
The Pi’s coprocessor daemon refuses to enter the operational state if proto_major doesn’t match its expected value (per coproc-protocol §4.3 “Version mismatch behaviour” and ADR-0017 §5). This is a hard fail — the operator sees COPROCESSOR PROTOCOL MISMATCH (v0 vs v1) on Row 24 of the primary display.
Build / flash workflow
Section titled “Build / flash workflow”# Initial setup (one-time):git clone https://github.com/raspberrypi/pico-sdkcd pico-sdk && git submodule update --initexport PICO_SDK_PATH=$PWD
# Build the firmware:cd kn86-picomkdir build && cd buildcmake .. -DPICO_BOARD=pico2make# Output: kn86-pico.uf2Flash paths:
-
Direct USB BOOTSEL (dev fast path). Hold BOOTSEL on the Pico, plug it into a host USB port, the Pico enumerates as USB MSC, drag
kn86-pico.uf2onto the mounted volume. The Pico re-enumerates with the new firmware. Used during F2 bring-up and tight iteration on the bench. -
Pi-mediated BOOTSEL (production path, ADR-0017 §6). The Pi’s coprocessor daemon pulses GPIO22 (BOOTSEL) low, asserts GPIO23 (RESET), releases RESET, waits 100 ms for the Pico to enumerate as USB MSC over the internal hub. The Pi mounts that MSC volume and copies
/lib/firmware/kn86-pico.uf2onto it; releases BOOTSEL; resets again. Used by the firmware-update flow so that a.kn86fwsystem update can ship a new Pi+Pico pair atomically. The Pico image lives in the Pi rootfs at/lib/firmware/kn86-pico.uf2; ADR-0011 ships an amendment that bundles the Pico image into the.kn86fwpayload (see ADR-0017 follow-on F6). -
picotool load(alternative dev path). With the Pico in BOOTSEL on the host:picotool load -x kn86-pico.uf2-xresets and runs the new firmware. Useful when integrating withmake flash.
Production / shipped units
Section titled “Production / shipped units”Shipped KN-86 units come pre-flashed at assembly time — the Pico has the matching kn86-pico.uf2 baked in. Field updates flow through path (2) above. Recovery for a totally bricked Pico (e.g., a corrupt flash image) is TBD final — ADR-0017 §6 mentions a debug header for last-resort recovery; the exact pin assignment and physical access path is TBD pending bring-up.
There is no JTAG/SWD on a stock Pico 2 module, so the recovery story for a bricked Pico-via-module is a USB-BOOTSEL gesture; for a bare-RP2350-on-custom-sub-board production layout (ADR-0017 KU#3) the SWD pads on the chip become the recovery interface. That decision is also TBD pending the production-tooling pass.