Files
VRIO/PlasmaNew/hc11dis.py
T
CydandClaude Opus 4.8 2dca8bfd8f PlasmaNew: firmware dump + reverse-engineering analysis
Adds the dumped U3 EPROM image (tms27pc512.BIN) and the analysis it enabled.

Findings (FIRMWARE.md):
- Memory map: code+data at CPU $8000-$FFFF (EPROM upper 32KB, 1:1); lower
  half unused. Valid HC11 vectors; SCI rx ISR at $B85C fills a ring buffer,
  main-loop parser dispatches.
- Full command set recovered from the two jump tables: 58 ESC commands
  ($98AC) + control chars ($994C). Decoded semantics for @/G/H/K/L (confirm
  earlier guesses) plus NEW commands: Q=set-row, R=set-col, I=draw-page,
  i=display-page (10-page double-buffering), A-F=vector graphics, X/Y=pen.
- 8 fonts at $BC03 (6x8, 6x10, 7x10, 12x16, 12x20); glyph bitmaps in ROM
  (~$C000-$DFFF, exact base TBD).
- Built-in demo enabled by jumper 6 (confirms the JP1 map).

Tooling: hc11dis.py, a purpose-built 68HC11 disassembler (the toolchain has
no m68hc11 target), with the analysis reproducible from the addresses noted.

The firmware is now the authoritative command spec, superseding the need for
the Babcock programming manual. Feeds both vPLASMA and the replica firmware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 17:13:11 -05:00

149 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""Minimal-but-practical Motorola 68HC11 disassembler for the PD01D221 ROM.
CPU address == ROM offset for $8000-$FFFF (EPROM upper 32KB maps 1:1)."""
import sys
# addressing modes and their extra operand length (beyond opcode)
INH='inh'; IMM8='imm8'; IMM16='imm16'; DIR='dir'; EXT='ext'; IDX='idx'; IDY='idy'; REL='rel'
BITDIR='bitdir'; BITIDX='bitidx'; BITIDY='bitidy' # BSET/BCLR (mask)
BRDIR='brdir'; BRIDX='bridx'; BRIDY='bridy' # BRSET/BRCLR (mask+rel)
# page 0
P0 = {
0x00:('TEST',INH),0x01:('NOP',INH),0x02:('IDIV',INH),0x03:('FDIV',INH),
0x04:('LSRD',INH),0x05:('ASLD',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',BRDIR),0x13:('BRCLR',BRDIR),
0x14:('BSET',BITDIR),0x15:('BCLR',BITDIR),0x16:('TAB',INH),0x17:('TBA',INH),
0x19:('DAA',INH),0x1B:('ABA',INH),
0x1C:('BSET',BITIDX),0x1D:('BCLR',BITIDX),0x1E:('BRSET',BRIDX),0x1F:('BRCLR',BRIDX),
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:('ASLA',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:('ASLB',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:('ASL',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:('ASL',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 1 ($18): Y versions
P1 = {
0x08:('INY',INH),0x09:('DEY',INH),0x1C:('BSET',BITIDY),0x1D:('BCLR',BITIDY),
0x1E:('BRSET',BRIDY),0x1F:('BRCLR',BRIDY),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:('ASL',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),
0xFE:('LDY',EXT),0xFF:('STY',EXT),
}
# page 2 ($1A): CPD, and X/Y cross indexed
P2 = {0x83:('CPD',IMM16),0x93:('CPD',DIR),0xA3:('CPD',IDX),0xB3:('CPD',EXT),
0xAC:('CPY',IDX),0xEE:('LDY',IDX),0xEF:('STY',IDX)}
# page 4 ($CD): X/Y cross indexed
P4 = {0xA3:('CPD',IDY),0xAC:('CPX',IDY),0xEE:('LDX',IDY),0xEF:('STX',IDY)}
def disasm(rom, addr, count):
out=[]
for _ in range(count):
start=addr
op=rom[addr]; addr+=1
tab=P0; pfx=''
if op==0x18: pfx='18 '; op=rom[addr]; addr+=1; tab=P1
elif op==0x1A: pfx='1A '; op=rom[addr]; addr+=1; tab=P2
elif op==0xCD: pfx='CD '; op=rom[addr]; addr+=1; tab=P4
ent=tab.get(op)
if ent is None:
out.append((start,f".byte ${op:02X}",'?')); continue
mn,mode=ent; operand=''; tgt=None
if mode==INH: pass
elif mode==IMM8: operand=f"#${rom[addr]:02X}"; addr+=1
elif mode==IMM16: operand=f"#${(rom[addr]<<8)|rom[addr+1]:04X}"; addr+=2
elif mode==DIR: operand=f"${rom[addr]:02X}"; addr+=1
elif mode==EXT:
v=(rom[addr]<<8)|rom[addr+1]; addr+=2; operand=f"${v:04X}"; tgt=v
elif mode==IDX: operand=f"${rom[addr]:02X},X"; addr+=1
elif mode==IDY: operand=f"${rom[addr]:02X},Y"; addr+=1
elif mode==REL:
rel=rom[addr]; addr+=1; d=rel-256 if rel>127 else rel
tgt=(addr+d)&0xFFFF; operand=f"${tgt:04X}"
elif mode==BITDIR: operand=f"${rom[addr]:02X} #${rom[addr+1]:02X}"; addr+=2
elif mode==BITIDX: operand=f"${rom[addr]:02X},X #${rom[addr+1]:02X}"; addr+=2
elif mode==BITIDY: operand=f"${rom[addr]:02X},Y #${rom[addr+1]:02X}"; addr+=2
elif mode==BRDIR:
dd=rom[addr]; mk=rom[addr+1]; rel=rom[addr+2]; addr+=3
s=rel-256 if rel>127 else rel; tgt=(addr+s)&0xFFFF
operand=f"${dd:02X} #${mk:02X} ${tgt:04X}"
elif mode==BRIDX:
dd=rom[addr]; mk=rom[addr+1]; rel=rom[addr+2]; addr+=3
s=rel-256 if rel>127 else rel; tgt=(addr+s)&0xFFFF
operand=f"${dd:02X},X #${mk:02X} ${tgt:04X}"
elif mode==BRIDY:
dd=rom[addr]; mk=rom[addr+1]; rel=rom[addr+2]; addr+=3
s=rel-256 if rel>127 else rel; tgt=(addr+s)&0xFFFF
operand=f"${dd:02X},Y #${mk:02X} ${tgt:04X}"
raw=' '.join(f'{rom[b]:02X}' for b in range(start,addr))
out.append((start,f"{pfx}{mn} {operand}".strip(), raw, mn, tgt))
return out
if __name__=='__main__':
rom=open(__import__('os').path.join(__import__('os').path.dirname(__file__),'tms27pc512.BIN'),'rb').read()
start=int(sys.argv[1],16); count=int(sys.argv[2]) if len(sys.argv)>2 else 40
for row in disasm(rom,start,count):
a=row[0]; txt=row[1]; raw=row[2]
print(f"{a:04X}: {raw:<12} {txt}")