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

483 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.