Files
nils 514983c158 Add tooling + EFM8UB20 QFP48 pinout doc
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.
2026-08-17 23:35:09 +02:00

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)