From 30d5223b9b84c7512449976c81e4e1165a881a16 Mon Sep 17 00:00:00 2001 From: Cyd Date: Mon, 6 Jul 2026 09:29:33 -0500 Subject: [PATCH] RIO firmware: v4.2 EPROM dump, 68HC11 disassembly, reply-latch wedge patch - RIOv4_2.bin: 64K image dumped from the board's AM27C512 (code at $C000-$FFFF, TMP68HC11). - disasm_6811.py + RIOv4_2.disasm.asm: vector-rooted 68HC11 disassembly; SCI ISR at $D630 traced to the $2521 reply-in-progress latch leak that wedges the analog reply path under button-mash stress. - make_patch.py + RIOv4_2_patched.bin: two in-place edits (abort-path stub at $DFF0, unconditional latch clear at $DA21) statically verified by re-disassembly diff. Dynamic proof awaits a burned W27C512. - Analysis + burn/validation plan in RIOv4_2-ANALYSIS.md and README.md. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + rio-firmware/README.md | 42 + rio-firmware/RIOv4_2-ANALYSIS.md | 175 + rio-firmware/RIOv4_2.bin | 38 + rio-firmware/RIOv4_2.disasm.asm | 13135 ++++++++++++++++++++++ rio-firmware/RIOv4_2_patched.bin | 38 + rio-firmware/RIOv4_2_patched.disasm.asm | 13133 +++++++++++++++++++++ rio-firmware/disasm_6811.py | 236 + rio-firmware/make_patch.py | 64 + 9 files changed, 26862 insertions(+) create mode 100644 rio-firmware/README.md create mode 100644 rio-firmware/RIOv4_2-ANALYSIS.md create mode 100644 rio-firmware/RIOv4_2.bin create mode 100644 rio-firmware/RIOv4_2.disasm.asm create mode 100644 rio-firmware/RIOv4_2_patched.bin create mode 100644 rio-firmware/RIOv4_2_patched.disasm.asm create mode 100644 rio-firmware/disasm_6811.py create mode 100644 rio-firmware/make_patch.py diff --git a/.gitignore b/.gitignore index d627f1a..58992b4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # RIOJoy deployment: built zips and bundled third-party installers (not source). /dist/ /deploy/vendor/ +/docs/vJoy_SDK/ # User-specific files *.rsuser diff --git a/rio-firmware/README.md b/rio-firmware/README.md new file mode 100644 index 0000000..4d5bf5b --- /dev/null +++ b/rio-firmware/README.md @@ -0,0 +1,42 @@ +# RIO board firmware + +- **`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. diff --git a/rio-firmware/RIOv4_2-ANALYSIS.md b/rio-firmware/RIOv4_2-ANALYSIS.md new file mode 100644 index 0000000..8115c2f --- /dev/null +++ b/rio-firmware/RIOv4_2-ANALYSIS.md @@ -0,0 +1,175 @@ +# 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. + +### 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. + +## Firmware memory map (as decoded so far) + +| addr | meaning | +|---|---| +| `$292F` | RX state-handler pointer (`JMP $00,X` dispatch) | +| `$2D3B` | TX state-handler pointer | +| `$2D34/$36/$38` | TX ring read/write/aux pointers (ring `$2932-$2D31`) | +| `$2520` | reply gate (analog request pending) | +| `$2521` | **reply-in-progress latch — the wedge** | +| `$2522` | `$87` analog-reply-byte-sent flag | +| `$316C/$6D` | game ACK / NAK received | +| `$316E` | unknown-command seen | +| `$316F/$70` | ACK / NAK pending to send (→ TX ISR) | +| `$3172` | last received byte | +| `$3173/$74/$75` | ACK/NAK/wait retry counters (limit 4) | +| `$317A/$7B` | RESTART / IDLE keep-alive counters | +| `$3184/$85` | give-up / error diagnostic counters | +| `$3186` | RX overrun flag (set at `$D701`, **never read** — not the cause) | +| `$102D/$2E/$2F` | SCCR2 / SCSR / SCDR (HC11 SCI) | diff --git a/rio-firmware/RIOv4_2.bin b/rio-firmware/RIOv4_2.bin new file mode 100644 index 0000000..19c5fe1 --- /dev/null +++ b/rio-firmware/RIOv4_2.bin @@ -0,0 +1,38 @@ +$"f qFLiÐÙƽôýâë,ʽ5ӽ$$ J K M 4 7  N$ɽ|$!$B$C$G$E$I$F$J$$K$$H$1111% %!%"Ty1y~y˽y~<<67̽4hɜ$F$E32889}$I'|$J$J&$!'м$J$I9̭#""9    =  =   @#""9 #""9^ q9oZ&J&9í$E#; @&    @ @#; $I#;%;<<76 %;% +&~8 & +&G ~8& +&6 &0~$') +&  &&~8$!˧A%523889~;&F(;'~O& ̽;'~^& ;'u~o& 4;'d~€ & h;'S~‘& ɜ;'B~¢&%ұB;'\~¹&ҽJ;'H~Ё&*p;'4~˧; '~A|,$!9&ν~;'~~%~ÐÙi M9 K~S J91Ԣ%9 P۰d< 1 18Z&9" + 32< 1 18Z&z 3&9`!b>9H#ν>9H!½>9$>9H$T>9H$>9$Ļ3U9%08 (9 >9 >9oZ&9   Π)Π!Π#Π%Π' 9 Π) 9 Π! 9 Π# 9 Π% 9 Π' 9 + '>9$"91D1X 9i&$"1''X' +''9~~ƿâë$$!J1Ѱ$ɽ1S~bNJǵ$!99˽ 9~ƾi&1'~ƏLiÐÙƽôýâë,$$ J K M 4 7  N$ɽ|$!$B$C$G$E$I$F$J$$K$$H$1111% %!%"~ƾ&z~ƾ&Ċ~ƾ&Ě~ƾ&Ī~ƾ&ĺ9i&1$Gi&1$K$GH$?967<<-A-B;8832967<<-A-B$!;8832967<<-A-B%%;8832967<<-A-B%%;8832967<<-A-B1;8832967<<-A-B1;8832967<<-A-B1;8832967<<$!'0 '% +%-B -A<%%8z%&;88329$!'-A$ -B 7;9$!'#%-A$ -B% J;9$!'#%-A$ -B% K;99 LΠ( Π8 !ʗ #ʰ &   + 9 LΠ Π0 !ʗ #ʰ &    9 LΠ" Π2 !ʗ  #ʰ &     9 LΠ$ Π4 !ʗ  #ʰ &     9 LΠ& Π6 !ʗ  #ʰ &     9 ' L&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ &#  H$ H~ʖ H~ʖ&  H$ H ~ʖ H 9  & ! %9< 89O_ % & #'|   ҽ9<6$!& 8  H  H 6I289<6$!& 8 % H & H 6I289 HDDDD9O L H 9 8962962962962962967<<  N P !̲8832967<<  N : P2: :!̲88329  4 4&~&O~&̀~ͱ "Z& 5 +< 1 18Z&z 5&z 4&9  + + 9 + +  9 + +   9 + + 9  6 5 NZ'D~&r~| z 5&z 6&967<< !:&7< 1 1 : N& J8~d K8$ Ȝ~l$ o8832967<< !:'@< 1 1 : N& J8~κ K8$ Ȝ~Ȇ$ J%o88329<6$!& 8 K H O L6I289 M& +$ ~$ # M& +$ +~0$ +967<< 7$B N$!P϶| NV!P϶| N܈!P϶ܺ!P϶| N!P϶!P϶P!P϶| N| N| N݂!P϶| Nݴ!P϶883299D967<<!R!PZ& 6 +< 1 18Z&z 6&8832967<9H#ν>9H!½>9$>9H$T>9H$>9$Ļ3U9%08 (9 >9 >9oZ&9   Π)Π!Π#Π%Π' 9 Π) 9 Π! 9 Π# 9 Π% 9 Π' 9 + '>9$"91D1X 9i&$"1''X' +''9~~ƿâë$$!J1Ѱ$ɽ1S~bNJǵ$!99˽ 9~ƾi&1'~ƏLiÐÙƽôýâë,$$ J K M 4 7  N$ɽ|$!$B$C$G$E$I$F$J$$K$$H$1111% %!%"~ƾ&z~ƾ&Ċ~ƾ&Ě~ƾ&Ī~ƾ&ĺ9i&1$Gi&1$K$GH$?967<<-A-B;8832967<<-A-B$!;8832967<<-A-B%%;8832967<<-A-B%%;8832967<<-A-B1;8832967<<-A-B1;8832967<<-A-B1;8832967<<$!'0 '% +%-B -A<%%8z%&;88329$!'-A$ -B 7;9$!'#%-A$ -B% J;9$!'#%-A$ -B% K;99 LΠ( Π8 !ʗ #ʰ &   + 9 LΠ Π0 !ʗ #ʰ &    9 LΠ" Π2 !ʗ  #ʰ &     9 LΠ$ Π4 !ʗ  #ʰ &     9 LΠ& Π6 !ʗ  #ʰ &     9 ' L&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ&#  H$ H~ʖ H~ʖ &#  H$ H~ʖ H~ʖ&  H$ H ~ʖ H 9  & ! %9< 89O_ % & #'|   ҽ9<6$!& 8  H  H 6I289<6$!& 8 % H & H 6I289 HDDDD9O L H 9 8962962962962962967<<  N P !̲8832967<<  N : P2: :!̲88329  4 4&~&O~&̀~ͱ "Z& 5 +< 1 18Z&z 5&z 4&9  + + 9 + +  9 + +   9 + + 9  6 5 NZ'D~&r~| z 5&z 6&967<< !:&7< 1 1 : N& J8~d K8$ Ȝ~l$ o8832967<< !:'@< 1 1 : N& J8~κ K8$ Ȝ~Ȇ$ J%o88329<6$!& 8 K H O L6I289 M& +$ ~$ # M& +$ +~0$ +967<< 7$B N$!P϶| NV!P϶| N܈!P϶ܺ!P϶| N!P϶!P϶P!P϶| N| N| N݂!P϶| Nݴ!P϶883299D967<<!R!PZ& 6 +< 1 18Z&z 6&8832967< $C000 confirms). + +Usage: python disasm_6811.py [RIOv4_2.bin] > RIOv4_2.disasm.asm +""" +import sys + +# ---- HC11 internal register block ($1000-$103F) annotations -------------- +REG = { + 0x00:"PORTA",0x02:"PIOC",0x03:"PORTC",0x04:"PORTB",0x05:"PORTCL", + 0x07:"DDRC",0x08:"PORTD",0x09:"DDRD",0x0A:"PORTE",0x0B:"CFORC", + 0x0C:"OC1M",0x0D:"OC1D",0x0E:"TCNT",0x10:"TIC1",0x12:"TIC2", + 0x14:"TIC3",0x16:"TOC1",0x18:"TOC2",0x1A:"TOC3",0x1C:"TOC4", + 0x1E:"TI4O5",0x20:"TCTL1",0x21:"TCTL2",0x22:"TMSK1",0x23:"TFLG1", + 0x24:"TMSK2",0x25:"TFLG2",0x26:"PACTL",0x27:"PACNT",0x28:"SPCR", + 0x29:"SPSR",0x2A:"SPDR",0x2B:"BAUD",0x2C:"SCCR1",0x2D:"SCCR2", + 0x2E:"SCSR",0x2F:"SCDR",0x30:"ADCTL",0x31:"ADR1",0x32:"ADR2", + 0x33:"ADR3",0x34:"ADR4",0x39:"OPTION",0x3A:"COPRST",0x3B:"PPROG", + 0x3C:"HPRIO",0x3D:"INIT",0x3E:"TEST1",0x3F:"CONFIG", +} +def reg_ann(addr): + if 0x1000 <= addr <= 0x103F and (addr-0x1000) in REG: + return " ; "+REG[addr-0x1000] + return "" + +# addressing modes and their extra operand byte counts +INH,IMM8,IMM16,DIR,EXT,IDX,IDY,REL = range(8) +BSET_DIR,BCLR_DIR,BRSET_DIR,BRCLR_DIR = range(8,12) +BSET_IDX,BCLR_IDX,BRSET_IDX,BRCLR_IDX = range(12,16) +MODELEN = {INH:0,IMM8:1,IMM16:2,DIR:1,EXT:2,IDX:1,IDY:1,REL:1, + BSET_DIR:2,BCLR_DIR:2,BRSET_DIR:3,BRCLR_DIR:3, + BSET_IDX:2,BCLR_IDX:2,BRSET_IDX:3,BRCLR_IDX:3} + +# base page opcode table: opcode -> (mnemonic, mode) +OP = { + 0x00:("TEST",INH),0x01:("NOP",INH),0x02:("IDIV",INH),0x03:("FDIV",INH), + 0x04:("LSRD",INH),0x05:("LSLD",INH),0x06:("TAP",INH),0x07:("TPA",INH), + 0x08:("INX",INH),0x09:("DEX",INH),0x0A:("CLV",INH),0x0B:("SEV",INH), + 0x0C:("CLC",INH),0x0D:("SEC",INH),0x0E:("CLI",INH),0x0F:("SEI",INH), + 0x10:("SBA",INH),0x11:("CBA",INH),0x12:("BRSET",BRSET_DIR),0x13:("BRCLR",BRCLR_DIR), + 0x14:("BSET",BSET_DIR),0x15:("BCLR",BCLR_DIR),0x16:("TAB",INH),0x17:("TBA",INH), + 0x19:("DAA",INH),0x1B:("ABA",INH), + 0x1C:("BSET",BSET_IDX),0x1D:("BCLR",BCLR_IDX),0x1E:("BRSET",BRSET_IDX),0x1F:("BRCLR",BRCLR_IDX), + 0x20:("BRA",REL),0x21:("BRN",REL),0x22:("BHI",REL),0x23:("BLS",REL), + 0x24:("BCC",REL),0x25:("BCS",REL),0x26:("BNE",REL),0x27:("BEQ",REL), + 0x28:("BVC",REL),0x29:("BVS",REL),0x2A:("BPL",REL),0x2B:("BMI",REL), + 0x2C:("BGE",REL),0x2D:("BLT",REL),0x2E:("BGT",REL),0x2F:("BLE",REL), + 0x30:("TSX",INH),0x31:("INS",INH),0x32:("PULA",INH),0x33:("PULB",INH), + 0x34:("DES",INH),0x35:("TXS",INH),0x36:("PSHA",INH),0x37:("PSHB",INH), + 0x38:("PULX",INH),0x39:("RTS",INH),0x3A:("ABX",INH),0x3B:("RTI",INH), + 0x3C:("PSHX",INH),0x3D:("MUL",INH),0x3E:("WAI",INH),0x3F:("SWI",INH), + 0x40:("NEGA",INH),0x43:("COMA",INH),0x44:("LSRA",INH),0x46:("RORA",INH), + 0x47:("ASRA",INH),0x48:("LSLA",INH),0x49:("ROLA",INH),0x4A:("DECA",INH), + 0x4C:("INCA",INH),0x4D:("TSTA",INH),0x4F:("CLRA",INH), + 0x50:("NEGB",INH),0x53:("COMB",INH),0x54:("LSRB",INH),0x56:("RORB",INH), + 0x57:("ASRB",INH),0x58:("LSLB",INH),0x59:("ROLB",INH),0x5A:("DECB",INH), + 0x5C:("INCB",INH),0x5D:("TSTB",INH),0x5F:("CLRB",INH), + 0x60:("NEG",IDX),0x63:("COM",IDX),0x64:("LSR",IDX),0x66:("ROR",IDX), + 0x67:("ASR",IDX),0x68:("LSL",IDX),0x69:("ROL",IDX),0x6A:("DEC",IDX), + 0x6C:("INC",IDX),0x6D:("TST",IDX),0x6E:("JMP",IDX),0x6F:("CLR",IDX), + 0x70:("NEG",EXT),0x73:("COM",EXT),0x74:("LSR",EXT),0x76:("ROR",EXT), + 0x77:("ASR",EXT),0x78:("LSL",EXT),0x79:("ROL",EXT),0x7A:("DEC",EXT), + 0x7C:("INC",EXT),0x7D:("TST",EXT),0x7E:("JMP",EXT),0x7F:("CLR",EXT), + 0x80:("SUBA",IMM8),0x81:("CMPA",IMM8),0x82:("SBCA",IMM8),0x83:("SUBD",IMM16), + 0x84:("ANDA",IMM8),0x85:("BITA",IMM8),0x86:("LDAA",IMM8),0x88:("EORA",IMM8), + 0x89:("ADCA",IMM8),0x8A:("ORAA",IMM8),0x8B:("ADDA",IMM8),0x8C:("CPX",IMM16), + 0x8D:("BSR",REL),0x8E:("LDS",IMM16),0x8F:("XGDX",INH), + 0x90:("SUBA",DIR),0x91:("CMPA",DIR),0x92:("SBCA",DIR),0x93:("SUBD",DIR), + 0x94:("ANDA",DIR),0x95:("BITA",DIR),0x96:("LDAA",DIR),0x97:("STAA",DIR), + 0x98:("EORA",DIR),0x99:("ADCA",DIR),0x9A:("ORAA",DIR),0x9B:("ADDA",DIR), + 0x9C:("CPX",DIR),0x9D:("JSR",DIR),0x9E:("LDS",DIR),0x9F:("STS",DIR), + 0xA0:("SUBA",IDX),0xA1:("CMPA",IDX),0xA2:("SBCA",IDX),0xA3:("SUBD",IDX), + 0xA4:("ANDA",IDX),0xA5:("BITA",IDX),0xA6:("LDAA",IDX),0xA7:("STAA",IDX), + 0xA8:("EORA",IDX),0xA9:("ADCA",IDX),0xAA:("ORAA",IDX),0xAB:("ADDA",IDX), + 0xAC:("CPX",IDX),0xAD:("JSR",IDX),0xAE:("LDS",IDX),0xAF:("STS",IDX), + 0xB0:("SUBA",EXT),0xB1:("CMPA",EXT),0xB2:("SBCA",EXT),0xB3:("SUBD",EXT), + 0xB4:("ANDA",EXT),0xB5:("BITA",EXT),0xB6:("LDAA",EXT),0xB7:("STAA",EXT), + 0xB8:("EORA",EXT),0xB9:("ADCA",EXT),0xBA:("ORAA",EXT),0xBB:("ADDA",EXT), + 0xBC:("CPX",EXT),0xBD:("JSR",EXT),0xBE:("LDS",EXT),0xBF:("STS",EXT), + 0xC0:("SUBB",IMM8),0xC1:("CMPB",IMM8),0xC2:("SBCB",IMM8),0xC3:("ADDD",IMM16), + 0xC4:("ANDB",IMM8),0xC5:("BITB",IMM8),0xC6:("LDAB",IMM8),0xC8:("EORB",IMM8), + 0xC9:("ADCB",IMM8),0xCA:("ORAB",IMM8),0xCB:("ADDB",IMM8),0xCC:("LDD",IMM16), + 0xCE:("LDX",IMM16),0xCF:("STOP",INH), + 0xD0:("SUBB",DIR),0xD1:("CMPB",DIR),0xD2:("SBCB",DIR),0xD3:("ADDD",DIR), + 0xD4:("ANDB",DIR),0xD5:("BITB",DIR),0xD6:("LDAB",DIR),0xD7:("STAB",DIR), + 0xD8:("EORB",DIR),0xD9:("ADCB",DIR),0xDA:("ORAB",DIR),0xDB:("ADDB",DIR), + 0xDC:("LDD",DIR),0xDD:("STD",DIR),0xDE:("LDX",DIR),0xDF:("STX",DIR), + 0xE0:("SUBB",IDX),0xE1:("CMPB",IDX),0xE2:("SBCB",IDX),0xE3:("ADDD",IDX), + 0xE4:("ANDB",IDX),0xE5:("BITB",IDX),0xE6:("LDAB",IDX),0xE7:("STAB",IDX), + 0xE8:("EORB",IDX),0xE9:("ADCB",IDX),0xEA:("ORAB",IDX),0xEB:("ADDB",IDX), + 0xEC:("LDD",IDX),0xED:("STD",IDX),0xEE:("LDX",IDX),0xEF:("STX",IDX), + 0xF0:("SUBB",EXT),0xF1:("CMPB",EXT),0xF2:("SBCB",EXT),0xF3:("ADDD",EXT), + 0xF4:("ANDB",EXT),0xF5:("BITB",EXT),0xF6:("LDAB",EXT),0xF7:("STAB",EXT), + 0xF8:("EORB",EXT),0xF9:("ADCB",EXT),0xFA:("ORAB",EXT),0xFB:("ADDB",EXT), + 0xFC:("LDD",EXT),0xFD:("STD",EXT),0xFE:("LDX",EXT),0xFF:("STX",EXT), +} +# page 2 ($18): Y-register/Y-indexed forms +OP18 = { + 0x08:("INY",INH),0x09:("DEY",INH),0x1C:("BSET",BSET_IDX),0x1D:("BCLR",BCLR_IDX), + 0x1E:("BRSET",BRSET_IDX),0x1F:("BRCLR",BRCLR_IDX),0x30:("TSY",INH),0x35:("TYS",INH), + 0x38:("PULY",INH),0x3A:("ABY",INH),0x3C:("PSHY",INH),0x60:("NEG",IDY), + 0x63:("COM",IDY),0x64:("LSR",IDY),0x66:("ROR",IDY),0x67:("ASR",IDY), + 0x68:("LSL",IDY),0x69:("ROL",IDY),0x6A:("DEC",IDY),0x6C:("INC",IDY), + 0x6D:("TST",IDY),0x6E:("JMP",IDY),0x6F:("CLR",IDY),0x8C:("CPY",IMM16), + 0x8F:("XGDY",INH),0x9C:("CPY",DIR),0xA0:("SUBA",IDY),0xA1:("CMPA",IDY), + 0xA2:("SBCA",IDY),0xA3:("SUBD",IDY),0xA4:("ANDA",IDY),0xA5:("BITA",IDY), + 0xA6:("LDAA",IDY),0xA7:("STAA",IDY),0xA8:("EORA",IDY),0xA9:("ADCA",IDY), + 0xAA:("ORAA",IDY),0xAB:("ADDA",IDY),0xAC:("CPY",IDY),0xAD:("JSR",IDY), + 0xAE:("LDS",IDY),0xAF:("STS",IDY),0xBC:("CPY",EXT),0xCE:("LDY",IMM16), + 0xDE:("LDY",DIR),0xDF:("STY",DIR),0xE0:("SUBB",IDY),0xE1:("CMPB",IDY), + 0xE2:("SBCB",IDY),0xE3:("ADDD",IDY),0xE4:("ANDB",IDY),0xE5:("BITB",IDY), + 0xE6:("LDAB",IDY),0xE7:("STAB",IDY),0xE8:("EORB",IDY),0xE9:("ADCB",IDY), + 0xEA:("ORAB",IDY),0xEB:("ADDB",IDY),0xEC:("LDD",IDY),0xED:("STD",IDY), + 0xEE:("LDY",IDY),0xEF:("STY",IDY),0xBE:("LDY",EXT),0xBF:("STY",EXT), +} +OP1A = {0x83:("CPD",IMM16),0x93:("CPD",DIR),0xA3:("CPD",IDX),0xB3:("CPD",EXT), + 0xAC:("CPY",IDX),0xEE:("LDY",IDX),0xEF:("STY",IDX)} +OPCD = {0xA3:("CPD",IDY),0xAC:("CPX",IDY),0xEE:("LDX",IDY),0xEF:("STX",IDY)} + +def u16(d,a): return (d[a]<<8)|d[a+1] + +class Insn: + __slots__=("addr","end","mnem","mode","opbytes","txt","target","flow") + def __init__(s,**k): + for n,v in k.items(): setattr(s,n,v) + +def decode(d,a): + """Decode one instruction at address a. Returns Insn or None (illegal).""" + start=a; op=d[a]; a+=1; pfx=None; table=OP + if op==0x18: pfx=0x18; table=OP18; op=d[a]; a+=1 + elif op==0x1A: pfx=0x1A; table=OP1A; op=d[a]; a+=1 + elif op==0xCD: pfx=0xCD; table=OPCD; op=d[a]; a+=1 + ent=table.get(op) + if ent is None: return None + mnem,mode=ent; n=MODELEN[mode]; ops=d[a:a+n]; a+=n + idxreg="Y" if (mode in (IDY,) or pfx in (0x18,) and mode in (IDX,)) else "X" + if pfx==0xCD: idxreg="Y" + if pfx==0x1A and mode==IDY: idxreg="Y" + tgt=None; flow="seq"; ann="" + if mode==INH: txt=mnem + elif mode==IMM8: txt=f"{mnem} #${ops[0]:02X}" + elif mode==IMM16: + v=(ops[0]<<8)|ops[1]; txt=f"{mnem} #${v:04X}" + elif mode==DIR: + txt=f"{mnem} ${ops[0]:02X}"; ann=reg_ann(ops[0]) + elif mode==EXT: + v=(ops[0]<<8)|ops[1]; txt=f"{mnem} ${v:04X}"; ann=reg_ann(v) + if mnem=="JSR": tgt=v; flow="call" + elif mnem=="JMP": tgt=v; flow="jump" + elif mode==IDX or mode==IDY: + txt=f"{mnem} ${ops[0]:02X},{idxreg}" + elif mode==REL: + rel=ops[0]-256 if ops[0]>127 else ops[0]; tgt=a+rel + txt=f"{mnem} ${tgt:04X}" + if mnem=="BRA": flow="jump" + elif mnem=="BSR": flow="call" + else: flow="branch" + elif mode in (BSET_DIR,BCLR_DIR): + txt=f"{mnem} ${ops[0]:02X},#${ops[1]:02X}"; ann=reg_ann(ops[0]) + elif mode in (BSET_IDX,BCLR_IDX): + txt=f"{mnem} ${ops[0]:02X},{idxreg},#${ops[1]:02X}" + elif mode in (BRSET_DIR,BRCLR_DIR): + rel=ops[2]-256 if ops[2]>127 else ops[2]; tgt=a+rel + txt=f"{mnem} ${ops[0]:02X},#${ops[1]:02X},${tgt:04X}"; ann=reg_ann(ops[0]); flow="branch" + elif mode in (BRSET_IDX,BRCLR_IDX): + rel=ops[2]-256 if ops[2]>127 else ops[2]; tgt=a+rel + txt=f"{mnem} ${ops[0]:02X},{idxreg},#${ops[1]:02X},${tgt:04X}"; flow="branch" + else: txt=mnem + if mnem in ("RTS","RTI","JMP","BRA","STOP","WAI") and flow not in ("call","branch"): + if mnem in ("RTS","RTI","STOP"): flow="end" + ob=bytes([d[i] for i in range(start,a)]) + return Insn(addr=start,end=a,mnem=mnem,mode=mode,opbytes=ob, + txt=txt+ann,target=tgt,flow=flow) + +def main(): + path=sys.argv[1] if len(sys.argv)>1 else "RIOv4_2.bin" + d=open(path,"rb").read() + LO,HI=0xC000,0x10000 + # entry points: reset + all IRQ vectors + entries=set() + for va in range(0xFFD6,0x10000,2): + entries.add(u16(d,va)) + # The RX/TX protocol runs as a pointer state machine: handlers are stored + # into dispatch-pointer RAM vars and reached via `JMP $00,X`, which a + # recursive tracer can't follow. Seed every handler by scanning for + # `LDX #imm16 ; STX ` (CE iw FF pp) and taking imm16. + DISPATCH={0x292F,0x2927,0x2929,0x292B,0x2D3B,0x2D34,0x2D36,0x2D38} + for a in range(LO,HI-5): + if d[a]==0xCE and d[a+3]==0xFF: + iw=(d[a+1]<<8)|d[a+2]; pp=(d[a+4]<<8)|d[a+5] + if pp in DISPATCH and LO<=iw RIOv4_2_patched.bin. + +Fix (see RIOv4_2-ANALYSIS.md): clear the reply-in-progress latch $2521 on +EVERY reply teardown, not just the $2522-gated success path. + + 1. Give-up path: redirect $D9DD `JMP $DA2F` to a stub at free ROM $DFF0 + that clears $2521/$2522 then continues to $DA2F. + 2. Success path: make $DA00's clear of $2521/$2522 unconditional. + +Each edit asserts the exact original bytes first, so a wrong assumption +aborts instead of corrupting the image. Address == file offset. +""" +import sys, hashlib + +SRC = sys.argv[1] if len(sys.argv) > 1 else "RIOv4_2.bin" +DST = sys.argv[2] if len(sys.argv) > 2 else "RIOv4_2_patched.bin" + +d = bytearray(open(SRC, "rb").read()) +assert len(d) == 0x10000, f"expected 64KB image, got {len(d)}" +orig_sha = hashlib.sha256(d).hexdigest() +assert orig_sha == "60a88718835c654b6135dbec7721c40ef99dca07df2ad4b57eedeb24037a5f73", \ + f"unexpected source image {orig_sha}" + +def patch(addr, expect, new): + got = bytes(d[addr:addr+len(expect)]) + assert got == bytes(expect), ( + f"@${addr:04X}: expected {got.hex()} to be {bytes(expect).hex()}") + assert len(new) == len(expect), "length mismatch" + d[addr:addr+len(new)] = bytes(new) + +# --- edit 1: give-up path redirect --------------------------------------- +# $D9DD 7E DA 2F JMP $DA2F -> 7E DF F0 JMP $DFF0 +patch(0xD9DD, [0x7E, 0xDA, 0x2F], [0x7E, 0xDF, 0xF0]) + +# stub at $DFF0 (was erased $FF): CLR $2521; CLR $2522; JMP $DA2F +patch(0xDFF0, [0xFF]*8, + [0x7F, 0x25, 0x21, # CLR $2521 + 0x7F, 0x25, 0x22, # CLR $2522 + 0x7E, 0xDA]) # JMP $DA2F (hi + first target byte) +patch(0xDFF8, [0xFF], [0x2F]) # JMP low byte + +# --- edit 2: success teardown, unconditional clear ----------------------- +# $DA21 B6 25 22 LDAA $2522 +# $DA24 81 01 CMPA #$01 +# $DA26 26 06 BNE $DA2E +# $DA28 7F 25 21 CLR $2521 +# $DA2B 7F 25 22 CLR $2522 (13 bytes $DA21-$DA2D; $DA2E RTS untouched) +# -> CLR $2521 ; CLR $2522 ; NOP x7 +patch(0xDA21, + [0xB6,0x25,0x22, 0x81,0x01, 0x26,0x06, 0x7F,0x25,0x21, 0x7F,0x25,0x22], + [0x7F,0x25,0x21, 0x7F,0x25,0x22, 0x01,0x01,0x01,0x01,0x01,0x01,0x01]) +assert d[0xDA2E] == 0x39, "RTS at $DA2E must be intact" + +open(DST, "wb").write(d) +new_sha = hashlib.sha256(d).hexdigest() +# byte-diff report +diffs = [(a, orig, d[a]) for a, orig in + enumerate(open(SRC,"rb").read()) if d[a] != orig] +print(f"source : {SRC} sha256 {orig_sha}") +print(f"patched: {DST} sha256 {new_sha}") +print(f"{len(diffs)} bytes changed:") +for a, o, n in diffs: + print(f" ${a:04X}: {o:02X} -> {n:02X}")