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.
21 KiB
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).
smart34010output 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/lnk34in this SDK. The assembler isGSPA.EXEand the linkerGSPLNK.EXE(the namessmart34010'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.libinto 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
SECTIONSorigin 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 | reference only | |
Patents/ |
five graphics-architecture patents | 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:
-
dis34 -b BASE -o region.bin > region.dis— disassemble with the opcode words in a trailing comment (-ois required; the fixups use them). -
dis2as34.py BASE SIZE region.dis > RT.ASM— apply the five fixups (section 3), prepend.text, append.end. -
Assemble and link:
gspa rt rt.obj rt.lst -l gsplnk rt.cmd gsprom -i rt.out rthi rtlowith
rt.cmd:-a -o rt.out -m rt.map rt.obj SECTIONS { .text 3F208000h: {} }The origin is a bit address.
-amakes an absolute module;-mwrites a map;-ewould set an entry point. -
gsprom -iwrites two Intel-hex files (high byte and low byte for a 16-bit EPROM pair). Interleave low byte then high byte per word. -
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.
-
Hex constants. GSPA has no
0x: a hex constant must start with a decimal digit and end inh.0x220→0220h;0xC00000B0→0C00000B0h(nine digits are accepted and truncated to 32 bits). GSPCV does the same conversion for the old>xxxxsyntax. -
MOVIshort vs long. GSPA has oneMOVImnemonic 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 longMOVI 0x53E0(three words) becomes a shortMOVIW(two words), and the short form sign-extends to0xFFFF53E0. Force the form with the third operand:movi 053E0h, sp, Lor..., W. The opcode word count in the-ocomment tells which form the ROM used. The same applies toADDI,CMPI, andSUBI. -
B-file
SP.dis34prints register 15 asspregardless of file. Bit 4 of the encoded register field says which file:0x0F= A-file SP,0x1F= B-file SP. GSPA's barespmatched the ROM for most forms (memory operands such as*sp(offset)assembled as B15;MOVI/MMFMregister operands as A15), but anRLdestination chose A15 where the ROM has B15 (0x312F vs 0x313F).b15forces the B-file register;a15is rejected. Check the register field, not the printed name. -
Long relative
JRprinted asJA. Indis34, the 2-word relative jump (opcode low byte0x00, 16-bit displacement) is printed with theja<cc>mnemonic, the same one used for the 3-word absoluteJA. The word count distinguishes them: 2 words → rewrite tojr<cc>, 3 words → leaveja<cc>. Without this, every long relative branch grows by a word and the image diverges. -
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
.setvalue in aJR(it emits the absoluteJAform and errors). Emit the exact opcode words from the-ocomment as.wordfor those branches. ForDSJS, 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
00000030is 3 words in. SECTIONSbinding is a bit address:.text 3F208000h: {}produced.text 0 3F208000 00020000in the map (origin bit, length bits).- The linker's
MEMORYorigins/lengths are bits too. - The COFF output carries the true 32-bit bit address. In
RT16.OUTthe optional header saystext = 0x3F208000,code size = 0x20000bits, and the.textsection header sayspaddr = vaddr = 0x3F208000,size = 0x20000bits. The file header magic is0x0090, flags0x103(including0x40, 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 byteiat physical byte addressA>>3 + i. Do not re-link at a different address than the load address: the C compiler emitsCALLAand 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 > progis 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/-crselect 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. char8 bits,short16,intandlong32, pointers 32-bit bit addresses,float32,double64.- 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–B14are unused by the compiler and available to assembly. Callee saves the A registers it uses exceptA8;FS1must 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.libwith-c(ROM autoinit;boot.objis pulled in and_c_int00becomes the entry) or-cr(loader does the.cinitcopy). The boot code allocatesSYS_STACKin.bss, setsFP/STK/SP, calls_main, then_exit. - C sections:
.text(code + string literals + float constants),.cinit(initialized-global table),.bss(globals + both stacks + heap)..datais the assembler's, not the C compiler's..bssmust 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), linkerMEMORY/SECTIONS,gsprom, and the simulator. This is where the bit-address andW/Lbehaviour 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)— thedb340/gspmonmonitor workflow; not directly applicable to the VT1200 unless a compatible monitor can be loaded.SPVU006 Math Graphics Function Libraryand the 1987Graphics Math Function Librarydisk (GRAFIX.LIB,TEXT.LIB,MATH.LIB+ sources) — optional drawing primitives; not evaluated against the VT1200's mono framebuffer.SPVS002A/Cproduction data — hardware electrical/timing detail; the repo's user's guide covers the programming model.SPVU015A/CTIGA,SPVU018ATIGA art,SPVU027graphics 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):
- Long relative
JRis printed asJA. Indis34.c, the0x00low-byte case (16-bit displacement, 2-word instruction) callsprintf("ja%s …"); only the 8-bit case printsjr%s. This makes the output non-assemblable without the word-count fix. Example:jagt L3F211EE0 ; c700 0167is a relative jump, butjagtis the 3-word absolute instruction. - B-file
SPis printed assp. The register name hides bit 4;sp(A-file) andb15(B-file) are different encodings. movihides 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-oword count is the only clue.- 64 KB input fails with
unexpected end of input at byte 65536; use ≤32 KB regions or fix the offset arithmetic. - Data tables decoded linearly can produce branches into the middle of
instructions;
-a(analyze) may help, but for byte-exact reassembly the.wordfallback 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, privateHOME, batch endingexitemu. - 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 3F208000dis2as34.py BASE SIZE IN.ASM— the five fixups.bench2.py— the DOSBox/dosemu benchmark (needs the private dosemuHOMEunderharness/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
gspoptoptimizer) were not exercised. If the newer code generator matters, that is a separate OS/2-in-QEMU project. - The 1987
GSPSIMsimulator 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.