gigatron/rom/Utils/gt1dump.py
2025-01-28 19:17:01 +03:00

336 lines
12 KiB
Python

#!/usr/bin/env python3
#-----------------------------------------------------------------------
#
# gt1dump.py -- Tool to dump GT1 files to ASCII
#
# 2019-05-04 (marcelk) Initial version
# 2019-05-05 (marcelk) Added disassembly option -d
# 2019-05-19 (marcelk) Check for bad segment size; Small numbers in decimal
# 2019-10-20 (marcelk) Disassemble CALLI, CMPHU, CMPHS
# 2020-02-23 (marcelk) Disassemble v6502 instructions
#
#-----------------------------------------------------------------------
import argparse
import pathlib
import re
import sys
# One-for-all error handler (don't throw scary stack traces at the user)
sys.excepthook = lambda exType, exValue, exTrace: print('%s: %s: %s' % (__file__, exType.__name__, exValue), file=sys.stderr)
#-----------------------------------------------------------------------
# Command line arguments
#-----------------------------------------------------------------------
parser = argparse.ArgumentParser(description='Dump GT1 file')
parser.add_argument('-d', '--disassemble', dest='disassemble',
help='disassemble as vCPU or 6502 code',
action='store_true', default=False)
parser.add_argument('-f', '--force', dest='force',
help='accept any filename extension',
action='store_true', default=False)
parser.add_argument('-dv', '--disassemble-vcpu', dest='disassemble',
help='disassemble as vCPU code only',
action='store_const', const='vCPU')
parser.add_argument('-p', '--prof', dest='prof',
help='display profile information from file ARG',
action='store', metavar='PFILE')
parser.add_argument('filename', help='GT1 file', nargs='?')
args = parser.parse_args()
#-----------------------------------------------------------------------
# Functions
#-----------------------------------------------------------------------
opcodes = [
{ # [0] is vCPU
0x11: ('LDWI', 2), 0x1a: ('LD', 1), 0x1f: ('CMPHS', 1),
0x21: ('LDW', 1), 0x2b: ('STW', 1), 0x35: ('Bcc', 2),
0x59: ('LDI', 1), 0x5e: ('ST', 1), 0x63: ('POP', 0),
0x75: ('PUSH', 0), 0x7f: ('LUP', 1), 0x82: ('ANDI', 1),
0x85: ('CALLI', 2), 0x88: ('ORI', 1), 0x8c: ('XORI', 1),
0x90: ('BRA', 1), 0x93: ('INC', 1), 0x97: ('CMPHU', 1),
0x99: ('ADDW', 1), 0xad: ('PEEK', 0), 0xb4: ('SYS', 1),
0xb8: ('SUBW', 1), 0xcd: ('DEF', 1), 0xcf: ('CALL', 1),
0xdf: ('ALLOC', 1), 0xe3: ('ADDI', 1), 0xe6: ('SUBI', 1),
0xe9: ('LSLW', 0), 0xec: ('STLW', 1), 0xee: ('LDLW', 1),
0xf0: ('POKE', 1), 0xf3: ('DOKE', 1), 0xf6: ('DEEK', 0),
0xf8: ('ANDW', 1), 0xfa: ('ORW', 1), 0xfc: ('XORW', 1),
0xff: ('RET', 0),
},
{ # [1] is v6502
0: ('BRK',0), 1: ('ORAIX',1),
5: ('ORAZ',1), 6: ('ASLZ',1),
8: ('PHP',0), 9: ('ORAIM',1), 10: ('ASLA',0),
13: ('ORA',2), 14: ('ASL',2),
16: ('BPL',1), 17: ('ORAIY',1),
21: ('ORAZX',1), 22: ('ASLZX',1),
24: ('CLC',0), 25: ('ORAAY',2),
29: ('ORAAX',2), 30: ('ASLAX',2),
32: ('JSR',2), 33: ('ANDIX',1),
36: ('BITZ',1), 37: ('ANDZ',1), 38: ('ROLZ',1),
40: ('PLP',0), 41: ('ANDIM',1), 42: ('ROLA',0),
44: ('BIT',2), 45: ('AND',2), 46: ('ROL',2),
48: ('BMI',1), 49: ('ANDIY',1),
53: ('ANDZX',1), 54: ('ROLZX',1),
56: ('SEC',0), 57: ('ANDAY',2),
61: ('ANDAX',2), 62: ('ROLAX',2),
64: ('RTI',0), 65: ('EORIX',1),
69: ('EORZ',1), 70: ('LSRZ',1),
72: ('PHA',0), 73: ('EORIM',1), 74: ('LSRA',0),
76: ('JMP',2), 77: ('EOR',2), 78: ('LSR',2),
80: ('BVC',1), 81: ('EORIY',1),
85: ('EORZX',1), 86: ('LSRZX',1),
88: ('CLI',0), 89: ('EORAY',2),
93: ('EORAX',2), 94: ('LSRAX',2),
96: ('RTS',0), 97: ('ADCIX',1),
101: ('ADCZ',1), 102: ('RORZ',1),
104: ('PLA',0), 105: ('ADCIM',1), 106: ('RORA',0),
108: ('JMI',2), 109: ('ADC',2), 110: ('ROR',2),
112: ('BVS',1), 113: ('ADCIY',1),
117: ('ADCZX',1), 118: ('RORZX',1),
120: ('SEI',0), 121: ('ADCAY',2),
125: ('ADCAX',2), 126: ('RORAX',2),
129: ('STAIX',1),
132: ('STYZ',1), 133: ('STAZ',1), 134: ('STXZ',1),
136: ('DEY',0), 138: ('TXA',0),
140: ('STY',2), 141: ('STA',2), 142: ('STX',2),
144: ('BCC',1), 145: ('STAIY',1),
148: ('STYZX',1), 149: ('STAZX',1), 150: ('STXZY',1),
152: ('TYA',0), 153: ('STAAY',2), 154: ('TXS',0),
157: ('STAAX',2),
160: ('LDYIM',1), 161: ('LDAIX',1), 162: ('LDXIM', 1),
164: ('LDYZ',1), 165: ('LDAZ',1), 166: ('LDXZ',1),
168: ('TAY',0), 169: ('LDAIM',1), 170: ('TAX',0),
172: ('LDY',2), 173: ('LDA',2), 174: ('LDX',2),
176: ('BCS',1), 177: ('LDAIY',1),
180: ('LDYZX',1), 181: ('LDAZX',1), 182: ('LDXZY',1),
184: ('CLV',0), 185: ('LDAAY',2), 186: ('TSX',0),
188: ('LDYAX',2), 189: ('LDAAX',2), 190: ('LDXAY',2),
192: ('CPYIM',1), 193: ('CMPIX',1),
196: ('CPYZ',1), 197: ('CMPZ',1), 198: ('DECZ',1),
200: ('INY',0), 201: ('CMPIM',1), 202: ('DEX',0),
204: ('CPY',2), 205: ('CMP',2), 206: ('DEC',2),
208: ('BNE',1), 209: ('CMPIY',1),
213: ('CMPZX',1), 214: ('DECZX',1),
216: ('CLD',0), 217: ('CMPAY',2),
221: ('CMPAX',2), 222: ('DECAX',2),
224: ('CPXIM',1), 225: ('SBCIX',1),
228: ('CPXZ',1), 229: ('SBCZ',1), 230: ('INCZ',1),
232: ('INX',0), 233: ('SBCIM',1), 234: ('NOP',0),
236: ('CPX',2), 237: ('SBC',2), 238: ('INC',2),
240: ('BEQ',1), 241: ('SBCIY',1),
245: ('SBCZX',1), 246: ('INCZX',1),
248: ('SED',0), 249: ('SBCAY',2),
253: ('SBCAX',2), 254: ('INCAX',2)
}
]
bccCodes = {
0x3f: 'EQ', 0x4d: 'GT', 0x50: 'LT', 0x53: 'GE', 0x56: 'LE', 0x72: 'NE',
}
bccIns6502 = ['BNE', 'BEQ', 'BCC', 'BCS', 'BVC', 'BVS', 'BMI', 'BPL']
def zpMode(ins):
# vCPU instructions that work on zero page
if ins in ['LD', 'LDW', 'STW', 'ST',
'INC', 'ADDW', 'SUBW', 'CALL',
'POKE', 'DOKE', 'ANDW', 'ORW',
'XORW', 'CMPHS', 'CMPHU']:
return True
# v6502 instructions that work on zero page
if ins[3:] in ['Z', 'ZX', 'ZY', 'IX', 'IY']:
return True
# Negative for all others
return False
zeroPageSyms = {
0x00: 'zeroConst',
0x01: 'memSize',
0x06: 'entropy',
0x09: 'videoY',
0x0e: 'frameCount',
0x0f: 'serialRaw',
0x11: 'buttonState',
0x14: 'xoutMask',
0x16: 'vPC',
0x17: 'vPC+1',
0x18: 'vAC',
0x19: 'vAC+1',
0x1a: 'vLR',
0x1b: 'vLR+1',
0x1c: 'vSP',
0x21: 'romType',
0x22: 'sysFn',
0x23: 'sysFn+1',
0x24: 'sysArgs+0',
0x25: 'sysArgs+1',
0x26: 'sysArgs+2',
0x27: 'sysArgs+3',
0x28: 'sysArgs+4',
0x29: 'sysArgs+5',
0x2a: 'sysArgs+6',
0x2b: 'sysArgs+7',
0x2c: 'soundTimer',
0x2e: 'ledState_v2',
0x2f: 'ledTempo',
0x80: 'oneConst',
}
def readByte(fp):
byte = fp.read(1)
if len(byte) == 0:
raise Exception('Unexpected end of file')
return byte[0]
#-----------------------------------------------------------------------
# Main
#-----------------------------------------------------------------------
if args.filename:
fp = open(args.filename, 'rb')
if not args.force:
suffix = pathlib.Path(args.filename).suffix
if suffix not in ['.gt1', '.gt1x']:
raise Exception('Bad extension %s' % repr(suffix))
print('* file: %s' % args.filename)
print()
else:
fp = sys.stdin.buffer # Note: `buffer` for binary mode
prof = None
if args.prof:
with open(args.prof) as f:
gb = {}
exec(f.read(), gb)
prof = gb['prof']
hiAddress = readByte(fp)
cpuType, cpuTag = 0, '[vCPU]' # 0 for vCPU, 1 for v6502
while True:
# Dump next segment
loAddress = readByte(fp)
address = (hiAddress << 8) + loAddress
segmentSize = readByte(fp) # Segment size
if segmentSize == 0:
segmentSize = 256
if loAddress + segmentSize > 256:
raise Exception('Bad size %d for segment $%04x' % (segmentSize, address))
j, text = 0, ''
ins, ops = None, 0
insaddr = None
for i in range(segmentSize):
byte = readByte(fp) # Data byte
if args.disassemble and hiAddress > 0: # Attempt disassembly
if ops == 0: # Not in instruction yet
# Decide what instruction set we're in
ins = None
if byte in opcodes[cpuType].keys(): # First probe the last ISA
ins, ops = opcodes[cpuType][byte]
elif args.disassemble == True:
xCpu = 1 - cpuType # 0 <-> 1
if byte in opcodes[xCpu].keys(): # Probe the other if current not valid
cpuType = xCpu # Swap if second is valid
cpuTag = '[v6502]' if xCpu else '[vCPU]'
ins, ops = opcodes[cpuType][byte]
if ins:
if j > 0: # Force new line
print('%s|%s|' % ((51-j)*' ', text))
j, text = 0, ''
insaddr = address+i
asm = '%-5s' % ins # Format mnemonic
else: # Already in instruction
if len(asm) > 5 and ops == 1:
asm = asm[:-2] + ('%02x' % byte) + asm[-2:]
elif zpMode(ins) and ops == 1 and byte in zeroPageSyms:
if byte == 0x00 and ins[-1] == 'W':
asm += ' ' + zeroPageSyms[1] + '-1' # Special case top of memory
else:
asm += ' ' + zeroPageSyms[byte] # Known address
elif ins == 'Bcc' and ops == 2 and byte in bccCodes:
asm = 'B%-4s' % bccCodes[byte]
elif ins in ['BRA', 'DEF'] or (ins == 'Bcc' and ops == 1):# Branch vCPU
asm += ' $%02x%02x' % (hiAddress, (byte+2)&255)
elif ins in bccIns6502: # Relative branch v6502
to = address + i + 1 + (byte ^ 128) - 128
asm += ' $%04x' % (to & 0xffff)
elif ins == 'SYS': # Operand to SYS
if byte != 128:
maxCycles = 28-2*((byte^128)-128)
asm += ' %d' % maxCycles # maxCycles in decimal
else:
asm = 'HALT' # Never executes
else:
asm += ' $%02x' % byte # Default as hex byte
ops -= 1
if j == 0: # Address on each new line
print('%04x ' % (address + i), end='')
print(' %02x' % byte, end='') # Print byte as hex value
j += 3
if j == 8*3: # Visual divider
print(' ', end='')
j += 1
text += chr(byte & 127) if 32<=(byte & 127)<127 else '.' # ASCII
if ops == 0:
if ins:
# Print as disassembled instruction
# Convert single digit operand to decimal
asm = re.sub(r'\$0([0-9])$', r'\1', asm)
prefix, cpuTag = (25 * ' ') + cpuTag, ''
if prof:
cycs='#%d' % prof[insaddr] if insaddr in prof else ''
print('%s %-25s %-13s|%s|' % (prefix[-25+j:], asm, cycs, text))
else:
print('%s %-25s|%s|' % (prefix[-25+j:], asm, text))
ins, j, text = None, 0, ''
elif (address + i) & 15 == 15 or i == segmentSize - 1:
# Print as pure data
print('%s|%s|' % ((51-j)*' ', text))
j, text = 0, ''
if ops > 0: # incomplete instruction
print('%s%-25s|%s|' % ((26 - j) * ' ', asm + ' ??', text))
print('* incomplete instruction')
print('* %d byte%s' % (segmentSize, '' if segmentSize == 1 else 's'))
print()
hiAddress = readByte(fp)
if hiAddress == 0:
break
# Dump start address
hiAddress, loAddress = readByte(fp), readByte(fp)
address = (hiAddress << 8) + loAddress
if address:
print('* start at $%04x' % address)
print()
if len(fp.read()) > 0:
raise Exception('Excess bytes')
if args.filename:
fp.close()
#-----------------------------------------------------------------------
#
#-----------------------------------------------------------------------