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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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()
|
||||
@@ -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}")
|
||||
Reference in New Issue
Block a user