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.
This commit is contained in:
SDD committed 2026-10-18 01:20:38 +03:00
1 parent 0c3d24b93b
commit 1608a68cdb
15 files changed
+753

No files matched your search

+1
View File
@@ -0,0 +1 @@
work/
+91
View File
@@ -0,0 +1,91 @@
# Hello world: a payload built with the TMS34010 SDK
A minimal payload written in GSPA assembly, assembled and linked by the 1987
TI TMS34010 SDK (`GSPA 2.00` / `GSPLNK 2.00`) under DOSBox, then loaded into
main RAM and entered by `harness/mame/hello.lua`.
This is **our** code, not a disassembly of the firmware. The ROM
(`vt1200_v22.bin`, md5 `b46a3e442ccb86134d2356bd70d22fd2`) is only read.
**Read `harness/mame/HELLO_WORLD_REPORT.md` for the evidence and caveats.** The
short version:
* linked at **bit `0x01800000`** (byte `0x00300000` of the 4 MiB main RAM);
* writes `0xDEAD`, `0xBEEF` and a loop counter `0x0400` to RAM at bit
`0x01810000`, and fills a 256x64 block in video RAM at row 100, column 64;
* both are observed from inside the emulator (RAM readback) and from a frame
dump (white block at screen x 256..511, y 136..199).
## Build
```sh
harness/hello-world/build.sh # origin defaults to 01800000h
harness/hello-world/build.sh 01840000 # alternate bit address
```
It extracts the DOS tools into `work/` on first use (same image and method as
`harness/sdk-roundtrip/roundtrip.sh`), assembles `hello.asm`, links it, and
extracts the `.text` section from the COFF with `cofextract.py`.
Outputs:
| file | what it is |
|---|---|
| `payload.bin` | the linked bytes (82 bytes) |
| `payload.map` | GSPLNK's map, with the bit-address origin |
| `work/HELLO.LST` | GSPA listing (addresses in **bits**) |
`payload.bin` and `payload.map` are committed, so the MAME run does not need
DOSBox or the SDK. Rebuild them to change the payload.
## Run
From the repository root:
```sh
M=/home/arti/code/k-space/mame0289/vt1200
HELLO_END=150 \
VT1200_DUMP_FRAME=145 VT1200_DUMP_RAM=145 \
VT1200_DUMP_DIR=harness/hello-world/evidence \
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none \
-autoboot_script harness/mame/hello.lua -autoboot_delay 0
```
Environment knobs (all optional):
| variable | default | meaning |
|---|---|---|
| `HELLO_AT` | 120 | frame at which to load the payload and arm the entry breakpoint |
| `HELLO_END` | 150 | frame at which to inspect the markers and exit |
| `HELLO_PAYLOAD` | `harness/hello-world/payload.bin` | linked payload |
| `HELLO_ORIGIN` | `0x01800000` | load/entry bit address |
Set `HELLO_AT=9999` for the control run: nothing is loaded, and the markers
stay zero.
## Mechanism (and what it is not)
`hello.lua` lets the firmware boot to the level-0 diagnostics menu, writes the
payload bytes into RAM at frame 120, and arms a breakpoint on the menu's idle
loop (`3FDCE400`, `dsjs a9, itself`). The breakpoint action assigns
`pc = 01800000` and resumes. The payload then runs, masks interrupts, writes
its markers and the VRAM block, and spins.
This is **debugger entry, not a delivery mechanism**. It proves the toolchain
produces runnable code and the payload runs on the emulated CPU; it says
nothing about how code would reach a real terminal.
## Evidence files
| file | what it shows |
|---|---|
| `evidence/hello-run.log` | transcript of the run above |
| `evidence/hello-ram-marker.txt` | RAM excerpt: payload bytes and marker words |
| `evidence/hello-frame-145.png` | the frame at 145, cropped to the menu + block |
| `evidence/control-frame-145.png` | the same crop with `HELLO_AT=9999` (no block) |
| `evidence/hello-payload.dis` | `dis34` disassembly of `payload.bin`, base `01800000` |
The `.ppm` and `.bin` dumps referenced in `hello-run.log` are large and are not
committed; the command above regenerates them.
+86
View File
@@ -0,0 +1,86 @@
#!/bin/sh
# Assemble and link the hello-world payload with the 1987 TI TMS34010 SDK
# (GSPA 2.00 / GSPLNK 2.00) under DOSBox, then extract the linked bytes from
# the COFF (not from gsprom: its hex addresses wrap at 64 KB).
#
# Everything is written under harness/hello-world/work/; nothing is written
# outside this tree. The assembled output is the payload itself, not a
# reassembly of the firmware, so this build proves the toolchain generates
# runnable code.
#
# Usage: build.sh [origin-bit-address]
# Default origin 01800000h (byte 00300000h of the 4 MiB main RAM).
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
DIR=$(cd "$(dirname "$0")" && pwd)
WORK="$DIR/work"
ORIGIN=${1:-01800000}
IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img"
DOSBOX_CONF="$ROOT/harness/sdk-scratch/dosbox-bench.conf"
# DOSBox is a 16-bit DOS environment: 8.3 upper-case names, no long paths.
HOSTNAME=HELLO
mkdir -p "$WORK"
# Extract the tool binaries once, the same way sdk-roundtrip/roundtrip.sh does.
if [ ! -x "$WORK/GSPA.EXE" ]; then
mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/"
mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/"
fi
cp "$DIR/hello.asm" "$WORK/HELLO.ASM"
# TI hex constants must begin with a decimal digit.
case "$ORIGIN" in
[0-9]*) ;;
*) ORIGIN="0$ORIGIN" ;;
esac
# Linker command file. CRLF, because the DOS tool reads it as text.
printf '%s\n' \
'-a' \
'-o hello.out' \
'-m hello.map' \
'hello.obj' \
'SECTIONS' \
'{' \
" .text ${ORIGIN}h: {}" \
'}' > "$WORK/HELLO.CMD"
sed -i 's/$/\r/' "$WORK/HELLO.CMD"
rm -f "$WORK"/HELLO.OBJ "$WORK"/HELLO.OUT "$WORK"/HELLO.LST \
"$WORK"/HELLO.MAP "$WORK"/HELLO.HI "$WORK"/HELLO.LO
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 180 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \
-c "mount c $WORK" -c "c:" \
-c "gspa hello hello.obj hello.lst -l" \
-c "gsplnk hello.cmd" \
-c "exit" -exit >/dev/null 2>&1
if [ ! -f "$WORK/HELLO.OUT" ]; then
echo "build.sh: GSPLNK produced no HELLO.OUT" >&2
exit 1
fi
# The assembler's own transcript is invisible in headless DOSBox; the listing
# is the record. Surface any error lines.
if [ ! -f "$WORK/HELLO.OBJ" ]; then
echo "build.sh: GSPA produced no HELLO.OBJ" >&2
grep -n '\*\*\*\*\*' "$WORK/HELLO.LST" >&2 || true
exit 1
fi
if grep -q '\*\*\*\*\*' "$WORK/HELLO.LST"; then
echo "build.sh: assembler reported diagnostics:" >&2
grep -n '\*\*\*\*\*' "$WORK/HELLO.LST" >&2
exit 1
fi
echo "--- hello.map (placement) ---"
sed -n '/SECTION ALLOCATION MAP/,/GLOBAL SYMBOLS/p' "$WORK/HELLO.MAP"
python3 "$DIR/cofextract.py" "$WORK/HELLO.OUT" "$DIR/payload.bin"
# The map comes from a DOS program (CRLF); keep the committed copy LF-only so
# it does not churn in git.
sed 's/\r$//' "$WORK/HELLO.MAP" > "$DIR/payload.map"
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Extract the first loadable section of a TI GSP COFF file as raw bytes.
GSPLNK writes a COFF whose section header carries the FULL 32-bit bit
address in `paddr`, so placement does not depend on gsprom's hex output
(whose addresses wrap at 64 KB -- documented in reference/tms34010-sdk.md).
Usage: cofextract.py hello.out payload.bin
Prints: origin=<bit address> size=<bytes> file_offset=<hex>
"""
import struct
import sys
# TI TMS34010 COFF: 20-byte file header, optional header size in the header.
# Section header: name[8] paddr(4) vaddr(4) size(4) scnptr(4) relptr(4)
# lnnoptr(4) nreloc(2) nlnno(2) flags(4) = 40 bytes
def main(out_path, bin_path):
d = open(out_path, "rb").read()
if len(d) < 20:
sys.exit("cofextract: file too short")
magic, nscns, _timdat, _symptr, _nsyms, opthdr, _flags = struct.unpack_from(
"<HHIIIHH", d, 0)
if magic != 0x0090:
sys.exit("cofextract: unexpected COFF magic %#06x" % magic)
base = 20 + opthdr
secs = []
for i in range(nscns):
off = base + i * 40
name = d[off:off + 8].rstrip(b"\0").decode("latin1")
paddr, vaddr, size, scnptr, _relptr, _lnnoptr, _nreloc, _nlnno, flags = \
struct.unpack_from("<IIIIIIHHI", d, off + 8)
secs.append((name, paddr, size, scnptr, flags))
# .text is the only initialized section GSPLNK emits for our payload.
text = [s for s in secs if s[0] == ".text"]
if not text:
sys.exit("cofextract: no .text section")
name, paddr, size, scnptr, _flags = text[0]
# size and paddr are bit units; bytes = size >> 3.
nbytes = size >> 3
payload = d[scnptr:scnptr + nbytes]
if len(payload) != nbytes:
sys.exit("cofextract: truncated section (%d of %d bytes)"
% (len(payload), nbytes))
open(bin_path, "wb").write(payload)
print("origin=%08X size=%d file_offset=%#x md5=%s" % (
paddr, nbytes, scnptr,
__import__("hashlib").md5(payload).hexdigest()))
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit("usage: cofextract.py hello.out payload.bin")
main(sys.argv[1], sys.argv[2])
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,24 @@
01800000: dint
01800010: setf 16, 0, 0
01800020: setf 32, 0, 1
01800030: movi 0xdead, a0
01800060: move a0, @0x1810000, 0
01800090: movi 0xbeef, a0
018000c0: move a0, @0x1810010, 0
018000f0: movi 0x4019040, a2
01800120: movi 0x40, a3
01800150: movi 0x10, a4
01800180: sub a1, a1
01800190: movi 0xffff, a0
L18001C0:
018001c0: move a4, a5
L18001D0:
018001d0: move a0, *a2+, 0
018001e0: addi 0x1, a1
01800200: dsjs a5, L18001D0
01800210: addi 0x300, a2
01800230: dsjs a3, L18001C0
01800240: move a1, @0x1810020, 0
L1800270:
01800270: nop
01800280: jruc L1800270
@@ -0,0 +1,20 @@
VT1200 hello-world payload: main-RAM excerpt from the frame-145 dump
(full dump: VT1200_DUMP_RAM=145 VT1200_DUMP_DIR=... -> vt1200-ram-145.bin, 4 MiB)
payload image at byte 0x00300000 (bit 0x01800000), 82 bytes:
dump == payload.bin: True
00300000: 60 03 50 05 40 07 e0 09 ad de 00 00 80 05 00 00
00300010: 81 01 e0 09 ef be 00 00 80 05 10 00 81 01 e2 09
00300020: 40 90 01 04 e3 09 40 00 00 00 e4 09 10 00 00 00
00300030: 21 44 e0 09 ff ff 00 00 85 4c 02 90 01 0b 01 00
00300040: 85 3c 02 0b 00 03 03 3d 81 05 20 00 81 01 00 03
00300050: fe c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00
markers at byte 0x00302000 (bit 0x01810000):
00302000: ad de ef be 00 04 00 00 00 00 00 00 00 00 00 00
00302010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00302020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
MAGIC1 = word @01810000 = 0xDEAD -> bytes AD DE
MAGIC2 = word @01810010 = 0xBEEF -> bytes EF BE
COUNT = word @01810020 = 0x0400 -> bytes 00 04 (64*16 = 1024 stores)
@@ -0,0 +1,25 @@
Warning: -video none doesn't make much sense without -seconds_to_run
vt1200: screen_update#1 frame=0 clip_y=0..0 screen=1280x901
HELLO: payload harness/hello-world/payload.bin bytes=82
vt1200: screen_update#2 frame=0 clip_y=1..900 screen=1280x901
vt1200: scanline#1 line=1 enabled=1
vt1200: guard#1 frame=0 dump_frame=145 dump_ram=145
vt1200: screen_update#3 frame=1 clip_y=0..0 screen=1280x901
vt1200: scanline#2 line=0 enabled=1
vt1200: screen_update#4 frame=1 clip_y=1..1 screen=1280x901
vt1200: scanline#3 line=1 enabled=1
vt1200: scanline#4 line=2 enabled=1
HELLO: loaded 41 words at bit 01800000; readback OK (0 bad)
HELLO: payload has run by frame 121 (PC=01800270)
vt1200: dumped frame 145 -> harness/hello-world/evidence/vt1200-frame-145.ppm (1280x901, 89974 lit = 7.8016%)
vt1200: dumped 4194304 bytes of main RAM (frame 145) -> harness/hello-world/evidence/vt1200-ram-145.bin
--- frame 150, 5 distinct PCs sampled ---
HELLO: MAGIC1@01810000 = DEAD (DEAD ok)
HELLO: MAGIC2@01810010 = BEEF (BEEF ok)
HELLO: COUNT @01810020 = 0400 (1024 stores ok)
HELLO: VRAM row100 @04019040: FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF
HELLO: VRAM row163 @04028C40: FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF
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)
Average speed: 872.26% (2 seconds)
+52
View File
@@ -0,0 +1,52 @@
; hello.asm -- minimal TMS34010 payload for the VT1200 harness.
;
; Built with GSPA 2.00 / GSPLNK 2.00 (1987 TI SDK, DOSBox) and linked at
; bit address 01800000h (byte 00300000h in main RAM).
;
; It leaves two independent pieces of evidence:
; 1. RAM: two magic words and a loop counter at bit 01810000h.
; 2. VRAM: a solid 256x64 block at framebuffer row 100, column 64.
;
; The screen renderer (harness/mame/vt1200.cpp, render_active_line) reads
; VRAM at bit 04000000h with a row pitch of 1024 bits and bit 0 = leftmost
; pixel, LSB-first inside each 16-bit word.
;
; Addresses in operands are BIT addresses, like every TMS34010 pointer.
; A 16-bit pixel row is 64 words; we write 16 words (256 pixels) and then
; advance the destination by 1024 - 256 = 768 bits (0300h).
.text
_start:
dint ; stay deterministic: no firmware ISR
setf 16, 0, 0 ; field 0 = 16 bits, zero-extended
setf 32, 0, 1 ; field 1 = 32 bits, zero-extended
; ---- channel 1: RAM markers -----------------------------------
movi 0DEADh, a0, L
move a0, @01810000h, 0 ; MAGIC1 (bytes AD DE in a dump)
movi 0BEEFh, a0, L
move a0, @01810010h, 0 ; MAGIC2 (bytes EF BE)
; ---- channel 2: solid block in video RAM ----------------------
movi 04019040h, a2, L ; 04000000h + 100*1024 + 64
movi 64, a3, L ; 64 rows
movi 16, a4, L ; 16 words = 256 pixels 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 by 16 bits
addi 1, a1 ; count this store
dsjs a5, word_loop
addi 0300h, a2 ; skip 768 bits to the next row
dsjs a3, row_loop
; The counter is 0400h only if all 64*16 stores executed.
move a1, @01810020h, 0 ; COUNT
spin:
nop
jruc spin
.end
Binary file not shown.
+34
View File
@@ -0,0 +1,34 @@
*****************************************************
GSP COFF Linker, Version 2.00, 87.300
*****************************************************
Sun Oct 18 01:20:31 2026
OUTPUT FILE NAME: <hello.out>
ENTRY POINT SYMBOL: 0
SECTION ALLOCATION MAP
output attributes/
section page origin length input sections
-------- ---- ---------- ---------- ----------------
.data 0 00000000 00000000
.bss 0 00000000 00000000 UNINITIALIZED
.text 0 01800000 00000290
01800000 00000290 hello.obj (.text)
GLOBAL SYMBOLS
address name address name
-------- ---- -------- ----
00000000 .bss 00000000 edata
00000000 .data 00000000 .bss
01800000 .text 00000000 end
00000000 edata 00000000 .data
00000000 end 01800000 .text
01800290 etext 01800290 etext
[6 symbols]
+187
View File
@@ -0,0 +1,187 @@
# 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`.
```asm
.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
```sh
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
```
+28
View File
@@ -75,6 +75,7 @@ Reports in this directory:
| `DUART_REPORT.md` | the DUARTs: wiring, the ISR-driven transmit, the datasheet-verified model edits |
| `BRINGUP_LOG.md` | the whole bring-up narrative, **including withdrawn claims** — read it sceptically |
| `ROMPATCH_REPORT.md` | runtime ROM patching (debug only — see the caveat) |
| `HELLO_WORLD_REPORT.md` | the TMS34010 SDK assembles an 82-byte payload of **our own** code; it runs, writes RAM markers and paints a VRAM block. Debugger entry, not a delivery mechanism |
## Address model (the important part)
@@ -428,3 +429,30 @@ break at 3f985060 ; just after 'sll 0x2,a11'
This is possible because the font parser's field moves use **field 1**, which
`SETF 0,0,1` leaves at 32 bits, so `n` is a full 32-bit `pcfReadInt32` value.
See `FINDINGS.md` for results and the rest of the analysis.
### Run our own payload (`hello.lua`)
`harness/hello-world/` is an 82-byte payload written in GSPA assembly, built by
the 1987 TI SDK (`GSPA`/`GSPLNK` under DOSBox). `hello.lua` loads it into main
RAM at **bit `0x01800000`** at frame 120, then arms a breakpoint on the level-0
menu idle loop (`3FDCE400`) whose action is `{ pc = 01800000 ; g }`.
This is **debugger entry, not a delivery mechanism**: it proves the toolchain
produces runnable code and the code runs on the emulated CPU, not how code
would reach a real terminal.
```sh
M=/home/arti/code/k-space/mame0289/vt1200
HELLO_END=150 VT1200_DUMP_FRAME=145 VT1200_DUMP_RAM=145 \
VT1200_DUMP_DIR=harness/hello-world/evidence \
$M vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-debug -debugger none \
-autoboot_script harness/mame/hello.lua -autoboot_delay 0
```
The payload writes `0xDEAD`, `0xBEEF` and a 1024-iteration counter at bit
`0x01810000`, and fills a 256x64 block in video RAM at row 100, column 64.
The script reads every value back and prints `ok`/`WRONG`; a frame dump shows
the block at screen x 256..511, y 136..199. `HELLO_AT=9999` is the control run
(nothing loads, markers stay zero). See `HELLO_WORLD_REPORT.md` and
`harness/hello-world/README.md`.
+152
View File
@@ -0,0 +1,152 @@
-- hello.lua -- load the hello-world payload into RAM and enter it.
--
-- MECHANISM: this is DEBUGGER ENTRY, not a delivery mechanism. The firmware
-- boots normally to the level-0 diagnostics menu; at frame HELLO_AT this script
-- writes the payload bytes into main RAM and arms a breakpoint on the menu's
-- idle loop (3FDCE400). The breakpoint action sets PC to the payload's bit
-- address and resumes. Nothing is patched on disk; the ROM is untouched.
--
-- Evidence printed from inside the emulator:
-- * payload bytes read back from RAM equal the linked binary
-- * the two magic words and the loop counter written by the payload
-- * the video-RAM block the payload wrote
--
-- <mame> vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
-- -debug -debugger none \
-- -autoboot_script harness/mame/hello.lua -autoboot_delay 0
--
-- Env:
-- HELLO_AT frame at which to load and enter (default 120)
-- HELLO_END frame at which to inspect and exit (default 150)
-- HELLO_PAYLOAD linked payload (default harness/hello-world/payload.bin)
-- HELLO_ORIGIN payload bit address (default 01800000)
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("HELLO ERROR: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("HELLO_AT") or "120")
local STOP_AT = tonumber(os.getenv("HELLO_END") or "150")
local ORIGIN = tonumber(os.getenv("HELLO_ORIGIN") or "0x01800000")
local PATH = os.getenv("HELLO_PAYLOAD") or "harness/hello-world/payload.bin"
local MAGIC1 = 0x01810000
local MAGIC2 = 0x01810010
local COUNT = 0x01810020
local VRAM_ROW = 0x04019040 -- 04000000h + 100*1024 + 64
local IDLE_PC = 0x3FDCE400 -- level-0 menu idle loop (dsjs a9, itself)
local payload = nil
do
local f, err = io.open(PATH, "rb")
if not f then
print("HELLO ERROR: cannot open " .. PATH .. ": " .. tostring(err))
machine:exit()
return
end
payload = f:read("*a")
f:close()
print(string.format("HELLO: payload %s bytes=%d", PATH, #payload))
end
local frames = 0
local loaded = false
local entered = false
local hist = {}
local function load_payload()
local n = math.floor(#payload / 2)
local words = {}
for i = 0, n - 1 do
local lo = payload:byte(i * 2 + 1)
local hi = payload:byte(i * 2 + 2)
words[i] = lo | (hi << 8)
prog:write_u16(ORIGIN + i * 16, words[i])
end
-- Read back and compare: this is the check that the load is placed at the
-- right bit address with the right word order.
local bad = 0
for i = 0, n - 1 do
if prog:read_u16(ORIGIN + i * 16) ~= words[i] then
bad = bad + 1
if bad <= 4 then
print(string.format("HELLO MISMATCH word %d at bit %s: wrote %04X read %04X",
i, hex(ORIGIN + i * 16), words[i], prog:read_u16(ORIGIN + i * 16)))
end
end
end
print(string.format("HELLO: loaded %d words at bit %s; readback %s (%d bad)",
n, hex(ORIGIN), bad == 0 and "OK" or "FAILED", bad))
-- Entry is a breakpoint action: the firmware reaches IDLE_PC every frame.
-- (The action only assigns PC and resumes; the payload proves itself.)
dbg:command("bpset " .. hex(IDLE_PC) .. ",,{ pc = " .. hex(ORIGIN) .. " ; g }")
end
local function inspect()
local m1 = prog:read_u16(MAGIC1)
local m2 = prog:read_u16(MAGIC2)
local ct = prog:read_u16(COUNT)
print(string.format("HELLO: MAGIC1@%s = %04X (%s)", hex(MAGIC1), m1,
m1 == 0xDEAD and "DEAD ok" or "WRONG"))
print(string.format("HELLO: MAGIC2@%s = %04X (%s)", hex(MAGIC2), m2,
m2 == 0xBEEF and "BEEF ok" or "WRONG"))
print(string.format("HELLO: COUNT @%s = %04X (%s)", hex(COUNT), ct,
ct == 0x0400 and "1024 stores ok" or "WRONG"))
-- The 16 words of row 100, then the same columns of row 163 (last row).
local function rowdump(bitaddr, tag)
local s = {}
for i = 0, 15 do
s[#s + 1] = string.format("%04X", prog:read_u16(bitaddr + i * 16))
end
print(string.format("HELLO: VRAM %s @%s: %s", tag, hex(bitaddr), table.concat(s, " ")))
end
rowdump(VRAM_ROW, "row100 ")
rowdump(VRAM_ROW + 63 * 1024, "row163 ")
-- Every word of the 64x16 region must be FFFF: no holes, no short rows.
local filled, holes = 0, 0
for r = 0, 63 do
for c = 0, 15 do
local v = prog:read_u16(VRAM_ROW + r * 1024 + c * 16)
if v == 0xFFFF then filled = filled + 1 else holes = holes + 1 end
end
end
print(string.format("HELLO: VRAM block %d/1024 words are FFFF, %d holes", filled, holes))
-- One word either side of the block must be untouched by us.
print(string.format("HELLO: VRAM row100 word before start = %04X, after end = %04X",
prog:read_u16(VRAM_ROW - 16), prog:read_u16(VRAM_ROW + 16 * 16)))
print(string.format("HELLO: PC at inspection = %s (payload spin expected)", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
local pcv = cpu.state['PC'].value
hist[pcv] = (hist[pcv] or 0) + 1
if frames == AT and not loaded then
loaded = true
load_payload()
end
if loaded and not entered and prog:read_u16(MAGIC1) == 0xDEAD then
entered = true
print(string.format("HELLO: payload has run by frame %d (PC=%s)",
frames, hex(cpu.state['PC'].value)))
end
if frames >= STOP_AT then
print(string.format("--- frame %d, %d distinct PCs sampled ---", frames,
(function() local n = 0 for _ in pairs(hist) do n = n + 1 end return n end)()))
inspect()
machine:exit()
end
end, "vt1200-hello")
dbg.visible_cpu = cpu
dbg.execution_state = 'run'