diff --git a/harness/hello-world/.gitignore b/harness/hello-world/.gitignore new file mode 100644 index 0000000..9d931c4 --- /dev/null +++ b/harness/hello-world/.gitignore @@ -0,0 +1 @@ +work/ diff --git a/harness/hello-world/README.md b/harness/hello-world/README.md new file mode 100644 index 0000000..03434d5 --- /dev/null +++ b/harness/hello-world/README.md @@ -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. diff --git a/harness/hello-world/build.sh b/harness/hello-world/build.sh new file mode 100755 index 0000000..be32888 --- /dev/null +++ b/harness/hello-world/build.sh @@ -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" diff --git a/harness/hello-world/cofextract.py b/harness/hello-world/cofextract.py new file mode 100755 index 0000000..ec30f9d --- /dev/null +++ b/harness/hello-world/cofextract.py @@ -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= size= file_offset= +""" +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( + "> 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]) diff --git a/harness/hello-world/evidence/control-frame-145.png b/harness/hello-world/evidence/control-frame-145.png new file mode 100644 index 0000000..3057b38 Binary files /dev/null and b/harness/hello-world/evidence/control-frame-145.png differ diff --git a/harness/hello-world/evidence/hello-frame-145.png b/harness/hello-world/evidence/hello-frame-145.png new file mode 100644 index 0000000..e613fcd Binary files /dev/null and b/harness/hello-world/evidence/hello-frame-145.png differ diff --git a/harness/hello-world/evidence/hello-payload.dis b/harness/hello-world/evidence/hello-payload.dis new file mode 100644 index 0000000..5310d5d --- /dev/null +++ b/harness/hello-world/evidence/hello-payload.dis @@ -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 diff --git a/harness/hello-world/evidence/hello-ram-marker.txt b/harness/hello-world/evidence/hello-ram-marker.txt new file mode 100644 index 0000000..b12ae09 --- /dev/null +++ b/harness/hello-world/evidence/hello-ram-marker.txt @@ -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) diff --git a/harness/hello-world/evidence/hello-run.log b/harness/hello-world/evidence/hello-run.log new file mode 100644 index 0000000..5b349ed --- /dev/null +++ b/harness/hello-world/evidence/hello-run.log @@ -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) diff --git a/harness/hello-world/hello.asm b/harness/hello-world/hello.asm new file mode 100644 index 0000000..80855f8 --- /dev/null +++ b/harness/hello-world/hello.asm @@ -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 diff --git a/harness/hello-world/payload.bin b/harness/hello-world/payload.bin new file mode 100644 index 0000000..7d61291 Binary files /dev/null and b/harness/hello-world/payload.bin differ diff --git a/harness/hello-world/payload.map b/harness/hello-world/payload.map new file mode 100644 index 0000000..df693c1 --- /dev/null +++ b/harness/hello-world/payload.map @@ -0,0 +1,34 @@ +***************************************************** +GSP COFF Linker, Version 2.00, 87.300 +***************************************************** +Sun Oct 18 01:20:31 2026 + +OUTPUT FILE NAME: +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] diff --git a/harness/mame/HELLO_WORLD_REPORT.md b/harness/mame/HELLO_WORLD_REPORT.md new file mode 100644 index 0000000..6010eed --- /dev/null +++ b/harness/mame/HELLO_WORLD_REPORT.md @@ -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 +``` diff --git a/harness/mame/README.md b/harness/mame/README.md index 664821e..7af24ea 100644 --- a/harness/mame/README.md +++ b/harness/mame/README.md @@ -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`. diff --git a/harness/mame/hello.lua b/harness/mame/hello.lua new file mode 100644 index 0000000..4ac2a62 --- /dev/null +++ b/harness/mame/hello.lua @@ -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 +-- +-- 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'