# The TMS34010 SDK in `tools/tms34010-sdk`: what runs, and what it buys us Investigated 2026-10-18. Short version: * The **1987 DOS toolchain runs** under DOSBox (and dosemu) on Linux: GSPA (assembler), GSPLNK (linker), GSPROM (hex converter), GSPAR (archiver), and the K&R C compiler (GSPCPP/GSPCC/GSPCG). * `smart34010` output **does** assemble with it, but not literally: five small fixups are needed. With them, a **32 KB region of the v2.2 ROM reassembles byte-for-byte**, md5-identical to the original. See the results table. * The **1990 r5.01 tools are OS/2 1.x NE binaries**, not DOS. They do not run in DOSBox; using them would need an OS/2 environment. The 1987 DOS tools are the usable set. * There is **no `as34` / `lnk34`** in this SDK. The assembler is `GSPA.EXE` and the linker `GSPLNK.EXE` (the names `smart34010`'s README mentions are from a later TI package). GSPA 2.00 is enough for the round trip. * The C compiler works and links with `rts.lib` into an absolute COFF image. It is **K&R C, not ANSI**: `int f(int a)` is a syntax error, `int f(a) int a;` works. * For a VT1200 payload the useful part is the **address model**: the linker's `SECTIONS` origin is a **bit address**, the COFF carries the full 32-bit bit address, and the bytes are extracted from GSPROM's high/low EPROM files. The old converter's own hex addresses wrap at 64 KB and must not be used for placement. Everything below was run against the ROM with md5 `b46a3e442ccb86134d2356bd70d22fd2`; the ROM was only read. --- ## 1. What is in the SDK, and what actually executes | package | files | host | runs here? | |---|---|---|---| | `1985-05-20 … Assembly Language Package` | GSPA/GSPLNK/GSPAR **v1.04** | DOS MZ | yes, but older | | `1987-11-06 … Simulator-C Tools-ASM Tools.img` | GSPA **2.00**, GSPLNK **2.00**, GSPAR 2.00, GSPROM **2.01**, GSPCV 2.00 (A2); GSPCPP/GSPCC/GSPCG **2.01**, RTS.LIB, FLIB.LIB (C2) | DOS MZ | **yes — the set used here** | | `1990-11-19 … Code Generation Tools r5.01` | gspa, gspac, gspar, gspcg, gspcl, gsplnk, gspopt, gsprom, clist | **OS/2 1.x NE** | **no** (DOSBox cannot run NE/OS2) | | `1990-11-19 … C Source Debugger r5.00` | db340.exe (DOS MZ), gspmon | DOS + target monitor | debugger runs, but needs a monitor/target link | | `1991-07 TIGA Promo Kit` | DOS art/demo disks, TIGA docs | DOS | not relevant to VT1200 | | `Docs/` | Users Guide, Assembly Tools Guide, C Compiler Guide, reference cards, TIGA, production data | PDF | reference only | | `Patents/` | five graphics-architecture patents | PDF | not relevant | The 1990 NE files were checked both with `file` and by parsing the NE header: signature `NE`, version 5.1, **target OS byte `0x01` = OS/2**. The 1985/1987 files are plain `MZ for MS-DOS`, 16-bit real mode, and need at most 640 KB. The **1987 disk is the sweet spot**: v2.00 is the version whose tools guide documents the `W`/`L` immediate-form operands and the `.usect`/`.if` features used below. The 1985 v1.04 tools are older and were not needed. ### Running them headless DOSBox 0.74-3 works with the SDL dummy drivers; a bare `-conf` file with `core=dynamic` / `cycles=max` makes it ~5× faster than the `cycles=3000` config used earlier. The commands in `roundtrip.sh` show the exact form. dosemu 1.4.0.8 also works: `dosemu -t -quiet -E 'c:\bench\bench.bat'`, where the batch ends in `exitemu`. `-E` alone does not exit DOS. See the benchmark section. --- ## 2. The round-trip experiment (the proof) ### Method For a region at file offset `OFF`, size `SIZE`, linked at bit address `BASE`: 1. `dis34 -b BASE -o region.bin > region.dis` — disassemble with the opcode words in a trailing comment (`-o` is required; the fixups use them). 2. `dis2as34.py BASE SIZE region.dis > RT.ASM` — apply the five fixups (section 3), prepend `.text`, append `.end`. 3. Assemble and link: ``` gspa rt rt.obj rt.lst -l gsplnk rt.cmd gsprom -i rt.out rthi rtlo ``` with `rt.cmd`: ``` -a -o rt.out -m rt.map rt.obj SECTIONS { .text 3F208000h: {} } ``` The origin is a **bit address**. `-a` makes an absolute module; `-m` writes a map; `-e` would set an entry point. 4. `gsprom -i` writes **two** Intel-hex files (high byte and low byte for a 16-bit EPROM pair). Interleave **low byte then high byte** per word. 5. Compare with `dd`-extracted original bytes. `roundtrip.sh` does exactly this and prints the md5. ### Results | region | file offset | base (bit) | size | result | |---|---|---|---|---| | boot/POST code | `0x1000` | `3F208000` | 4 KB | **0 differing**, md5 `a4018fed12e062658bc81957f149265b` | | same, larger | `0x1000` | `3F208000` | 16 KB | **0 differing**, md5 `0a2e679bc864d52d32c59740142c5b96` | | same, larger | `0x1000` | `3F208000` | **32 KB** | **0 differing**, md5 `43adb6669571041985f3e26fd9ff7cb9` | | another code region | `0x8000` | `3F240000` | 4 KB | **0 differing**, md5 `438e743431d56e0c12ededce1c36952a` | The ROM md5 after all runs is unchanged: `b46a3e442ccb86134d2356bd70d22fd2`. A 64 KB region trips `dis34` itself (`unexpected end of input at byte 65536`); split larger spans into ≤32 KB pieces. This is a disassembler limitation, not an assembler one. ### What this proves, and what it does not It proves the assembler accepts the disassembler's instruction syntax and reproduces the **exact instruction encodings** for 32 KB of real firmware, including PC-relative branches, absolute calls, data words, and immediate forms. It does **not** prove the payload runs on a VT1200; nothing here was executed on the target. --- ## 3. The five fixups (and why they are needed) `dis34` output is *close* to GSPA syntax but not directly assemblable. The converter `harness/sdk-scratch/dis2as34.py` is the executable form of this list. 1. **Hex constants.** GSPA has no `0x`: a hex constant must start with a decimal digit and end in `h`. `0x220` → `0220h`; `0xC00000B0` → `0C00000B0h` (nine digits are accepted and truncated to 32 bits). GSPCV does the same conversion for the old `>xxxx` syntax. 2. **`MOVI` short vs long.** GSPA has one `MOVI` mnemonic with a 16-bit sign-extended form and a 32-bit form, and auto-selects by "does the value fit in 16 bits (unsigned)". That is **not** encoding-preserving: a long `MOVI 0x53E0` (three words) becomes a short `MOVIW` (two words), and the short form sign-extends to `0xFFFF53E0`. Force the form with the third operand: `movi 053E0h, sp, L` or `..., W`. The opcode word count in the `-o` comment tells which form the ROM used. The same applies to `ADDI`, `CMPI`, and `SUBI`. 3. **B-file `SP`.** `dis34` prints register 15 as `sp` regardless of file. Bit 4 of the encoded register field says which file: `0x0F` = A-file SP, `0x1F` = B-file SP. GSPA's bare `sp` matched the ROM for most forms (memory operands such as `*sp(offset)` assembled as B15; `MOVI`/`MMFM` register operands as A15), but an `RL` destination chose A15 where the ROM has B15 (0x312F vs 0x313F). `b15` forces the B-file register; `a15` is rejected. Check the register field, not the printed name. 4. **Long relative `JR` printed as `JA`.** In `dis34`, the 2-word relative jump (opcode low byte `0x00`, 16-bit displacement) is printed with the `ja` mnemonic, the same one used for the 3-word absolute `JA`. The word count distinguishes them: 2 words → rewrite to `jr`, 3 words → leave `ja`. Without this, every long relative branch grows by a word and the image diverges. 5. **Branches whose target is not an instruction boundary.** This happens for targets outside the disassembled region (common) and, in data-table areas, for targets inside a multi-word instruction. GSPA also mishandles a negative absolute `.set` value in a `JR` (it emits the absolute `JA` form and errors). Emit the exact opcode words from the `-o` comment as `.word` for those branches. For `DSJS`, whose short range is only ±32 words, this is the only reliable way. One practical note: with the slow `cycles=3000` DOSBox config, a 16 KB source with `-l` takes about 35 s wall time; a 32 KB source takes about 1.5–2 minutes. That is DOSBox being cycle-limited, not the assembler hanging. The benchmark section uses `core=dynamic`/`cycles=max`, which removes most of it. --- ## 4. Addresses, sections, and output formats ### The unit is bits The TMS34010 is bit-addressed. In this toolchain: * The assembler's location counter and labels are in **bits**; each instruction word advances it by 16. A listing address of `00000030` is 3 words in. * `SECTIONS` binding is a **bit address**: `.text 3F208000h: {}` produced `.text 0 3F208000 00020000` in the map (origin bit, length bits). * The linker's `MEMORY` origins/lengths are bits too. * The COFF output carries the true 32-bit bit address. In `RT16.OUT` the optional header says `text = 0x3F208000`, `code size = 0x20000` bits, and the `.text` section header says `paddr = vaddr = 0x3F208000`, `size = 0x20000` bits. The file header magic is `0x0090`, flags `0x103` (including `0x40`, the TMS34010 little-endian "16 bits per word, LSB first" flag). * To place a payload at bit address `A`, link `.text A: {}` and put file byte `i` at physical byte address `A>>3 + i`. Do not re-link at a different address than the load address: the C compiler emits `CALLA` and absolute pointers. ### GSPROM's hex addresses are not the load address `gsprom -i in out_hi out_lo` produces two 8-bit Intel-hex files, one high byte and one low byte per 16-bit word. The address in those records is a **16-bit word index inside a 64 KB byte window**, not the bit address and not the full byte address. Measured: | section bit origin | first record address | |---|---| | `0x00020000` | `0x2000` | | `0x3F208000` | `0x0800` | | `0x3F240000` | `0x4000` | That matches `((bit >> 3) & 0xFFFF) >> 1`. The data order is still correct, so take the byte stream in order and place it at the address from the map/COFF. `gsprom -t` (TI-tagged) has the same 16-bit address limitation. For a loader, the clean source of truth is the **COFF** (`-a` output): section `paddr`/`vaddr` and the optional header's entry point are full 32-bit bit addresses. A small COFF reader can emit a raw image with its true load address; GSPROM is only a convenience for an EPROM programmer. ### Linker command syntax gotchas * Comments are `/* … */`; `;` is a syntax error in a command file. * `.text > prog` is a syntax error. Use either `.text 3F208000h: {}` (bind directly) or `.text: {} > prog` (named memory). * `-a` = absolute executable, `-m` = map, `-o` = output, `-e` = entry symbol, `-l lib` = library. `-c` / `-cr` select the C ROM/RAM autoinit models. --- ## 5. The C compiler It runs, and the whole pipeline works: ``` gspcpp cmain # .c -> .cpp gspcc cmain # .cpp -> .if gspcg cmain # .if -> .asm gspa cmain # .asm -> .obj gsplnk cmain.cmd # .obj + rts.lib -> .out, map gsprom -i cmain.out … ``` A test with `fib` and `main` linked cleanly with: ``` -c -o cmain.out -m cmain.map cmain.obj -l rts.lib MEMORY { prog: origin = 3F208000h, length = 10000h ram: origin = 0h, length = 100000h } SECTIONS { .text: {} > prog .cinit: {} > prog .data: {} > prog .bss: {} > ram } ``` The map confirms: ``` ENTRY POINT SYMBOL: "_c_int00" address: 3F2082F0 .text 3F208000 00000A90 .cinit 3F208A90 00000060 .bss 00000000 0000FE40 (uninitialized) ``` So the compiler, assembler, runtime library, linker, and converter form a complete C path. **It was not executed on a VT1200.** ### Language and ABI facts worth knowing From `SPVU005A` and the generated code: * **K&R C only.** ANSI prototypes (`int f(int a)`) are a syntax error; `int f(a) int a; { … }` works. Plan for old-style declarations. * `char` 8 bits, `short` 16, **`int` and `long` 32**, pointers **32-bit bit addresses**, `float` 32, `double` 64. * Alignment: objects are on 16-bit boundaries (bit addresses with four LSBs zero); structure/array members are packed. * Dedicated registers: `SP` = system stack, `A14` (`STK`) = program stack, `A13` (`FP`) = frame pointer; `A8` = integer/pointer return value. `B0–B14` are unused by the compiler and available to assembly. Callee saves the A registers it uses except `A8`; `FS1` must be 32 inside C code. * Arguments are pushed on the **program stack** in reverse order, widened to 32 bits; calls are `CALLA` (absolute), so **code is position-dependent**. * Runtime: link `rts.lib` with `-c` (ROM autoinit; `boot.obj` is pulled in and `_c_int00` becomes the entry) or `-cr` (loader does the `.cinit` copy). The boot code allocates `SYS_STACK` in `.bss`, sets `FP`/`STK`/`SP`, calls `_main`, then `_exit`. * C sections: `.text` (code + string literals + float constants), `.cinit` (initialized-global table), `.bss` (globals + both stacks + heap). `.data` is the assembler's, not the C compiler's. `.bss` must be RAM. For a payload, the simplest C model is: link `.text`/`.cinit` at the load address, put `.bss` in a free RAM range, and enter at `_c_int00`. A pure-assembly payload avoids the runtime entirely. --- ## 6. Building a VT1200 payload: practical recipe Assembly-only payload (smallest, no runtime): ``` .text start: movi 01234h, a0 move a0, @0C0000010h, 0 ; example on-chip register rets .end ``` Link command: ``` -a -e start -o payload.out -m payload.map payload.obj SECTIONS { .text 02000000h: {} /* example: bit 0x02000000 = byte 0x400000, in RAM */ } ``` Then either read `payload.out` as COFF (true bit address in the section header) or `gsprom -i payload.out hi lo`, interleave low/high, and place the bytes at `0x02000000 >> 3 = 0x400000`. The RAM region is `0x0000000`–`0x1FFFFFF` (4 MiB) and the ROM is `0x3F200000`–`0x3FFFFFFF` in the MAME model; choose a free RAM range for a payload, or load into RAM and jump. The SDK supplies the image and its entry point, **not** the network delivery mechanism. Delivery is a separate problem: the COFF gives a loader the exact bit address and entry point; GSPROM's hex is only for burning EPROMs. For C, use the same idea with `.text`/`.cinit` at the payload address and `.bss` in RAM; `-c` will include `boot.obj` and make `_c_int00` the entry. --- ## 7. Documentation: what the SDK adds over the repo The repo already has `docs/TMS34010-users-guide.pdf`/`.txt` (the 1988 hardware user's guide). The SDK's unique, useful additions are: * **`SPVU004A TMS34010 Assembly Language Tools Users Guide (1987).pdf`** — the key document: assembler directives and expression rules, the COFF appendix (file/section/relocation/symbol formats), linker `MEMORY`/`SECTIONS`, `gsprom`, and the simulator. This is where the bit-address and `W`/`L` behaviour is specified. * **`SPVU004A … Assembly Language Tools Reference Card (1987).pdf`** — compact invocation/option/directive summary; the fastest lookup. * **`SPVU005A TMS34010 C Compiler Reference Guide (1988-08).pdf`** — data types, ABI, runtime environment, library reference. The ABI section is what makes C/assembly interworking possible. * **`SPVU005A … C Compiler Reference Card (1988-08).pdf`** — quick ABI/options. * `SPVU005 TMS34010 C Compiler Users Guide (1986-12).pdf` — older; the 1988 guide supersedes it. * `SPVU002A Software Development Board Users Guide` + pocket reference — the SDB and its monitor; useful only if you have that board. * `SPVU021A C Source Debugger Users Guide (1991-09)` — the `db340`/`gspmon` monitor workflow; not directly applicable to the VT1200 unless a compatible monitor can be loaded. * `SPVU006 Math Graphics Function Library` and the 1987 `Graphics Math Function Library` disk (`GRAFIX.LIB`, `TEXT.LIB`, `MATH.LIB` + sources) — optional drawing primitives; not evaluated against the VT1200's mono framebuffer. * `SPVS002A/C` production data — hardware electrical/timing detail; the repo's user's guide covers the programming model. * `SPVU015A/C` TIGA, `SPVU018A` TIGA art, `SPVU027` graphics library — TIGA is a PC graphics-interface standard, not the VT1200 terminal; not relevant. * `Docs/Video Interfaces/*` — TLC3407x RAMDAC/palette datasheets; the VT1200 is monochrome, so not relevant. * `Patents/` — graphics-architecture patents; no code or format value. --- ## 8. Findings about `smart34010` itself These are worth fixing upstream (they cost real time here): 1. **Long relative `JR` is printed as `JA`.** In `dis34.c`, the `0x00` low-byte case (16-bit displacement, 2-word instruction) calls `printf("ja%s …")`; only the 8-bit case prints `jr%s`. This makes the output non-assemblable without the word-count fix. Example: `jagt L3F211EE0 ; c700 0167` is a relative jump, but `jagt` is the 3-word absolute instruction. 2. **B-file `SP` is printed as `sp`.** The register name hides bit 4; `sp` (A-file) and `b15` (B-file) are different encodings. 3. **`movi` hides the short/long choice.** The disassembler prints one mnemonic for both encodings, while the assembler auto-selects (and may change the meaning via sign extension). The `-o` word count is the only clue. 4. **64 KB input fails** with `unexpected end of input at byte 65536`; use ≤32 KB regions or fix the offset arithmetic. 5. Data tables decoded linearly can produce branches into the middle of instructions; `-a` (analyze) may help, but for byte-exact reassembly the `.word` fallback in the converter is what matters. The good news: the instruction syntax itself (mnemonics, operands, `@`/`*` addressing, labels, `.word` data) matches GSPA 2.00 closely; the problems are these specific ambiguities, not a different dialect. --- ## 9. DOSBox vs dosemu: benchmark Separate from the toolchain result. The owner asked whether dosemu is actually faster. Workload is the real assembler, **GSPA 2.00** (there is no `as34` in this SDK), on the round-trip sources. * **A:** `gspa t1 t1.obj t1.lst -l` — 4 KB region, 1,478 lines, with listing. * **B:** `gspa -q rt16 rt16.obj` — 16 KB region, 6,124 lines, no listing. * DOSBox 0.74-3, `core=dynamic`, `cycles=max`, `SDL_VIDEODRIVER=dummy`, `SDL_AUDIODRIVER=dummy`. * dosemu 1.4.0.8, `-t -quiet`, private `HOME`, batch ending `exitemu`. * 1 warm-up + 3 measured runs; median; all exit status 0. * COFF output compared with the 4-byte time stamp zeroed (raw COFF differs only in that stamp). | workload | DOSBox median | dosemu median | ratio | |---|---:|---:|---:| | A: 4 KB with listing | **1.99 s** | **0.35 s** | 5.7× | | B: 16 KB no listing | **2.23 s** | **0.43 s** | 5.2× | The timestamp-normalised object md5 is identical for every run of each workload and across the two emulators: * A: `e9c59beb4a5e29e391495ba2b312fc79` * B: `69a525b4689f60f51afc3059b66b66a8` Raw md5s differ because bytes 4–7 of the COFF header are the creation time; the assembled bytes are the same. Setup cost: DOSBox needed only an existing config; dosemu auto-created `~/.dosemu`, required a drive C mapping and a batch (because `-E` does not exit without `exitemu`), and prints harmless CD-ROM/`unknown window sizes` warnings. That is a one-off few-minute setup, not per-run. **Recommendation:** dosemu is materially faster (≈5×) under these settings and is worth using for batch/iterative assembly. DOSBox remains the known-good fallback and is what `roundtrip.sh` uses. Note that with DOSBox's original `cycles=3000` config the gap is much larger (the 16 KB listing workload took ~35 s), because DOSBox was being cycle-limited, not because dosemu is 70× faster. --- ## 10. Reproduction Scripts live in `harness/sdk-scratch/` (scratch; not part of the SDK): * `roundtrip.sh OFF SIZE BASE` — extract, disassemble, convert, assemble, link, convert, interleave, diff. Example: `./roundtrip.sh 0x1000 32768 3F208000` * `dis2as34.py BASE SIZE IN.ASM` — the five fixups. * `bench2.py` — the DOSBox/dosemu benchmark (needs the private dosemu `HOME` under `harness/sdk-scratch/dosemu-home`). * `dosbox.conf` / `dosbox-bench.conf` — headless DOSBox configs. The SDK itself is untouched; all extraction goes to `harness/sdk-scratch/` (`work/`, `cgtools/`, `sdb/`, `disk1987/`), which is not staged. --- ## 11. Open questions / not tested * No payload was executed on a VT1200 or in MAME. The round trip proves the encoder, not the runtime or the network delivery. * No OS/2 environment was set up, so the r5.01 tools (and their `gspopt` optimizer) were not exercised. If the newer code generator matters, that is a separate OS/2-in-QEMU project. * The 1987 `GSPSIM` simulator was not run; MAME is the better execution model for this project. * The Math Graphics Function Library and TIGA libraries were not evaluated against the VT1200's monochrome display. * The optimal free RAM range for a payload depends on what the OS has allocated; the toolchain can link anywhere, but the loader must choose a safe range.