Files
vt1200/reference/tms34010-sdk.md
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

21 KiB
Raw Permalink Blame History

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<cc> mnemonic, the same one used for the 3-word absolute JA. The word count distinguishes them: 2 words → rewrite to jr<cc>, 3 words → leave ja<cc>. 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.