Files
vt1200/harness/mame/HELLO_WORLD_REPORT.md
SDD 1608a68cdb hello-world: the TMS34010 SDK assembles code that runs on the emulated VT1200
An 82-byte payload written in GSPA assembly, linked at bit 01800000h and
loaded into main RAM by harness/mame/hello.lua, executes on the CPU: it
writes DEAD/BEEF and a 1024-iteration counter into RAM and fills a 256x64
block in video RAM, which appears in the frame dump at x 256..511,
y 136..199.  Entry is a debugger breakpoint on the level-0 menu idle loop,
not a delivery mechanism.  Includes the build script, a COFF .text
extractor (gsprom's hex addresses wrap at 64 KB), the run log, RAM excerpt,
frame crops including a no-load control, and HELLO_WORLD_REPORT.md.
2026-10-18 01:20:38 +03:00

8.8 KiB

Hello world: the TMS34010 SDK builds code that runs

Status: DONE. The 1987 TI toolchain (GSPA 2.00 / GSPLNK 2.00) assembles and links an 82-byte payload from our own source, and the payload executes on the emulated VT1200 CPU: it writes checkable values into main RAM and a solid block into video RAM, and the block appears in a frame dump.

This is the first time code we generated has run here. The payload is written from scratch — it is not a reassembly of the firmware. The ROM (vt1200_v22.bin, md5 b46a3e442ccb86134d2356bd70d22fd2) is only read.

What the payload does

Source: harness/hello-world/hello.asm. Linked image: payload.bin (md5 db91788991ebba476b55a11127a36475), independently disassembled in evidence/hello-payload.dis.

        .text
_start:
        dint                       ; no firmware ISR while we run
        setf    16, 0, 0           ; field 0 = 16 bits
        setf    32, 0, 1           ; field 1 = 32 bits
        movi    0DEADh, a0, L
        move    a0, @01810000h, 0  ; MAGIC1
        movi    0BEEFh, a0, L
        move    a0, @01810010h, 0  ; MAGIC2
        movi    04019040h, a2, L   ; VRAM base + row 100*1024 + col 64
        movi    64, a3, L          ; 64 rows
        movi    16, a4, L          ; 16 words (256 px) per row
        sub     a1, a1             ; counter = 0
        movi    0FFFFh, a0, L
row_loop:
        move    a4, a5
word_loop:
        move    a0, *a2+, 0        ; store word, postincrement 16 bit-address units
        addi    1, a1
        dsjs    a5, word_loop
        addi    0300h, a2          ; 1024 - 256 bits to the next row
        dsjs    a3, row_loop
        move    a1, @01810020h, 0  ; COUNT = 0400h only if all 1024 stores ran
spin:
        nop
        jruc    spin
        .end

That is two independent channels:

  1. RAM — 0xDEAD at bit 0x01810000, 0xBEEF at 0x01810010, and a counter at 0x01810020 that reaches 0x0400 only if the two nested loops really executed 64 x 16 stores.
  2. Video RAM — 1024 words at bit 0x04019040 (framebuffer row 100, column 64) all become 0xFFFF, i.e. a filled 256x64 block.

Placement and loading

  • Linked origin: bit 0x01800000 = byte 0x00300000 of the 4 MiB main RAM. The linker map (payload.map) says .text origin 01800000 length 00000290 — those are bit addresses, so the image is 0x290 >> 3 = 82 bytes.
  • cofextract.py reads the linked COFF directly: the .text section header carries paddr = 01800000 (full 32-bit bit address), size = 0x290 bits and scnptr = 0xa8 (file offset). It does not use gsprom, whose hex addresses wrap at 64 KB — the documented trap.
  • harness/mame/hello.lua writes the 41 words into program space with write_u16(ORIGIN + i*16, lo | hi<<8), then reads every word back and compares it with the file. Result: 0 bad. The frame-145 RAM dump also contains payload.bin byte-for-byte at byte 0x00300000 (evidence/hello-ram-marker.txt).
  • The load address is far above the firmware's working set at that point (the frame-600 RAM dump's highest non-zero byte is 0x503F1), so nothing the firmware does before entry can overwrite it.
  • The image has no absolute self-references, so it also runs relinked at 01840000h with HELLO_ORIGIN=0x01840000 (checked: same markers, PC 01840280 in the spin). Only the code's placement changes; the RAM and VRAM operand addresses are absolute by design.

Entry is debugger entry, not a delivery mechanism. hello.lua lets the firmware boot normally to the level-0 diagnostics menu, loads the payload at frame 120, and arms a breakpoint on the menu idle loop (3FDCE400, dsjs a9, itself). The action is { pc = 01800000 ; g }. This proves the toolchain produces runnable machine code and the payload runs on the emulated CPU. It says nothing about how code would reach a real terminal, and nothing here should be read as a network or firmware-loading route.

Evidence observed (not inferred)

