#!/usr/bin/env python3 """Reconstruct the VT1200 X-server image from the raw EPROM dumps. The VT1200 firmware is held in 14 x 128 KiB (27C010) byte-wide EPROMs per firmware revision. The EPROMs are wired as 16-bit words: consecutive part numbers form a pair, and within a pair the ODD-numbered part holds the low byte and the EVEN-numbered part holds the high byte of the word. Example for v2.1: pair (23-210E9, 23-211E9) -> 256 KiB bank, 210E9 = high byte, 211E9 = low byte There are two possible orderings of the seven 256 KiB banks: * ``linear`` (default) -- CPU *address* order. The ROM is top-aligned at physical byte 0x07E40000, and the bank with the *highest* part numbers sits at the lowest address. In this order file offset == physical byte - base, which is what a disassembler/emulator wants. * ``--part-order`` -- legacy ascending part-number order (bank 0 first). The image is 1.75 MiB (0x1C0000) and can be fed to a TMS34010 disassembler (e.g. smart34010's ``dis34`` or MAME's ``unidasm``) or loaded into an emulator at base 0x07E40000. See MEMORY_MAP.md. Note the firmware stores *bit* addresses (physical byte = bit >> 3). Usage: ./reconstruct.py v2.2 # -> vt1200_v22.bin (address order) ./reconstruct.py v2.1 out.bin ./reconstruct.py v2.1 --part-order """ import re import os import glob import argparse HERE = os.path.dirname(os.path.abspath(__file__)) # Physical byte address the ROM is mapped at; also the size of one image. ROM_BASE = 0x07E40000 def load(dirname): roms = {} for path in sorted(glob.glob(os.path.join(HERE, dirname, "*.bin"))): m = re.search(r"(\d+)E9", os.path.basename(path)) if not m: continue with open(path, "rb") as fh: roms[int(m.group(1))] = fh.read() return roms def reconstruct(dirname, linear=True): """De-interleave the EPROMs of a revision. Returns (parts, image) where parts is the ascending list of part numbers and image is the reconstructed ROM. With linear=True the banks are placed in CPU address order (file offset == physical byte - ROM_BASE). """ roms = load(dirname) parts = sorted(roms) if len(parts) % 2: raise SystemExit(f"{dirname}: expected an even number of EPROMs, got {len(parts)}") banks = [] for i in range(0, len(parts), 2): even_pn, odd_pn = parts[i], parts[i + 1] # odd part = low byte low, high = roms[odd_pn], roms[even_pn] if len(low) != len(high): raise SystemExit(f"{dirname}: size mismatch between {odd_pn} and {even_pn}") img = bytearray(len(low) * 2) img[0::2] = low img[1::2] = high banks.append(bytes(img)) if linear: banks.reverse() # highest part number sits at the lowest address return parts, b"".join(banks) def main(argv=None): ap = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) ap.add_argument("revision", help="ROM directory, e.g. v2.1 or v2.2") ap.add_argument("output", nargs="?", help="output file (default vt1200_vNN.bin)") ap.add_argument( "--linear", dest="linear", action="store_true", help="emit CPU address order (default): offset == physical byte - 0x07E40000", ) ap.add_argument( "--part-order", dest="linear", action="store_false", help="emit legacy ascending part-number order", ) ap.set_defaults(linear=True) args = ap.parse_args(argv) default = "vt1200_" + args.revision.replace(".", "") + ".bin" out = args.output or os.path.join(HERE, default) parts, img = reconstruct(args.revision, linear=args.linear) with open(out, "wb") as fh: fh.write(img) order = "address" if args.linear else "part-number" strings = len(re.findall(rb"[\x20-\x7e]{16,}", img)) print(f"{args.revision}: {len(parts)} EPROMs ({parts[0]}..{parts[-1]}) " f"{order} order -> {out} ({len(img)} bytes = 0x{len(img):X}), " f"{strings} text runs") if args.linear: print(f" load base: 0x{ROM_BASE:08X} (bit 0x{ROM_BASE * 8:08X})") if __name__ == "__main__": main()