Files
SDD af7d3565a5 dis34: print long relative JR as jr and surface B-file SP as b15
The 16-bit-displacement jump (low byte 0x00, two words) is JRcc, not the
three-word JAcc (low byte 0x80).  Printing it as ja made linear
disassembly report the wrong control flow and made every such branch grow
by a word when reassembled.

Register 15 is the stack pointer in either file: 0x0F is A15 and 0x1F is
B15.  The disassembler printed both as sp, hiding B-file stack-pointer
code.  Print b15 when the R bit selects the B file.

Validated with the 1987 TI GSPA/GSPLNK/GSPROM round trip: the 32 KB region
at bit 3F208000 still reassembles byte-for-byte (md5
43adb6669571041985f3e26fd9ff7cb9), as do the 4 KB and 16 KB spans and the
0x8000/3F240000 region.  The converter no longer needs its ja->jr or
sp->b15 rewrites (5 fixups -> 3); a whole-ROM diff contains only those two
output categories.  The ROM is unchanged
(b46a3e442ccb86134d2356bd70d22fd2).
2026-10-18 00:54:34 +03:00

114 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Convert smart34010 (dis34 -o) output into TI gspa/as34 2.00 syntax.
Usage: dis2as34.py BASE_HEX SIZE_BYTES INPUT.ASM > OUT.ASM
The input must be produced with the fixed ``dis34 -o`` (opcode words in a
trailing comment), which prints both relative-jump forms as ``jr<cc>`` and
the B-file stack pointer as ``b15``. The 1987 assembler is *not*
byte-faithful on its own:
* ``MOVI`` is one mnemonic with a short (16-bit, sign-extended) and a long
(32-bit) form. gspa auto-selects by "does the value fit in 16 bits", so a
long MOVI of 0x53E0 (which the ROM encodes as a 3-word instruction) is
silently shortened -- and sign extension then changes its meaning. The
explicit ``W`` / ``L`` operand forces the form.
* the same applies to ADDI/CMPI/SUBI.
* register-indirect displacements are signed 16-bit fields. dis34 prints the
raw two's-complement bits (0xFFE0); gspa rejects those as out of bounds, so
they are rendered as signed decimal (-32).
* constants: dis34 emits ``0xHHHH``; gspa wants ``0HHHHh`` (must start with a
decimal digit, so 0xC00000B0 becomes 0C00000B0h).
* a PC-relative branch whose target is not an instruction boundary in the
region (outside it, or -- as happens in data tables -- inside a multi-word
instruction) cannot be expressed as a label. gspa also mishandles a
negative absolute ``.set`` target in a JR. The exact opcode words are
therefore emitted as ``.word`` for those branches.
"""
import re
import sys
DISP = re.compile(r"(\*\s*[ab]\d+|\*\s*sp)\(\s*0x([0-9a-fA-F]+)\s*\)")
HEX = re.compile(r"0x([0-9a-fA-F]+)")
FORM_OPS = {"movi", "addi", "cmpi", "subi"}
RELATIVE = re.compile(r"^(jreq|jrne|jrlt|jrle|jrgt|jrge|jrc|jrnc|jrlo|jrls|jrhi|jrhs|jrnz|jrz|"
r"jruc|jrxn|jrxnn|jrxz|jrxeq|jrxgt|jrxle|jryn|jrynn|jryz|jryeq|jrygt|jryle|"
r"call|callr)$")
DSJ_OPS = {"dsj", "dsjs", "dsjeq", "dsjne"}
def ti_hex(digits: str) -> str:
return "0" + digits.upper() + "h"
def fix_disp(m):
base, digits = m.group(1), m.group(2)
v = int(digits, 16)
if v >= 0x8000:
return f"{base}({v - 0x10000})"
return f"{base}(0{digits.upper()}h)"
def raw_words(words):
return " .word " + ", ".join(ti_hex(w) for w in words)
def convert(src: str, base: int, size: int) -> str:
end = base + size * 8
lines = src.splitlines()
defined = set()
for line in lines:
code = line.partition(";")[0]
dm = re.match(r"^(L[0-9A-Fa-f]{8}):", code)
if dm:
defined.add(dm.group(1).upper())
out = []
referenced = set()
for line in lines:
code, sep, comment = line.partition(";")
for lm in re.findall(r"\b(L[0-9A-Fa-f]{8})\b", code):
referenced.add(lm.upper())
words = re.findall(r"\b[0-9a-fA-F]{4}\b", comment)
stripped = code.rstrip()
parts = stripped.split()
op = parts[0].lower() if parts else ""
# short/long immediate forms (see docstring)
if op in FORM_OPS and words:
if len(words) == 2:
stripped += ", W"
mm = re.match(r"^(\s*\w+\s+)0x([0-9a-fA-F]+)(,.*)$", stripped)
if mm and int(mm.group(2), 16) >= 0x8000:
stripped = (mm.group(1) + str(int(mm.group(2), 16) - 0x10000)
+ mm.group(3))
elif len(words) == 3:
stripped += ", L"
# relative branch to a label that is not an instruction boundary
mm = re.match(r"^(\s*)(\w+)(\s+.*?)(L[0-9A-Fa-f]{8})\s*$", stripped)
if mm and mm.group(4).upper() not in defined:
op2 = mm.group(2).lower()
if op2 in DSJ_OPS or RELATIVE.match(op2):
stripped = raw_words(words)
# relative branch to an absolute address outside the region
mm = re.match(r"^(\s*)([a-z]+)(\s+)0x([0-9a-fA-F]+)\s*$", stripped)
if mm and RELATIVE.match(mm.group(2).lower()):
target = int(mm.group(4), 16)
if not (base <= target < end):
stripped = raw_words(words)
stripped = DISP.sub(fix_disp, stripped)
stripped = HEX.sub(lambda mm: ti_hex(mm.group(1)), stripped)
out.append(stripped)
missing = sorted(referenced - defined)
if missing:
out.append("")
for name in missing:
off = int(name[1:], 16) - base
out.append(f"{name} .set " + ("0%Xh" % off if off >= 0 else str(off)))
return "\n".join(out) + "\n"
if __name__ == "__main__":
base = int(sys.argv[1], 16)
size = int(sys.argv[2])
sys.stdout.write(convert(open(sys.argv[3]).read(), base, size))