Run: HELLO_END=150 VT1200_DUMP_FRAME=145 VT1200_DUMP_RAM=145 VT1200_DUMP_DIR=harness/hello-world/evidence, no other environment. Transcript: evidence/hello-run.log.

HELLO: loaded 41 words at bit 01800000; readback OK (0 bad)
HELLO: payload has run by frame 121 (PC=01800270)
HELLO: MAGIC1@01810000 = DEAD (DEAD ok)
HELLO: MAGIC2@01810010 = BEEF (BEEF ok)
HELLO: COUNT @01810020 = 0400 (1024 stores ok)
HELLO: VRAM block 1024/1024 words are FFFF, 0 holes
HELLO: VRAM row100 word before start = 003C, after end = 0000
HELLO: PC at inspection = 01800280 (payload spin expected)

0x01800280 is the jruc spin instruction — the CPU is still in the payload's own spin loop at inspection time, not back in the firmware.

The frame dump shows the block at screen x 256..511, y 136..199 — exactly 64 rows of 256 pixels. That matches the renderer's geometry (render_active_line(): x = heblnk + fx, y = veblnk + fby with the firmware's heblnk = 192, veblnk = 36), so the pixels are the ones the payload wrote, at the row and column it wrote them.

evidence/hello-frame-145.png is the crop (menu title + block). evidence/control-frame-145.png is the same run with HELLO_AT=9999, so the payload never loads:

  • markers all 0000, 0/1024 words are FFFF, PC is 3FDCE400 (firmware);
  • the full-frame difference between the two runs is 13,900 pixels — the 16,384 pixels of the block minus the 2,484 text pixels that were already white.

So the block and the markers exist only when the payload is entered.

Toolchain traps that cost time (and how this build avoids them)

trap what happened resolution
jr is not a mnemonic INVALID OPCODE; GSPA has JReq/JRge/… and JRUC for unconditional use jruc
MOVI …,W truncation movi 0FFFFh,a0,W gives WARNING - VALUE TRUNCATED (short immediate is sign-extended) use the ,L form
gsprom hex addresses wrap at 64 KB cannot be used for placement parse the COFF section header (paddr, size, scnptr); cofextract.py
bit vs byte linker origin/length are bit addresses (0x290 = 82 bytes) verified by RAM byte comparison and execution
headless DOSBox console output invisible; DOS upper-cases 8.3 names judge success by the .LST listing and the exit files; HELLO.ASM/HELLO.CMD

One thing that did not cause trouble: word order. Loading the gsprom/COFF bytes low-byte-first as little-endian write_u16 words produced the intended instructions, confirmed both by readback and by the CPU executing them (dint, setf, stores, loops, spin).

Build and reproduce

harness/hello-world/build.sh        # GSPA + GSPLNK under DOSBox, ~2 s

Then the run in harness/hello-world/README.md. payload.bin and payload.map are committed so the emulator run needs no DOS toolchain.

Configurations and baselines

No driver or existing script was modified. Checked after the work:

configuration result
no env, progress.lua, 200 frames PC=3FDCE400 samples=198 (3 distinct PCs) — unchanged
VT1200_IO_PATH_STUB=1, 200 frames samples=53 (75 distinct PCs) — unchanged
VT1200_SKIP_LEVEL1=1, frame 3000 Terminal Manager renders; VT1200_MOUSE=1 frame dump is pixel-identical (magick compare -metric AE = 0)
ROM md5 b46a3e442ccb86134d2356bd70d22fd2, unchanged

One documentation observation, not a regression: the committed harness/mame/duart2_default_tm_frame3000.png differs from a current frame-3000 dump by 4,118 pixels around the Messages pane and shows Ethernet Address 00-00-00-00-00-00, i.e. it predates the LANCE address ROM. It is a stale artifact; the 0-pixel tablet-vs-mouse comparison is the configuration check that matters here.

Not attempted

The stretch goal — calling the firmware's printer routine (3FDE00C0) to put a string on the host/printer port — was skipped deliberately. That routine walks a null-terminated list of string pointers in a9 and calls the firmware's output path at 3FDF97F0, which is ISR-driven on DUART1 (DUART_REPORT.md); the payload masks interrupts, and re-enabling the firmware's interrupt state to satisfy a printer test would confound the "payload runs" result. The RAM marker and the pixels are complete evidence on two independent channels, and both are machine-checked.

Files added

harness/hello-world/hello.asm        payload source (GSPA syntax)
harness/hello-world/build.sh         assemble + link + extract
harness/hello-world/cofextract.py    COFF .text extractor (bit addresses intact)
harness/hello-world/payload.bin      82-byte linked image
harness/hello-world/payload.map      GSPLNK map (origin 01800000 bits)
harness/hello-world/README.md        build/run/mechanism
harness/hello-world/evidence/        run log, RAM excerpt, frame crops, disassembly
harness/mame/hello.lua               loader + entry breakpoint + checks