Files
SDD 7d299451da reference: TMS34010 SDK investigation; prove GSPA round trip
The 1987 DOS toolchain (GSPA/GSPLNK/GSPROM) runs under DOSBox and
reassembles 32 KB of the v2.2 ROM byte-for-byte after five documented
fixups to smart34010 output.  The 1990 r5.01 tools are OS/2 NE and do
not run in DOS.  Adds the converter, round-trip script, and the
DOSBox/dosemu benchmark.
2026-10-18 00:39:32 +03:00

126 lines
5.3 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 ``dis34 -o`` (opcode words in a trailing
comment) because 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.
* dis34 prints a *long relative* JR (16-bit displacement) as ``ja<cc>``, the
same mnemonic it uses for the 3-word absolute JA. The opcode word count
disambiguates: 2 words = long JR, 3 words = absolute JA.
* dis34 prints B-file register 15 as ``sp`` too; gspa's bare ``sp`` is the
A-file register. B-file SP has to be written ``b15`` (the assembler rejects
``a15``).
* 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()
# long relative JR printed as JA (see module docstring)
mm = re.match(r"^(\s*)ja([a-z]+)(\s+)(\S.*)$", stripped)
if mm and len(words) == 2:
stripped = f"{mm.group(1)}jr{mm.group(2)}{mm.group(3)}{mm.group(4)}"
parts = stripped.split()
op = parts[0].lower() if parts else ""
# B-file SP (see docstring)
if op == "rl" and words and stripped.endswith(", sp"):
if int(words[0], 16) & 0x1f == 0x1f:
stripped = stripped[:-3] + "b15"
# 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))