diff --git a/emulator/RIO-NOTES.md b/emulator/RIO-NOTES.md index c123aaa..3f94d3d 100644 --- a/emulator/RIO-NOTES.md +++ b/emulator/RIO-NOTES.md @@ -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 PCSPAK.ASM, the game side, matches). -**UPDATE 2026-07-04: steps 1-2 DONE.** The board's MCU is a **Toshiba -TMP68HC11** (read off the chip) and the user dumped the EPROM: -`emulator/rio-firmware/RIOv4_2.bin` (v4.2, 64KB image, code in -$C000-$FFFF). First-look analysis in `emulator/rio-firmware/README.md` — +**UPDATE 2026-07-04: steps 1-3 DONE — ROOT CAUSE FOUND.** Full writeup: +`emulator/rio-firmware/RIOv4_2-ANALYSIS.md` (disassembly via +`disasm_6811.py` -> `RIOv4_2.disasm.asm`). The wedge = an orphaned +"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 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 diff --git a/emulator/rio-firmware/RIOv4_2-ANALYSIS.md b/emulator/rio-firmware/RIOv4_2-ANALYSIS.md new file mode 100644 index 0000000..e609bb2 --- /dev/null +++ b/emulator/rio-firmware/RIOv4_2-ANALYSIS.md @@ -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) | diff --git a/emulator/rio-firmware/RIOv4_2.disasm.asm b/emulator/rio-firmware/RIOv4_2.disasm.asm new file mode 100644 index 0000000..06b047e --- /dev/null +++ b/emulator/rio-firmware/RIOv4_2.disasm.asm @@ -0,0 +1,13135 @@ +C000 0F LC000: SEI +C001 8E 80 00 LDS #$8000 +C004 86 00 LDAA #$00 +C006 B7 10 24 STAA $1024 ; TMSK2 +C009 B7 10 22 STAA $1022 ; TMSK1 +C00C BD C1 66 JSR $C166 +C00F BD D6 20 JSR $D620 +C012 BD D6 71 JSR $D671 +C015 BD C4 46 JSR $C446 +C018 BD C3 4C JSR $C34C +C01B BD C3 69 JSR $C369 +C01E BD C3 90 JSR $C390 +C021 BD C3 99 JSR $C399 +C024 BD C3 C6 JSR $C3C6 +C027 BD C3 B4 JSR $C3B4 +C02A BD C3 BD JSR $C3BD +C02D BD C3 A2 JSR $C3A2 +C030 BD C3 AB JSR $C3AB +C033 BD C4 2C JSR $C42C +C036 BD C3 F1 JSR $C3F1 +C039 BD C4 CA JSR $C4CA +C03C BD C4 35 JSR $C435 +C03F BD C4 D3 JSR $C4D3 +C042 BD C4 F0 JSR $C4F0 +C045 7F 24 1E CLR $241E +C048 7F 24 17 CLR $2417 +C04B 7F 20 4A CLR $204A +C04E 7F 20 4B CLR $204B +C051 7F 20 4D CLR $204D +C054 7F 20 34 CLR $2034 +C057 7F 20 37 CLR $2037 +C05A 7F 20 FB CLR $20FB +C05D 86 01 LDAA #$01 +C05F B7 20 4E STAA $204E +C062 7F 24 16 CLR $2416 +C065 BD D3 C9 JSR $D3C9 +C068 BD CB 7C JSR $CB7C +C06B 7F 24 21 CLR $2421 +C06E 7F 24 42 CLR $2442 +C071 7F 24 43 CLR $2443 +C074 7F 24 47 CLR $2447 +C077 7F 24 45 CLR $2445 +C07A 7F 24 49 CLR $2449 +C07D 7F 24 46 CLR $2446 +C080 7F 24 4A CLR $244A +C083 7F 24 E5 CLR $24E5 +C086 7F 24 4B CLR $244B +C089 86 80 LDAA #$80 +C08B B7 24 E8 STAA $24E8 +C08E 7F 24 48 CLR $2448 +C091 7F 24 E9 CLR $24E9 +C094 7F 31 84 CLR $3184 +C097 7F 31 85 CLR $3185 +C09A 7F 31 86 CLR $3186 +C09D 7F 31 87 CLR $3187 +C0A0 7F 25 20 CLR $2520 +C0A3 7F 25 21 CLR $2521 +C0A6 7F 25 22 CLR $2522 +C0A9 BD C1 1D JSR $C11D +C0AC BD C1 54 JSR $C154 +C0AF 0E CLI +C0B0 BD C5 79 LC0B0: JSR $C579 +C0B3 BD CF 31 JSR $CF31 +C0B6 BD C5 79 JSR $C579 +C0B9 BD CC 7E JSR $CC7E +C0BC BD C5 79 JSR $C579 +C0BF BD C1 CB JSR $C1CB +C0C2 BD C5 79 JSR $C579 +C0C5 BD C0 EB JSR $C0EB +C0C8 7E C0 B0 JMP $C0B0 +C0CB 3C LC0CB: PSHX << $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