- 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>
237 lines
12 KiB
Python
237 lines
12 KiB
Python
#!/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()
|