diff --git a/restoration/rio-firmware/README.md b/restoration/rio-firmware/README.md new file mode 100644 index 00000000..27abd47e --- /dev/null +++ b/restoration/rio-firmware/README.md @@ -0,0 +1,100 @@ +# RIO board firmware + +> **Pod owner, not an engineer?** Read +> [wedge-explained.md](wedge-explained.md) — a plain-language account of +> the "stick died mid-battle" bug that RIO 4.3 fixes. + +- **`RIOv4_2.bin`** — RIO cockpit I/O board firmware **v4.2**, dumped + 2026-07-04 from one of our own boards' EPROM: an **AMD AM27C512-150** + (64K x 8 UV EPROM, 150ns — the image fills it exactly). + sha256 `60a88718835c654b6135dbec7721c40ef99dca07df2ad4b57eedeb24037a5f73`. + For the eventual patched burn: a pin-compatible Winbond W27C512 + (electrically erasable, TL866-friendly) drops straight into the socket; + the original AMD chip gets labeled and preserved unmodified. + +## First-look analysis (from the image alone, confirmed on hardware) + +- MCU: **Toshiba TMP68HC11** (read off the chip; the code fingerprint + agrees — 6800-family opcodes with writes into the 68HC11 internal + register block at `$10xx`). +- Memory map: image is FF up to **0xC000**; 16KB of code occupies + `$C000-$FFFF` (EPROM mapped at the top of the HC11 address space). +- Startup at `$C000`: `SEI; LDS #$8000; STAA $1024 (TMSK2); + STAA $1022 (TMSK1); ...` then a long `JSR` init chain — textbook HC11 + bring-up. +- Vector table (`$FFD6-$FFFF`, big-endian): + - `$FFFE` RESET → `$C000` + - **`$FFD6` SCI (serial) → `$D630`** — the entry point of the board's + receive/protocol interrupt handler. The suspected board-side + DISABLE_AND_DIE-style wedge (see RIO-NOTES.md: the board mirrors the + game's PCSPAK state machine, and mash-stress leaves the reply path + dead while the button/event path stays alive) is reachable from here. + - `$FFE4` → `$C1B2`, `$FFE6` → `$C18E` (timer output-compares); most + other vectors → `$DB07..$DB3D` stubs. + +## Why this exists + +The remaining RIO reliability issue is board-side: under button-mash +stress the board's reply/analog state machine wedges (RX dead, TX alive; +a button press or power cycle revives it), reproduced identically on two +different USB serial adapters. The game-side half of the protocol was +binary-patched for tolerance (BTL4OPT patches v2-v4); the board firmware +is the other half. Plan (RIO-NOTES.md "Board firmware patch plan"): +disassemble as 68HC11 from `$C000` with the vector entries as roots, find +the SCI state machine (protocol constants FC=ACK FD=NAK FE=RESTART +FF=IDLE, idle-reload-4 patterns), patch the early-ACK/error wedge path or +widen its window, burn a new EPROM, keep this original safe. + +## RIO 4.3 — `RIOv4_3.bin` (current production firmware) + +**Christened 2026-07-19** after full on-hardware certification. Built +by `make_patch.py --e0thresh=5 --checkrepaint --reportversion=4.3 +RIOv4_2.bin RIOv4_3.bin`; sha256 +`6d67a2fc77130b601fdb0ac02042dd4d0a98ac7e29a8077d588987a81073939c` +(69 bytes changed vs stock). 9600 baud, native-game compatible. On the +stock v4.2 base it carries: + +1. **Reply-latch wedge fix** (edits 1-2) — bench-certified. +2. **E0-display threshold, N=5** (edit 5) — certified: display holds + `F0000000` through sub-threshold error events, flips to the live + `E0` readout at the 5th teardown (`E0000105` on the bench). +3. **Check self-test display repaint** (edit 6) — certified both ways: + version+check leaves `F0000000` when healthy, and re-renders the E0 + readout when counters are over threshold (`E0000305` on the bench) + instead of stock's stale `04000000`. +4. **Version bump** (edit 7, `--reportversion=4.3`) — the VersionReply + now announces **4.3** (one operand byte at `$C6FF`). Nothing + host-side validates the value (native games verified not to check + it). The certification runs below were made on the pre-edit-7 bytes; + the only delta is the version literal. Chips burned before this edit + report 4.2 — re-burn to announce 4.3. + +Certification runs (all in `testlogs/` + RIOv4_2-ANALYSIS.md): two +`--e0test` acceptance passes (display verified by eye at each step) and +a 120 s mash at **549 presses/min sustained** — zero wedges, board +counters flat, zero NAK, 247 resends all healed +(`riomash-rio43rc1-9600-20260719-164901.log`). + +## FastRIO 4.3 — `RIOv4_3_fastrio.bin` + +Same generation for the RIOJoy-only cockpits: `make_patch.py +--baud31250 --widen-ackwait --e0thresh=5 --checkrepaint +--reportversion=4.3 RIOv4_2.bin RIOv4_3_fastrio.bin`; sha256 +`ee807831fe0df9dc147eb5cdc3302672ae21f6ff349da30babea27fc863234b9` +(71 bytes vs stock — differs from `RIOv4_3.bin` in exactly two bytes: +`$D62B` baud `30→02` and `$D9E7` ACK-wait `04→28`). 31250 baud, +FTDI-class adapter required, **not** native-game compatible. +Disassembly-verified; run the acceptance ladder on the FastRIO cockpit +before deploying (`--e0test COM1` auto-probes 31250, then a mash +spot-check at `--baud 31250`). + +## Archive + +All prior generations live in [`archive/`](archive/) with their +disassemblies: `RIOv4_2_patched` (wedge fix only), `_31250`/`_31250v2` +(FastRIO speed builds), `_62500`/`_125000` (speed-ladder science runs), +and the `_e0t5` pair (threshold without check-repaint). Bench verdicts +and per-edit history: RIOv4_2-ANALYSIS.md. The pristine dump +`RIOv4_2.bin` stays at top level — it is the source every patch builds +from. RIO 4.3 is in the cockpit socket as of 2026-07-19; the original +AMD chip and the retired e0t5 burn are labeled and preserved. diff --git a/restoration/rio-firmware/RIOv4_2-ANALYSIS.md b/restoration/rio-firmware/RIOv4_2-ANALYSIS.md new file mode 100644 index 00000000..4bc0404e --- /dev/null +++ b/restoration/rio-firmware/RIOv4_2-ANALYSIS.md @@ -0,0 +1,559 @@ +# RIO v4.2 firmware — protocol wedge analysis + +Reverse-engineering of `RIOv4_2.bin` (Toshiba TMP68HC11, AM27C512) to find +the board-side cause of the "reply path wedges under stress, button-press +revives it" fault. Disassembly by `disasm_6811.py` → +`RIOv4_2.disasm.asm`. **Research only** — the fix below is proposed, not +yet burned or tested (no spare EPROM on hand). Validate on hardware with +the `RIO_TAP` mash test before trusting. + +Addresses are CPU = file offset (EPROM at `$C000-$FFFF`; reset `$FFFE`→ +`$C000`). RAM lives at `$20xx-$31xx`. + +## How the serial protocol is structured + +- **SCI interrupt** (`$FFD6`→`$D630`): `JSR $D634; RTI`. `$D634` runs BOTH + workers every interrupt: `JSR $D6EA` (RX) then `JSR $D887` (TX). So the + transmitter is poked after every received byte, not only on TX-empty + interrupts. +- **RX ISR** `$D6EA`: reads SCSR/SCDR, stores the byte at `$3172`, then + `LDX $292F; JMP $00,X` — dispatches through a **state-handler pointer** + at `$292F`. Handlers classify bytes (`$D717`: `FE`=RESTART, `FF`=IDLE, + `FC`/`FD`=game ACK/NAK, `$82`=analog request, table lookup at `$3144`), + accumulate the body + checksum (`AND $7F`), and on a complete packet run + the ACK/NAK decision at `$D81F`. +- **TX ISR** `$D887`: if TDRE, send a pending ACK (`$316F`→`$FC`) or NAK + (`$3170`→`$FD`), else dispatch through the TX state pointer `$2D3B` + (`$D8C2` ring-drain → `$D90E` reply/retry machine). When idle it disarms + the TX interrupt (SCCR2 `#$2C`, TIE off) at `$D918`; the enqueue routine + `$D63B` re-arms it (SCCR2 `#$AC`, TIE on) at `$D664`. + +## The wedge: an orphaned "reply-in-progress" latch (`$2521`) + +`$2521` = "an analog reply is in progress." The analog-request handler +gates on it: + +``` +D74F CMPB #$82 ; analog request from the game +D753 LDAA #$01 +D755 STAA $2520 ; arm reply generation +D758 TST $2521 ; already replying? +D75B BNE $D77A ; YES -> D77A: CLR $2520, drop this request +``` + +So while `$2521` is set, **every analog request is silently dropped**. +The latch is set when a reply is generated: + +``` +D847 JSR $C5EC ; build the analog reply +D84C STAA $2521 ; reply-in-progress = 1 +``` + +and is cleared in only three places: power-on init (`$C0A3`), a host +reset/init command handler (`$C686`), and the reply **success** teardown +(`$DA00`). The success teardown is reached at `$D9C1` when the game ACKs +the reply, and clears the latch — but only conditionally: + +``` +DA21 LDAA $2522 ; did the $87 reply byte actually start sending? +DA24 CMPA #$01 +DA26 BNE $DA2E ; if not, skip the clears <-- fragile +DA28 CLR $2521 +DA2B CLR $2522 +``` + +`$2521` is set the instant the reply is *generated* (`$D84C`), but `$2522` +is set only once the `$87` command byte *starts transmitting* (`$D8FD`). + +**The leak** is the retry-exhausted give-up path, which is *separate* from +the success teardown. When the game fails to ACK a reply, `$D90E`/`$D9BE` +retries up to 4 times, then gives up: + +``` +D9D5 LDAB #$FE ; give up: send RESTART +D9D7 STAB $102F ; SCDR +D9DA INC $317A +D9DD JMP $DA2F ; teardown -- but DA2F never touches $2521 +``` + +`$DA2F` resets the TX pointers and calls `$D5F2` (a debug-counter +formatter that does *not* clear the latch), then returns. **`$2521` is +left set forever.** From then on every `$82` analog request is dropped at +`$D758` → the board is mute to analog while its RX/event path stays fully +alive. + +### Why a button press / new game revives it + +The only mid-run code that clears `$2521` is the host command handler at +`$C669-$C689` (it clears `$2520`/`$2521`/`$2522` plus a raft of state). +That runs for a host-level reset/init command — exactly what the game +sends at game-start / on the player's opening button actions. Mid-mission +button-mashing sends no such command, so the leaked latch stays stuck +until the next game-start reset. This matches the field observation +precisely: the board goes mute under stress and only a new-game/button +resync brings analog back. + +### Why mash stress triggers it + +Button-event traffic floods the link while the board is mid-analog-reply; +the reply's ACKs collide/drop, the 4-retry budget exhausts, and the +give-up path (`$DA2F`) fires — leaking the latch. Light traffic rarely +exhausts the retries, so it's a stress-only fault. Two different USB +adapters showed the identical stall because the defect is in the board, +not the transport — consistent with this being firmware, not timing. + +## Proposed fix (minimal, in-place; UNTESTED) + +Clear `$2521` on *every* reply teardown, not just the `$2522`-gated +success path. Two edits, no code-size change, 8 KB of free ROM exists at +`$DFF0-$FFBF` for the stub: + +1. **Give-up path** — redirect its teardown through a stub that clears the + latch first. At `$D9DD` change `JMP $DA2F` (`7E DA 2F`) → + `JMP $DFF0` (`7E DF F0`), and place at `$DFF0`: + ``` + DFF0 7F 25 21 CLR $2521 + DFF3 7F 25 22 CLR $2522 + DFF6 7E DA 2F JMP $DA2F + ``` +2. **Success path** — make the clear unconditional (belt-and-suspenders, + covers an abort before `$87` is sent). Replace `$DA21-$DA2D` (13 bytes) + in place: + ``` + DA21 7F 25 21 CLR $2521 + DA24 7F 25 22 CLR $2522 + DA27 01 01 01 01 01 01 01 (NOP x7) + DA2E 39 RTS (unchanged) + ``` + +Rationale: `$2521` means "a reply is in progress"; any path that tears +down reply state must release it. There is no case where you reset the +reply machine yet want the latch to stay set, so unconditional clearing is +safe. This is the board-side analogue of the game-side "make collisions +harmless" patches (BTL4OPT v2-v4) — instead of widening a timing window it +removes the latch leak entirely. + +### Patched binary — built & statically verified (2026-07-04) + +`make_patch.py` applies both edits to `RIOv4_2.bin` (asserting the exact +original bytes at each site first) → **`RIOv4_2_patched.bin`** +(sha256 `3fc8170caf60e2580641724ff995176c93c4f2e706f31487beded8233142493f`, +23 bytes changed). Re-disassembling it (`RIOv4_2_patched.disasm.asm`) and +diffing against the original confirms the change is confined to exactly +three regions with no downstream desync: +- `$D9DD` `JMP $DA2F` → `JMP $DFF0` +- `$DFF0` new stub: `CLR $2521 ; CLR $2522 ; JMP $DA2F` +- `$DA21` `CLR $2521 ; CLR $2522 ; NOP×7` (RTS at `$DA2E` intact) + +Flash `RIOv4_2_patched.bin` directly to the W27C512 (DIP-28). This is +static verification only; dynamic proof still needs the burned chip. + +### 31250-baud variant (2026-07-17) — `RIOv4_2_patched_31250.bin` + +`make_patch.py --baud31250` adds **one byte** to the wedge patch: the SCI +init at `$D62A` (`LDAA #$30 ; STAA $102B BAUD`) becomes `LDAA #$02`. +BAUD `$30` = prescale ÷13, divider ÷1 → 2 MHz E-clock / (16·13) = 9615 +("9600"); `$02` = prescale ÷1, divider ÷4 → 2 MHz / (16·4) = **31250 +exactly** (3.3× faster; analog exchange ~15 ms → ~4.6 ms; 96-lamp burst +~0.4 s → ~0.12 s; every collision window shrinks 3×). The BAUD write +confirms the 8 MHz crystal / 2 MHz E-clock — 19200/38400 are unreachable. + +sha256 `9f866cf353d04906e1d3e3847b6375eaae251c987b656f68f093e61ba1bd545b`, +24 bytes changed (the 23 wedge bytes + `$D62B: 30→02`). Re-disassembly +diff vs the classic patched image shows exactly the one operand line. + +**Caveats:** 31250 is non-standard — FTDI-class USB adapters make it +exactly, classic 16550 UARTs cannot; the **native games still speak +9600**, so a 31250 chip is bench/RIOJoy-only until Firestorm/Red Planet +get BTL4OPT-style baud patches; and the 2 MHz HC11's RX ISR has ~640 +cycles/byte at this rate (fine) — do NOT be tempted to `$01`/62500 or +`$00`/125 k without cycle-counting the `$D630` ISR worst path first. +PC side: `SerialPortTransport` takes a baud parameter and the +monitor/mash tools accept `--baud 31250`. Validate the wedge patch at +9600 FIRST (one variable at a time), then A/B this chip with +`--mash COM1 300 --label patched-31250 --baud 31250`. + +### Validation plan (when a chip is available) +Burn the two edits to a W27C512, socket it (preserve the original AMD +chip), then run the `RIO_TAP` two-handed 8-button mash test. Expect: no +permanent analog mute; any collision self-recovers without a game-start +reset. Compare dropout counts to the 2026-07-03/04 baseline taps. + +**Instrumented harness** (2026-07-17): the mash protocol is mechanized in +`tools/RioSerialMonitor` — + + dotnet run --project tools/RioSerialMonitor -- --mash COM1 300 --label patched + +It disables the app's own >5 s reset-recovery (a wedge must stay observable), +echoes lamps on every press (lamp/reply collisions are the trigger), and logs +to `riomash-