The old README still described the repo as ROM images plus a Ghidra setup. The project now spans the MAME harness, the real terminal, the COFF/SDK toolchain and the networking reverse engineering, so rewrite it as the entry point. New top-level content: - a dated status table (ROM, Ghidra, MAME, real unit, our code, networking, LAT, font path), with the open items named as open; - what the firmware actually contains (MIT X11 R4, BSD TCP/IP via 386BSD, BIND, telnet, TI RTS, fonts); - the address model table, corrected against harness/mame/vt1200.cpp (RAM 0..0x1FFFFFF; DUART bit 0x06200000/0x06400000); - how the faithful MAME run stops at level 0, why no documented device can pass it, and what the stub does and does not prove; - the real-unit debug channel (TCP 771), the challenge, and a table of what is proven on hardware; - the three routes for running our own code, and the heap model; - the LANCE + BSD stack mapping, the RMD1_FRAM blocker and the claim deferred for real silicon; - LAT and the idle-crash investigation, and the two font results; - a curated "odd findings" list and a documentation index. Factual corrections made while checking sources: the rdebug gate is a SETF 2 field (the port was never closed), CSR0=0x1BB has ERR clear, the pcfReadBitmaps overflow is not reachable on the TFTP path, and the real-unit framebuffer is 1024x2048 line-interleaved (the "line doubled" look was a partial read). The XCROSS 123k figure is the unmapped log, not a VT1200_CAL proxy hit. ROM md5 unchanged.
32 KiB
DEC VT1200 X Terminal — ROM images, emulation, and what we found
The DEC VT1200 is a monochrome X Window System terminal (circa 1990). It runs the X server, the terminal manager, the window manager and a VT emulator entirely from ROM on a Texas Instruments TMS34010 CPU, with 2–4 MB of RAM and no virtual memory.
This repository started as the firmware ROM dumps and has grown into a full reverse-engineering and execution project: a MAME machine that runs the firmware, a Ghidra database with the bit-address problems solved, a 1987 TI toolchain that still builds working code, and a debug channel into a real terminal over the LAN.
The ROM stays byte-identical throughout. Every experiment patches at
runtime, if at all; the reference image is vt1200_v22.bin
(md5 b46a3e442ccb86134d2356bd70d22fd2).
Status at a glance
As of 2026-10-30.
| Area | State |
|---|---|
| ROM images | Done. v2.1 and v2.2 reconstructed from 14 EPROMs each, bank order and byte interleave proven, checksums verified. |
| Disassembly / Ghidra | Done. TMS34010 module rebuilt for Ghidra 12, bit-address pointer constants materialised as references (~40k in v2.2). |
| MAME harness | Running. Boots POST, fails level 0 honestly, parks in the diagnostics menu. With VT1200_SKIP_LEVEL1=1 it boots the X server and reaches the Terminal Manager, with keyboard, mouse/tablet and LANCE. |
| Real terminal | Opened. The firmware's hidden debug TCP service on port 771 is reversed, its challenge is solved offline, and we can read, write, dump the ROM, load COFF images and run them. |
| Our own code | Runs. TI SDK assembly, a C program (HELLO in video RAM), and an X11 window — on the emulator and on the real unit. |
| Networking RE | Mapped. The LANCE driver and the BSD-derived TCP/IP stack are mapped function by function, with explicit firmware-specific entries where no counterpart exists. One claim still needs real silicon. |
| LAT / the idle crash | Open. The receive path is audited, the crash is narrowed to service announcements (not the data stream), and a soak procedure is ready for when the unit is back. |
| Font path | Characterised, not exploitable as hoped. The known pcfReadBitmaps overflow is real but is not reachable through the TFTP font loader. |
What the machine actually runs
The image is not just DEC code. reference/third-party-code.md maps what is
inside vt1200_v22.bin:
| component | evidence |
|---|---|
MIT X11 R4 — server (dix, mfb/cfb) and Xlib |
mfb*, pcf*, snf*, bdf*, Xlib strings; exact match to X11R4 pcfrbitmaps.c |
| BSD 4.3/4.4 TCP/IP | $Header/@(#) IDs for the socket/TCP/IP/UDP/mbuf set, imported at DEC by craig, 1989-10-05/06 |
| BSD BIND resolver, telnet + TN3270, inetd, lpd lineage, libc pieces | file IDs and code match |
| TI C runtime V2.00 | strlen/strcpy/strcmp match RTS.SRC instruction for instruction |
| Bitstream / Bigelow & Holmes / DEC Japan fonts | copyright strings in the font data |
Source-ID strings (RCS $Header:, SCCS @(#)) name the original files and
dates. They are strong evidence of provenance, but they are file names, not
licence statements.
Quick start
md5sum vt1200_v22.bin # b46a3e442ccb86134d2356bd70d22fd2
./reconstruct.py v2.1 # -> vt1200_v21.bin (address order)
./reconstruct.py v2.2 # -> vt1200_v22.bin
python3 -m unittest test_reconstruct test_ptr_constants -v
harness/mame/build.sh # build the MAME single-driver binary
harness/mame/run.sh # faithful: stops in the diagnostics menu
harness/mame/run.sh skip # VT1200_SKIP_LEVEL1=1: boot the OS to the GUI
harness/mame/run.sh stub # + VT1200_IO_PATH_STUB=1: get past level 0
harness/mame/run.sh pie # runtime-patch the failing checks; level 1 runs
harness/mame/run.sh net # skip + tablet + LANCE on a host tap
The confirmed memory map is in MEMORY_MAP.md; the harness detail — devices, env knobs, regression baseline, working conventions — is in harness/mame/README.md.
ROM images
| Folder | Firmware | ROMs | Part numbers |
|---|---|---|---|
v2.1/ |
V2.1 | 14 × 128 KiB | 23-210E9 .. 23-223E9 |
v2.2/ |
V2.2 | 14 × 128 KiB | 23-274E9 .. 23-287E9 |
Each file is 131072 bytes and is named by its DEC part number. Dumped and
published by 9track.net (Dave/9track collection):
https://9track.net/roms/ (section "DEC VT1200", category Terminal),
files at https://9track.net/roms/files/dec/vt1200/. Fetched 2026-10-15.
The bitsavers terminal archive does not host VT1200 images (checked /bits/
and /pdf/dec/terminal/), so 9track.net is the canonical source.
Firmware layout (reverse-engineered)
The 14 EPROMs are byte-interleaved 16-bit words, not a flat byte stream. Within each consecutive pair of part numbers, the odd part number holds the low byte and the even part number holds the high byte:
v2.1 pairs: (210,211) (212,213) (214,215) (216,217) (218,219) (220,221) (222,223)
v2.2 pairs: (274,275) (276,277) (278,279) (280,281) (282,283) (284,285) (286,287)
The seven 256 KiB banks are laid out so that file offset == physical byte - 0x07E40000: the highest part-number pair sits at the lowest address.
reconstruct.py writes that 1.75 MiB (0x1C0000) image by default; pass
--part-order for the legacy ascending-part-number ordering.
Evidence the interleave is correct: only it produces readable text, e.g.
-DEC-Terminal-Medium-R-Normal--14-100-100-100-C-80-ISO8859-1, WM XIO Error,
packet_size = %d exceeds MAXSIZE_RCVPKT, and SCCS headers such as
$Header: ctype.c,v 1.1 89/10/05 19:40:49 craig Exp $. Getting the interleave
wrong shifts every string by one byte (DEC becomes EDC).
The address model (read this before any address)
The TMS34010 is bit-addressed. A register, PC or stored pointer P selects
a bit; the referred physical byte is P >> 3. The top two logical address
bits are not emitted, so physical_bit == logical_bit & 0x3FFFFFFF.
| region | physical byte | bit address |
|---|---|---|
| RAM (4 MiB) | 0x000000 |
0x00000000 |
| Video RAM | — | 0x04000000 |
| DUART2 (keyboard/mouse) | 0x00C40000 |
0x06200000 |
| DUART1 (host/printer) | 0x00C80000 |
0x06400000 |
| NVR (battery-backed settings) | — | 0x06000000 |
| Ethernet address ROM (8 bytes) | — | 0x06800000 |
| LANCE RDP / RAP | — | 0x05800000 / 0x05900000 |
| ROM (1.75 MiB) | 0x07E40000 |
0x3F200000 |
ROM aliases (0x7f…, 0xbf…, 0xff200000–0xffffffff) |
same ROM | top 2 bits undecoded |
| Reset vector | file 0x1BFFFC |
logical 0xFFFFFFE0 → 0x3F200040 |
Consequences that cost real time if forgotten:
read_word(0x3f200000)returns ROM bytes 0..1 (physical0x07e40000).- MAME models the program space as
memory_access<32, 1, 3>— the address-map unit is one bit, so a 16-bit word spans 16 address units. The TMS34020 usesmemory_access<32, 2, 3>and is not interchangeable. - Ghidra loads the image at byte base
0x07E40000; firmware bit address = Ghidra address × 8.unidasm/dis34print bit addresses; Ghidra prints byte addresses. currentProgram.getImageBase()is not the ROM base — use the loaded block.
Disassembly and Ghidra
MAME's bundled unidasm has a tms34010 backend:
unidasm vt1200_v22.bin -arch tms34010 -basepc 0x7E40000 -count 40
smart34010's dis34 (note -b is a bit-address base; two encodings were
corrected and validated by round-trip through the 1987 assembler — see
harness/sdk-roundtrip/):
dis34 -a -l -b 3F200000 vt1200_v21.bin
tools/vt1200_map.py prints the vectors and verifies the per-bank header
checksums:
python3 tools/vt1200_map.py vt1200_v22.bin
tools/ptr_constants.py finds bit-address pointer constants and resolves their
targets (--strings for string targets, --tsv for a table). On v2.2 it finds
~44,700 constants, of which ~3,680 distinct targets are strings, e.g.
0x004B08 <- 0x3F225840 'LAT: could not allocate XCB for …'.
Ghidra has no built-in TMS34010 language, so we install the community module
ZPaul2Fresh8/Ghidra_TMS34010:
tools/ghidra-tms34010/— vendored, patched module sourcetools/ghidra-tms34010-local.patch— all local changes vs. upstreamtools/install-ghidra-plugin.sh— installs and compiles it (needs sudo)
The patch fixes two things: the Ghidra 12 pspec schema (space="rom"
lowercase, initialized="false" on memory blocks) and the upstream Midway
arcade board map, whose GFX/ROM blocks collided with the loaded VT1200
image.
ghidra/ is a ready-to-open project (ghidra/vt1200.gpr) with both images
imported at byte base 0x07E40000. ghidra_scripts/FixPtrConstants.java
creates the bit-constant references (37,187 in v2.1, 40,290 in v2.2);
AssertPtrFix.java is the headless regression check. Caveat: the module's
pcode semantics are incomplete, so flow-following cannot expand far — the boot
bank is disassembled linearly as well.
Running the firmware under MAME
A minimal machine (harness/mame/vt1200.cpp) loads the firmware into a
TMS34010 so it can be run under MAME's debugger: trace, breakpoints,
single-stepping, memory watch. It is a visibility tool, not an accurate
VT1200 emulation.
A bare harness/mame/run.sh with no environment is the faithful configuration. It
models video RAM, the board-window handshake, both DUARTs (real SCN2681
devices), the LK401 keyboard and a pointing device, the LANCE, the NVR and the
Ethernet address ROM. Two of those were not conveniences:
- the level-1 host-port transmit is ISR-driven, so a fake DUART cannot work;
- without a real DUART2, the board write-back latch answered a read of the write-only IMR with its own value, so the OS serviced a phantom interrupt that could never clear (and the GUI never appeared).
Where a faithful run stops
A bare run passes POST and then fails level 0's I/O data path test, parking in the interactive diagnostics menu:
VERSION 2.08 - SELECTED TEST TYPE:
That test wants 10 bytes on DUART2 channel A, and nothing is attached. It
is not satisfiable by any documented device: the routine writes one byte
(0xE1, "disable auto-repeat", which has no reply), sets the channel to
normal mode, not loopback, and checks only the count — the received
byte is stored and never compared. The keyboard's reply is 4 bytes, the mouse's
4, the tablet's 5. A faithful LK401 would not help: the init sequence's
receiver reset flushes the FIFO. See reference/level0-10byte-puzzle.md.
It used to "pass" by accident, because the write-back latch returned 0x0099
from the status register and bit 0 is RXRDY, feeding the drain loop ten
phantom bytes. Making DUART2 real moved the regression baseline — that looks
like a regression and is the removal of a false pass.
The one deliberate test-fixture stand-in
VT1200_IO_PATH_STUB=1 is not a keyboard model. It stands in for an
unknown fixture on the keyboard port: after the one 0xE1 write it feeds
10 arbitrary (0x00) bytes back on the same receive path, only while the
channel is polled. Nothing compares them; only the count matters. It is off by
default, and the faithful run still fails level 0 exactly as before.
With it on, level 0 clears and level 1 reports HOST PORT - 15 and
PRINTER PORT - 16, prints TYPE 'GO' TO CONTINUE, and continues — then
wedges in a DUART1 counter-ready interrupt storm from frame ~2839. It does
not boot the OS, and it does not park at HOST PORT - 15.
How far the OS gets
VT1200_SKIP_LEVEL1=1 drives DUART1 IP3 high, which the firmware takes as
a signal to jump to the OS cold-start and skip all 15 level-1 tests. This is a
modelled board input; the real machine reads IP3 low. A skipped run is
evidence about the OS, graphics and crash behaviour, and is not evidence
about the real terminal's POST.
With the skip, the OS boots a running X11 server and reaches the
Terminal Manager: the root-window weave renders, the cursor is handed to
the terminal's own mouse cursor, and the V2.2 box appears — title bar,
Messages, Ethernet/IP/LAT, a Memory bar, and "All Diagnostics Passed". The
keyboard and mouse drivers initialise and speak the real protocol; the LANCE is
initialised and the TM shows the real hardware address
08-00-2B-16-4B-5A. Evidence frames: harness/mame/skip_frame3000.png,
duart2_default_tm_frame3000.png, tablet_cursor_*.png.
Regression baseline
progress.lua at 200 frames, no env:
PC=3FDCE400 samples=198 (3 distinct PCs)
It is a change detector, not a specification. It has moved twice, both
deliberately; harness/mame/README.md records why, including the
VT1200_IO_PATH_STUB=1 value (samples=53, 75 PCs) as a separate baseline.
Deliberate divergences
| divergence | default | what it does | restore the real machine |
|---|---|---|---|
| VSXXX-AB tablet | on | tablet on DUART2-B; absolute host-pointer reports | VT1200_MOUSE=1 |
VT1200_SKIP_LEVEL1 |
off | DUART1 IP3 high → skip all level-1 tests | leave unset |
VT1200_IO_PATH_STUB |
off | 10 bytes after the level-0 0xE1 write |
leave unset |
The full env-knob table (21 knobs, down from 33) is in
harness/mame/README.md.
The real terminal
The unit is a stock v2.2 machine with MAC 08:00:2b:16:4b:5a, static IP
100.103.100.103, reached from host 100.103.255.254.
The big find is that the firmware runs a remote debugger it calls
DEBUG TCP Port, listening on TCP 771 (dwt/notes/rdebug-static.md). Over
one connection a client can read and write the entire address space, call any
address as a function, capture console output, draw text, and make the terminal
open an outbound telnet connection.
The path is gated, and the static note's first gate turned out not to be one:
the settings word around 0xAECD0 reads 0x8070, but the compare is a
SETF 2 field at 0xAECD2 — two bits, and bits 2..3 are zero, so the
connection is accepted. The real gate is a 16-byte echo challenge at bit
0x3F59FDC0 (dwt_prf). It is solved offline: dwt/challenge.py computes
the reply from the challenge alone, and two live sessions passed the memcmp.
That gave us, on real hardware:
| proven on the real unit | evidence |
|---|---|
full ROM dump byte-identical to vt1200_v22.bin |
dwt/evidence/real-unit-rom.bin |
the read primitive (peek), screenshots assembled over it |
dwt/evidence/real-unit-canvas.png |
the write path: malloc → poke → COFF loader → create_process |
dwt/loader/evidence/hostload/ |
the first C program on the terminal (HELLO into VRAM) |
harness/c-hello/ |
| a real X11 window created by an injected client | dwt/gui/, dwt/notes/GUI_APPS.md |
| a LAN TFTP fetch and load | dwt/notes/TCPIP.md |
| NVRAM and the LANCE address ROM read back and matching the model | dwt/nvram_dump.py, dwt/rom_dump.py |
The terminal's own MOP remote console also works (dwt/mop_console.py); SHOW
reports Monitor Type VR319 and Firmware Revision V2.2.
Real-unit display geometry (it is not what the emulator shows)
- The framebuffer is 1024×2048, line-interleaved, 256 KiB: the image sits on even rows at 1:1, odd rows carry a mask. The visible raster is 1280×1024, with the 1024-wide canvas at x=192.
- A 128 KiB read holds only the top 512 image lines; doubling that is what
produced the early "line doubled" and "missing bottom" pictures. The full
256 KiB read plus
dwt/screenshot.py --field even --stretch 1280x1024matches the glass. - The Terminal Manager's
0x55/0xffrow pattern is a non-displayed mask, not a background setting: setting the root background to 100% white changes the root and leaves the TM pattern intact, while the screen looks solid white. - The bottom
Ethernet Port Error - 24box is the separate AUI/transceiver termination test, not the address ROM test — that one passes.
MAME models a linear 1bpp framebuffer and therefore never shows the interleave; that is a display-fetch difference, not a CPU difference.
Running our own code
Three independent routes exist, in increasing order of realism:
- MAME debugger entry.
harness/hello-world/is an 82-byte GSPA payload built by the 1987 TI SDK (GSPA/GSPLNKunder DOSBox).hello.lualoads it at bit0x01800000, arms a breakpoint on the diagnostics idle loop, and jumps to it. It writes RAM markers and paints a VRAM block. This proves the toolchain, not a delivery mechanism. - COFF loader +
create_process.dwt/loader/loader.asmtakes a linked GSPLNK COFF already in RAM, places sections at their header addresses, zeroes.bss, and runs the image as a first-class process viacreate_process(entry, "LOADER", 0x6000, 1, 0). The loader also refuses a target outside a heap free-list block (status 8), which is a real check, not a reservation. - Real hardware.
dwt/loader/hostload.pydrives the whole chain over rdebug:malloc→ poke the COFF → poke the loader → descriptor →create_process→ markers/PNG.harness/c-hello/build.shbuilds a K&R C program for the real unit (ROW_BYTES 256) andbuild.sh emufor MAME (128).dwt/gui/xhello.asmdoesXOpenDisplay→XCreateSimpleWindow→XMapWindow→XSyncfrom a firmware-created process and leaves a real window on the screen.
Address convention in the payload work: Ghidra = 0x7E40000 + file offset,
and firmware bit address = Ghidra × 8.
The heap, and why payload addresses matter
Payloads are poked to fixed scratch addresses at bit 0x01800000 (byte
0x00300000) and above, at the top of the 4 MiB RAM. On a default boot that
region is not allocated — it lies inside the largest free block
(byte 0x12B772..0x400000, ~2.97 MB), so the OS may hand it out. There is a
real allocator and it is callable: malloc at bit 0x3F701230, free at
0x3F702440, a bit-addressed first-fit over three arenas. The global free-byte
counter is itself in bits (bit 0x1E8BA0); on the real unit it reported
3,251,638 bytes free. dwt/heapmap.py maps the free list read-only.
Networking: the LANCE and the BSD stack
harness/mame/NETWORK_REPORT.md is the reference (4,000+ lines, all
measurement and disassembly).
The driver. The LANCE is an AM79C90 at two register windows (RAP bit
0x05900000, RDP bit 0x05800000), with bus-master DMA where LANCE byte B
lands in main-RAM word B >> 1, and its interrupt on INT1. The driver does
not hard-code the registers in its normal paths; it keeps a device struct —
found in a live RAM dump at bit 0x23CBF0. The Ethernet address ROM is
eight bytes, 08 00 2b 16 4b 5a c1 86: the six-byte MAC plus two checksum bytes
the firmware validates.
The stack. It is 4.3/4.4BSD, imported at DEC in October 1989 and descended
from the 386BSD line. Every function entry the report mentions now has a
mapping row: a quoted 4.4BSD counterpart where one exists (leinit, leintr,
ipintr, ip_output, icmp_input, icmp_reflect, icmp_send, tcp_input,
tcp_dooptions, tcp_mss, udp_input, udp_output, rip_input,
in_arpinput, arpresolve, in_pcblookup), otherwise an explicit
firmware-specific row saying why. The coverage check is mechanical, not a spot
check: 225 addressed sites, 143 initially absent from a mapping row, of which 41
sat in 26 distinct entry points — all 26 are now mapped. The honest caveat:
fourteen entries are marked firmware-specific because no counterpart was
found, which is not a proof that none exists.
What a faithful model still owes. C — LANCE TEST (the firmware's internal
loopback) fails on one bit: the test expects RMD1 = 0x6B00, MAME reports
0x4B00 and never sets RMD1_FRAM. It is not an FCS loss — the passing
DTCR=1 case carries a correct FCS, and the failing case is a negative test
that deliberately sends none. M — ETHERNET ROM TEST passes.
One claim needs real silicon. On the real unit, INIT → IDON → STRT works
(CSR0 = 0x1B3, then a stable 0x33), but with a bare-TDMD kick the
transmitter never services the demand (TDMD stays set, the TX descriptor stays
owned at TMD1 = 0x8338). MAME cannot tell the two kicks apart, so this is
deferred until the hardware is back. Note the corrected decode: CSR0 = 0x1BB
has ERR clear; the anomaly is INIT still set while IDON is set.
BOOTP
harness/serve.py --bootp answers the request the terminal broadcasts at
power-up, so its address no longer has to be typed in. Two details worth
keeping: chaddr carries eight bytes (MAC plus the two checksum bytes), and
the request is only visible at L2 — the host IP stack never receives it from
the MAME tap, so the responder is an AF_PACKET socket, while the packet itself
stays an RFC 951 reply. It is off by default.
LAT, and the crash we are chasing
lat/ holds the normative DEC LAT specification (AA-NL26A-TE, June 1989),
the latd implementation and the Wireshark dissector, so the firmware's LAT
subsystem can be read against a spec and against working code.
lat/notes/lat-receive-audit.md audits the receive path. The demultiplexer
walks nbr_slots (wire byte 1) with no length bound, and slots are acted
on: Data_a is delivered, Start/Stop/Reject mutate session state. On a real
unit this produced an idle crash:
LEVEL0 ERROR -> Illegal OPCODE Occurrence - 28
007AA3F0 00200008 00000260 3F9B8EA0
Decoded as bit addresses, only 3F9B8EA0 is in ROM; the rest are RAM. The CPU
was executing out of RAM — the signature of a hijack via memory corruption.
Two candidate triggers were ranked: the LAT receive path, and the font parser.
Replaying the exact 27,529-byte session stream did not reproduce it (twice
over, ~2.4×). The remaining protocol difference is latd's periodic LAT
service announcements (message type 0xA, multicast), 91 of them in the
crashing session, none in the replay. The soak procedure in NEXT_SESSION.md
(20 s heartbeat, watch both screen and wire keepalives) is written up and
waiting on the unit.
Fonts, and two results that correct earlier hopes
- The TFTP "Disk full" abort is not storage. A capture shows the terminal
itself sends TFTP error 3; it is the firmware's
tftp_readbuffer check (bufsize (%d) too small for %d bytes) reporting an incomplete read. The font buffers are in correctly-mapped RAM, so there is no second map overlap. - The
pcfReadBitmapsoverflow is real but unreachable on the TFTP path. The arithmetic is confirmed (nis a full 32-bitpcfReadInt32, no bound check;n=0x10000001→fosAlloc(0x40000004),n=0x40000000→ alloc of 0), but the TFTP font loader recognises the PCF magic and calls a metadata reader that handles PROPERTIES/ACCELERATORS/METRICS/ENCODINGS and silently skips the BITMAPS table. A craftednum_charsis never read there. Seeharness/mame/PCF_VALIDATION_REPORT.md.
The earlier "disk full" symptom was traced to a client-side condition, which is also why a server-side change cannot cause or cure it.
Odd findings and other tidbits
- Every 256 KiB ROM bank starts with an 8-byte header
[checksum16][?][AAAA][5555].tools/vt1200_map.pyverifies them. - The reset vector is identical in v2.1 and v2.2, and points at byte-identical
POST code (
0x3F200040→0x07E40008). - The bank order is reversed: the highest part numbers sit at the lowest addresses. A natural-looking ascending order silently produces garbage.
- The TM's stipple is a mask the display never shows. The raw framebuffer is striped; the glass is white because only the even field is fetched.
- The debug service is documented in the firmware itself — the string is
literally
DEBUG TCP Port, next to the X11 port (0x1770= 6000) in the same descriptor table. - The "closed" rdebug gate was a field-size mistake.
0x8070looked non-zero, so the port looked shut; the compare isSETF 2, a two-bit field, and bits 2..3 are zero. The port was always open. - The challenge has an ordering rule. Send the opcode alone, read and answer
the challenge, then send the handler's arguments. Sent together,
dwt_prfeats the first 16 argument bytes as the answer and the handler reads shifted fields. - The challenge is per-8-byte independent. Changing bytes 0–7 never changes output bytes 8–15 (0/64 single-bit trials), so the transform is blockwise.
- The debug connection writes a message into the Terminal Manager: each
connection adds a
Debug connection from IP host …line to Messages. Five lines in the first real-unit screenshot are five connections, not an error. - The firmware contains a full X11 R4 server and Xlib, so our payloads are
X clients.
XOpenDisplayfrom the rdebug dispatcher never completes; the same code works from a process created bycreate_process, with theDisplay *kept in a callee-saved register. - MAME's address-map precedence is last-registered-wins, not first — which is
why one
VT1200_CALproxy over0x00400000saw zero hits while another over0x00000000fired, and why the "unmapped" log's 123k accesses were not unmapped at all (XCROSS_REPORT.md). The labels were right; the mental model was wrong. - The level-1 VIDEO MEMORY failure is test code 22, a 16-bit walking-bit
check plus a 32-bit pattern over
0x20000units starting at bit0x04000000(CODE22_REPORT.md). - The NVR was invisible because its address was swallowed by the video-memory
mapping; it is at bit
0x6000000and the font path lives there, not the font data. scn2681.cppdoes not exist in the MAME tree — the SCN2681 lives inmc68681.*. Searching for the former walks the whole disk before failing.- A blank LAT window is not a crash.
sleep 3600printed nothing and an idle session is legitimately blank; the connect handshake was identical to the healthy session. Use an output-producing soak. - A capture filter can hide the suspect:
ether host <mac>does not capture multicast. Useether proto 0x6004. - The 1987 TI toolchain still works under DOSBox, and
as34/ld34were ported to 64-bit Linux for assembler-level work.dis34had two encoding bugs, both fixed and round-tripped through GSPA/GSPLNK (harness/sdk-roundtrip/).
Documentation index
| file | what it records |
|---|---|
MEMORY_MAP.md |
the confirmed ROM/RAM/vector map, with per-section confidence |
harness/mame/README.md |
harness devices, env knobs, divergences, regression baseline |
harness/mame/NETWORK_REPORT.md |
LANCE driver + BSD stack mapping, function by function |
harness/mame/GATE_CLEANUP_REPORT.md |
how the working config became the default |
harness/mame/DUART_REPORT.md |
DUART wiring, ISR-driven transmit, datasheet-verified edits |
harness/mame/RENDER_REPORT.md |
the active-pixel scanline renderer and its geometry |
harness/mame/INPUT_REPORT.md |
LK401 + VSXXX-AA, and the cursor evidence |
harness/mame/TABLET_REPORT.md |
the VSXXX-AB tablet and 5-byte absolute reports |
harness/mame/LANCE_REPORT.md |
the LANCE model, address ROM, byte↔bit DMA |
harness/mame/LANCE_LOOPBACK_REPORT.md |
why C — LANCE TEST fails (one bit) |
harness/mame/NVRAM_REPORT.md |
the battery-backed settings RAM |
harness/mame/LEVEL1_CONTINUE_REPORT.md |
what the stub run actually does |
harness/mame/CODE22_REPORT.md |
which test raises VIDEO MEMORY - 22 |
harness/mame/XCROSS_REPORT.md |
MAME address-map precedence, one instrument at a time |
harness/mame/FONT_STORAGE_REPORT.md |
the TFTP "Disk full" is a client-side read check |
harness/mame/PCF_VALIDATION_REPORT.md |
the overflow exists but the TFTP path never reaches it |
harness/mame/HELLO_WORLD_REPORT.md |
the SDK payload, debugger entry only |
harness/mame/FINDINGS.md |
font-parser and ROM-mapping analysis |
harness/mame/BRINGUP_LOG.md |
the bring-up narrative, including withdrawn claims |
harness/BOOTP_REPORT.md |
the BOOTP responder and why it is L2 |
dwt/notes/rdebug-static.md |
the debug TCP service, statically |
dwt/notes/CHALLENGE_REVERSE.md |
the 16-byte challenge, solved |
dwt/notes/REAL_UNIT.md |
first contact, geometry, corrections |
dwt/notes/GUI_APPS.md |
the X11 architecture and what a payload can do |
dwt/notes/LOADER.md |
the COFF loader and create_process |
dwt/notes/TCPIP.md |
the socket API and the TFTP fetch |
dwt/notes/HEAP.md |
the allocator and where payloads live |
dwt/notes/MESSAGEBOX.md |
the bottom diagnostics box and its status mask |
lat/notes/lat-receive-audit.md |
the LAT receive path, finding by finding |
reference/level0-10byte-puzzle.md |
why level 0 cannot be satisfied faithfully |
reference/level1-test-sequence.md |
the 15 level-1 tests and their codes |
reference/third-party-code.md |
what open-source code is in the ROM |
reference/tms34010-sdk.md |
the 1987 toolchain, buildable today |
EXPLOIT_NOTES.md |
the original code-execution audit and the crash decode |
NEXT_SESSION.md |
the LAT soak handoff (older; see the reports for later work) |
Repository layout
vt1200/
├── v2.1/ v2.2/ # original 28 EPROM dumps
├── reconstruct.py # de-interleave -> address-ordered image
├── vt1200_v21.bin vt1200_v22.bin
├── MEMORY_MAP.md # the confirmed memory map
├── ghidra/ ghidra_scripts/ # Ghidra project + Verify/Stats/FixPtr/Assert
├── tools/
│ ├── vt1200_map.py # vectors + bank-header checksums
│ ├── ptr_constants.py # find/resolve bit-address constants
│ ├── smart34010/ # dis34 (two encoding fixes)
│ ├── as34/ # TMS34010 binutils, ported to 64-bit Linux
│ ├── tms34010-sdk/ # the 1987 TI SDK (under DOSBox)
│ └── ghidra-tms34010/ # patched processor module + install script
├── harness/
│ ├── mame/ # the MAME driver, Lua probes, reports
│ ├── hello-world/ # 82-byte SDK payload (debugger entry)
│ ├── c-hello/ # first C program, runs on real hardware
│ ├── udpstream/ # UDP/TCP receive-throughput payloads
│ ├── sdk-roundtrip/ # dis34 fixes vs. the real assembler
│ └── serve.py # TFTP (+ optional BOOTP) server
├── dwt/ # real-unit debug client and notes
│ ├── challenge.py exploit.py # the challenge and the rdebug client
│ ├── loader/ # COFF loader + hostload over rdebug
│ ├── gui/ # injected X11 window
│ ├── tcpip/ # socket API payloads
│ └── notes/ # the real-unit investigation
├── lat/ # LAT spec, implementations, audit, probes
├── reference/ # datasheets, manuals, disassembly, boot video
├── docs/ # DEC/TI manuals (PDF + .txt), wiki drafts
└── images/ # real-unit photographs
Ground rules
The project has five hard-won rules, written down in
harness/mame/README.md: search with an explicit path (never from /), stage
only your own paths (never git add -A), one sample is not evidence, any change
to MAME needs a quote and a second reader, and do not fake it — model what
the documentation supports, report the gap, and make any stand-in opt-in and
named for what it is.
That last rule is why the faithful configuration is the default, why
VT1200_IO_PATH_STUB is off and described as a test fixture rather than a
keyboard, and why harness/mame/BRINGUP_LOG.md keeps its withdrawn claims.