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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-06 09:29:33 -05:00
co-authored by Claude Fable 5
parent d1a938dd43
commit 30d5223b9b
9 changed files with 26862 additions and 0 deletions
+1
View File
@@ -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
+42
View File
@@ -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.
+175
View File
@@ -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) |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Recursive-descent 68HC11 disassembler for the RIO board firmware.
The RIO board (Toshiba TMP68HC11 + AM27C512) speaks the PCSPAK serial
protocol to the game. This tool disassembles RIOv4_2.bin to help locate the
receive/reply state machine and the wedge where the reply path dies under
stress. Address == file offset for this image (EPROM occupies $C000-$FFFF,
reset vector $FFFE -> $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 <dispatchvar>` (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<HI:
entries.add(iw)
insns={}; labels=set(); calls=set()
work=[e for e in entries if LO<=e<HI]
for e in work: labels.add(e)
seen=set()
while work:
a=work.pop()
while LO<=a<HI and a not in insns:
ins=decode(d,a)
if ins is None: break
insns[a]=ins
if ins.target is not None and LO<=ins.target<HI:
labels.add(ins.target)
if ins.flow in ("call",): calls.add(ins.target)
if ins.flow in ("call","branch","jump"):
if ins.target not in insns: work.append(ins.target)
if ins.flow in ("end","jump"): break
a=ins.end
# emit listing
out=[]
a=LO
while a<HI:
if a in insns:
ins=insns[a]
lbl=f"L{a:04X}:" if a in labels else ""
mark=" <<<CALLED" if a in calls else ""
hexb=" ".join(f"{b:02X}" for b in ins.opbytes)
out.append(f"{a:04X} {hexb:<20} {lbl:<8}{ins.txt}{mark}")
a=ins.end
else:
# data byte
out.append(f"{a:04X} {d[a]:02X} .byte ${d[a]:02X}")
a+=1
sys.stdout.write("\n".join(out)+"\n")
sys.stderr.write(f"decoded {len(insns)} insns, {len(labels)} labels, "
f"{len(calls)} call targets\n")
if __name__=="__main__":
main()
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Apply the RIO v4.2 reply-wedge fix to RIOv4_2.bin -> 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}")