RIO firmware: disassemble v4.2, find the reply-wedge root cause
68HC11 recursive-descent disassembler (disasm_6811.py, follows the pointer-based RX/TX state dispatch) + full disassembly + analysis doc. Root cause of the stress wedge: an orphaned reply-in-progress latch ($2521). It gates every analog request ($D758); it is set when a reply is generated ($D84C) but the 4-retry give-up path ($D9DD -> $DA2F) tears down reply state without clearing it, and the success teardown ($DA00) clears it only conditionally on $2522. Once leaked, all analog requests are dropped -> board mute, while RX/event stays alive; only the game-start host-reset command ($C686) clears it -- matching the field button-resync ritual exactly. Proposed minimal in-place fix (clear $2521 on every teardown) documented with byte patches and a hardware validation plan; untested pending a spare EPROM. RIOv4_2-ANALYSIS.md has the details. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+16
-4
@@ -210,10 +210,22 @@ No firmware source or image exists in the archive (searched sda4 + CODE
|
|||||||
for *.HEX/*.A51/*.S19/*.ROM and for the protocol constant names — only
|
for *.HEX/*.A51/*.S19/*.ROM and for the protocol constant names — only
|
||||||
PCSPAK.ASM, the game side, matches).
|
PCSPAK.ASM, the game side, matches).
|
||||||
|
|
||||||
**UPDATE 2026-07-04: steps 1-2 DONE.** The board's MCU is a **Toshiba
|
**UPDATE 2026-07-04: steps 1-3 DONE — ROOT CAUSE FOUND.** Full writeup:
|
||||||
TMP68HC11** (read off the chip) and the user dumped the EPROM:
|
`emulator/rio-firmware/RIOv4_2-ANALYSIS.md` (disassembly via
|
||||||
`emulator/rio-firmware/RIOv4_2.bin` (v4.2, 64KB image, code in
|
`disasm_6811.py` -> `RIOv4_2.disasm.asm`). The wedge = an orphaned
|
||||||
$C000-$FFFF). First-look analysis in `emulator/rio-firmware/README.md` —
|
"reply-in-progress" latch `$2521`: it gates every analog request (`$D758
|
||||||
|
TST $2521; BNE drop`), is set when a reply is generated (`$D84C`), but the
|
||||||
|
retry-exhausted give-up path (`$D9DD JMP $DA2F`) tears down reply state
|
||||||
|
WITHOUT clearing it (and the success teardown `$DA00` clears it only
|
||||||
|
conditionally on `$2522`). Leaked -> all analog requests dropped -> mute;
|
||||||
|
RX/event path stays alive; only a game-start host reset command (`$C686`
|
||||||
|
clears `$2521`) revives it = the observed button-resync ritual. Proposed
|
||||||
|
fix (untested, no spare chip): clear `$2521` on every teardown (redirect
|
||||||
|
`$D9DD` to a free-space stub at `$DFF0` clearing `$2521/$2522`; make
|
||||||
|
`$DA00`'s clear unconditional). Byte patches + validation plan in the doc.
|
||||||
|
Original first-look (MCU=Toshiba TMP68HC11, EPROM=AM27C512, code
|
||||||
|
$C000-$FFFF, reset $C000, SCI vector $D630) in
|
||||||
|
`emulator/rio-firmware/README.md` -
|
||||||
reset vector $C000, **SCI serial interrupt vector → $D630** (the protocol
|
reset vector $C000, **SCI serial interrupt vector → $D630** (the protocol
|
||||||
state machine's entry point for step 3's disassembly). Also new evidence
|
state machine's entry point for step 3's disassembly). Also new evidence
|
||||||
for the wedge shape: a button press revives a mute board (event path
|
for the wedge shape: a button press revives a mute board (event path
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
### 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 it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
decoded 2728 insns, 328 labels, 112 call targets
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user