Make the repo self-contained (no dependency on the parent firmware-tools/ checkout): copy in the patch/disasm/syx tools and the c2probe RP2040 C2 flash-reader/patcher firmware. - patch_usb1_to_cv.py : byte-level USB-1->CV patch (imported by roundtrip.py) - d8051.py : standalone 8051 disassembler - syx_extract.py : SysEx extractor - c2probe/ : RP2040 C2 flash reader + host scripts (c2probe.c, cdc/dump_flash/patch_c2/reflash_page/verify.py, CMake build) - RECOVERY.md : C2 flash recovery procedure - EFM8UB20_PINOUT.md : reverse-engineered QFP48 pinout + firmware pin usage - roundtrip.py : import local patch_usb1_to_cv (parent as fallback) - .gitignore : exclude c2probe/.venv, c2probe/build Verified: stock + patched round-trips still re-assemble byte-identical.
191 lines
7.5 KiB
Python
191 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Minimal but complete MCS-51 (8051) disassembler."""
|
|
|
|
SFR = {
|
|
0x80: 'P0', 0x81: 'SP', 0x82: 'DPL', 0x83: 'DPH', 0x84: 'DPL1', 0x85: 'DPH1',
|
|
0x86: 'DPS', 0x87: 'PCON', 0x88: 'TCON', 0x89: 'TMOD', 0x8A: 'TL0', 0x8B: 'TL1',
|
|
0x8C: 'TH0', 0x8D: 'TH1', 0x8E: 'CKCON', 0x90: 'P1', 0x91: 'TMR3CN',
|
|
0x98: 'SCON0', 0x99: 'SBUF0', 0xA0: 'P2', 0xA8: 'IE', 0xB0: 'P3', 0xB8: 'IP',
|
|
0xC0: 'SMB0CN', 0xC8: 'TMR2CN', 0xD0: 'PSW', 0xD8: 'WDTCN', 0xE0: 'ACC',
|
|
0xE8: 'EIE1', 0xF0: 'B', 0xF8: 'EIP1',
|
|
}
|
|
BITSFR = {0xD0: 'PSW', 0xD8: 'WDTCN', 0xE0: 'ACC', 0xF0: 'B', 0x80: 'P0', 0x88: 'TCON',
|
|
0x90: 'P1', 0x98: 'SCON0', 0xA0: 'P2', 0xA8: 'IE', 0xB0: 'P3', 0xB8: 'IP',
|
|
0xC0: 'SMB0CN', 0xC8: 'TMR2CN'}
|
|
|
|
|
|
def d(a):
|
|
return SFR.get(a, f'0x{a:02X}')
|
|
|
|
|
|
def bit(a):
|
|
base = a & 0xF8 if a >= 0x80 else 0x20 + (a >> 3)
|
|
if a >= 0x80:
|
|
return f'{BITSFR.get(base, f"0x{base:02X}")}.{a & 7}'
|
|
return f'0x{base:02X}.{a & 7}'
|
|
|
|
|
|
# name, length, operand formatter
|
|
def _(n, l, f=None):
|
|
return (n, l, f)
|
|
|
|
|
|
def build():
|
|
t = {}
|
|
simple = {
|
|
0x00: 'NOP', 0x03: 'RR A', 0x04: 'INC A', 0x06: 'INC @R0', 0x07: 'INC @R1',
|
|
0x13: 'RRC A', 0x14: 'DEC A', 0x16: 'DEC @R0', 0x17: 'DEC @R1',
|
|
0x22: 'RET', 0x23: 'RL A', 0x26: 'ADD A,@R0', 0x27: 'ADD A,@R1',
|
|
0x32: 'RETI', 0x33: 'RLC A', 0x36: 'ADDC A,@R0', 0x37: 'ADDC A,@R1',
|
|
0x46: 'ORL A,@R0', 0x47: 'ORL A,@R1', 0x56: 'ANL A,@R0', 0x57: 'ANL A,@R1',
|
|
0x66: 'XRL A,@R0', 0x67: 'XRL A,@R1', 0x73: 'JMP @A+DPTR',
|
|
0x83: 'MOVC A,@A+PC', 0x84: 'DIV AB', 0x93: 'MOVC A,@A+DPTR',
|
|
0x96: 'SUBB A,@R0', 0x97: 'SUBB A,@R1', 0xA3: 'INC DPTR', 0xA4: 'MUL AB',
|
|
0xA5: 'DB 0A5h', 0xB3: 'CPL C', 0xC3: 'CLR C', 0xC4: 'SWAP A',
|
|
0xC6: 'XCH A,@R0', 0xC7: 'XCH A,@R1', 0xD3: 'SETB C', 0xD4: 'DA A',
|
|
0xD6: 'XCHD A,@R0', 0xD7: 'XCHD A,@R1', 0xE0: 'MOVX A,@DPTR',
|
|
0xE2: 'MOVX A,@R0', 0xE3: 'MOVX A,@R1', 0xE4: 'CLR A',
|
|
0xE6: 'MOV A,@R0', 0xE7: 'MOV A,@R1', 0xF0: 'MOVX @DPTR,A',
|
|
0xF2: 'MOVX @R0,A', 0xF3: 'MOVX @R1,A', 0xF4: 'CPL A',
|
|
0xF6: 'MOV @R0,A', 0xF7: 'MOV @R1,A',
|
|
}
|
|
for k, v in simple.items():
|
|
t[k] = (v, 1, None)
|
|
|
|
# register-form groups: base opcode -> template with {r}
|
|
for base, tmpl in ((0x08, 'INC R{r}'), (0x18, 'DEC R{r}'), (0x28, 'ADD A,R{r}'),
|
|
(0x38, 'ADDC A,R{r}'), (0x48, 'ORL A,R{r}'), (0x58, 'ANL A,R{r}'),
|
|
(0x68, 'XRL A,R{r}'), (0x98, 'SUBB A,R{r}'), (0xC8, 'XCH A,R{r}'),
|
|
(0xE8, 'MOV A,R{r}'), (0xF8, 'MOV R{r},A')):
|
|
for r in range(8):
|
|
t[base + r] = (tmpl.format(r=r), 1, None)
|
|
for r in range(8):
|
|
t[0x78 + r] = (f'MOV R{r},#{{i}}', 2, 'imm')
|
|
t[0x88 + r] = (f'MOV {{d}},R{r}', 2, 'dir')
|
|
t[0xA8 + r] = (f'MOV R{r},{{d}}', 2, 'dir')
|
|
t[0xB8 + r] = (f'CJNE R{r},#{{i}},{{t}}', 3, 'immrel')
|
|
t[0xD8 + r] = (f'DJNZ R{r},{{t}}', 2, 'rel')
|
|
|
|
two_dir = {0x05: 'INC {d}', 0x15: 'DEC {d}', 0x25: 'ADD A,{d}', 0x35: 'ADDC A,{d}',
|
|
0x42: 'ORL {d},A', 0x45: 'ORL A,{d}', 0x52: 'ANL {d},A', 0x55: 'ANL A,{d}',
|
|
0x62: 'XRL {d},A', 0x65: 'XRL A,{d}', 0x86: 'MOV {d},@R0',
|
|
0x87: 'MOV {d},@R1', 0x95: 'SUBB A,{d}', 0xA6: 'MOV @R0,{d}',
|
|
0xA7: 'MOV @R1,{d}', 0xC0: 'PUSH {d}', 0xC5: 'XCH A,{d}',
|
|
0xD0: 'POP {d}', 0xE5: 'MOV A,{d}', 0xF5: 'MOV {d},A'}
|
|
for k, v in two_dir.items():
|
|
t[k] = (v, 2, 'dir')
|
|
two_imm = {0x24: 'ADD A,#{i}', 0x34: 'ADDC A,#{i}', 0x44: 'ORL A,#{i}',
|
|
0x54: 'ANL A,#{i}', 0x64: 'XRL A,#{i}', 0x74: 'MOV A,#{i}',
|
|
0x76: 'MOV @R0,#{i}', 0x77: 'MOV @R1,#{i}', 0x94: 'SUBB A,#{i}'}
|
|
for k, v in two_imm.items():
|
|
t[k] = (v, 2, 'imm')
|
|
two_bit = {0x72: 'ORL C,{b}', 0x82: 'ANL C,{b}', 0x92: 'MOV {b},C',
|
|
0xA0: 'ORL C,/{b}', 0xA2: 'MOV C,{b}', 0xB0: 'ANL C,/{b}',
|
|
0xB2: 'CPL {b}', 0xC2: 'CLR {b}', 0xD2: 'SETB {b}'}
|
|
for k, v in two_bit.items():
|
|
t[k] = (v, 2, 'bitop')
|
|
two_rel = {0x40: 'JC {t}', 0x50: 'JNC {t}', 0x60: 'JZ {t}', 0x70: 'JNZ {t}',
|
|
0x80: 'SJMP {t}'}
|
|
for k, v in two_rel.items():
|
|
t[k] = (v, 2, 'rel')
|
|
t[0x10] = ('JBC {b},{t}', 3, 'bitrel')
|
|
t[0x20] = ('JB {b},{t}', 3, 'bitrel')
|
|
t[0x30] = ('JNB {b},{t}', 3, 'bitrel')
|
|
t[0x02] = ('LJMP {a}', 3, 'addr16')
|
|
t[0x12] = ('LCALL {a}', 3, 'addr16')
|
|
t[0x90] = ('MOV DPTR,#{a}', 3, 'addr16')
|
|
t[0x43] = ('ORL {d},#{i}', 3, 'dirimm')
|
|
t[0x53] = ('ANL {d},#{i}', 3, 'dirimm')
|
|
t[0x63] = ('XRL {d},#{i}', 3, 'dirimm')
|
|
t[0x75] = ('MOV {d},#{i}', 3, 'dirimm')
|
|
t[0x85] = ('MOV {d2},{d1}', 3, 'dirdir')
|
|
t[0xB4] = ('CJNE A,#{i},{t}', 3, 'immrel')
|
|
t[0xB5] = ('CJNE A,{d},{t}', 3, 'dirrel')
|
|
t[0xB6] = ('CJNE @R0,#{i},{t}', 3, 'immrel')
|
|
t[0xB7] = ('CJNE @R1,#{i},{t}', 3, 'immrel')
|
|
t[0xD5] = ('DJNZ {d},{t}', 3, 'dirrel')
|
|
for hi in range(8):
|
|
t[(hi << 5) | 0x01] = ('AJMP {a}', 2, 'addr11')
|
|
t[(hi << 5) | 0x11] = ('ACALL {a}', 2, 'addr11')
|
|
return t
|
|
|
|
|
|
TAB = build()
|
|
|
|
|
|
def disasm_one(mem, pc):
|
|
"""Return (text, length, target_or_None, is_call, ends_block)."""
|
|
op = mem[pc]
|
|
ent = TAB.get(op)
|
|
if ent is None:
|
|
return (f'DB 0{op:02X}h', 1, None, False, False)
|
|
name, ln, kind = ent
|
|
b = mem[pc:pc + ln]
|
|
if len(b) < ln:
|
|
return (f'DB 0{op:02X}h', 1, None, False, False)
|
|
nxt = pc + ln
|
|
tgt = None
|
|
if kind == 'imm':
|
|
s = name.format(i=f'0{b[1]:02X}h')
|
|
elif kind == 'dir':
|
|
s = name.format(d=d(b[1]))
|
|
elif kind == 'bitop':
|
|
s = name.format(b=bit(b[1]))
|
|
elif kind == 'rel':
|
|
tgt = (nxt + ((b[1] ^ 0x80) - 0x80)) & 0xFFFF
|
|
s = name.format(t=f'0x{tgt:04X}')
|
|
elif kind == 'bitrel':
|
|
tgt = (nxt + ((b[2] ^ 0x80) - 0x80)) & 0xFFFF
|
|
s = name.format(b=bit(b[1]), t=f'0x{tgt:04X}')
|
|
elif kind == 'immrel':
|
|
tgt = (nxt + ((b[2] ^ 0x80) - 0x80)) & 0xFFFF
|
|
s = name.format(i=f'0{b[1]:02X}h', t=f'0x{tgt:04X}')
|
|
elif kind == 'dirrel':
|
|
tgt = (nxt + ((b[2] ^ 0x80) - 0x80)) & 0xFFFF
|
|
s = name.format(d=d(b[1]), t=f'0x{tgt:04X}')
|
|
elif kind == 'dirimm':
|
|
s = name.format(d=d(b[1]), i=f'0{b[2]:02X}h')
|
|
elif kind == 'dirdir':
|
|
s = name.format(d1=d(b[1]), d2=d(b[2]))
|
|
elif kind == 'addr16':
|
|
a = (b[1] << 8) | b[2]
|
|
s = name.format(a=f'0x{a:04X}')
|
|
if op != 0x90:
|
|
tgt = a
|
|
elif kind == 'addr11':
|
|
a = (nxt & 0xF800) | ((op & 0xE0) << 3) | b[1]
|
|
tgt = a
|
|
s = name.format(a=f'0x{a:04X}')
|
|
else:
|
|
s = name
|
|
is_call = op in (0x12,) or (op & 0x1F) == 0x11
|
|
ends = op in (0x02, 0x22, 0x32, 0x80, 0x73) or (op & 0x1F) == 0x01
|
|
return (s, ln, tgt, is_call, ends)
|
|
|
|
|
|
class Image:
|
|
def __init__(self, path, base):
|
|
self.data = open(path, 'rb').read()
|
|
self.base = base
|
|
|
|
def __getitem__(self, k):
|
|
if isinstance(k, slice):
|
|
return self.data[k.start - self.base:k.stop - self.base]
|
|
return self.data[k - self.base]
|
|
|
|
def __contains__(self, a):
|
|
return self.base <= a < self.base + len(self.data)
|
|
|
|
|
|
def listing(img, start, count=40, labels=None):
|
|
out, pc = [], start
|
|
for _ in range(count):
|
|
if pc not in img:
|
|
break
|
|
s, ln, tgt, _c, ends = disasm_one(img, pc)
|
|
raw = bytes(img[pc:pc + ln]).hex(' ')
|
|
lab = f'{labels.get(pc,""):>12} ' if labels else ''
|
|
out.append(f'{lab}{pc:04X}: {raw:<9} {s}')
|
|
pc += ln
|
|
return '\n'.join(out)
|