# PD01D221 firmware analysis (`tms27pc512.BIN`) Reverse-engineering notes for the dumped controller firmware — the 64 KB TI TMS27PC512 EPROM (U3) from the Babcock PD01D221. This is the authoritative source for the display's command set, and it feeds both [vPLASMA](../src/VPlasma.App/) and the planned [hardware replica](README.md). Dump: `tms27pc512.BIN`, 65,536 bytes, MD5 `b775427806857f60ca4a4cc501f4b5cc`. Analysis tooling: [`hc11dis.py`](hc11dis.py) (a purpose-built 68HC11 disassembler — the toolchain has no m68hc11 target). ## Memory map - **CPU $8000–$FFFF = EPROM upper 32 KB, 1:1** (the HC11 vector table lands at ROM offset `$FFC0–$FFFF` and is valid, which pins the mapping). The EPROM's **lower 32 KB is unused** (all `$00`) — only A15-high is decoded to the ROM. - **Code:** `$9000–$B8xx`. **Data/tables:** `$8000–$8FFF` (demo), `$98AC+` (dispatch tables), `$BC03+` (font descriptors), `$C000–$DFFF` (glyph bitmaps + graphics). - **RAM (Mosel MS62256, 32 KB) at low addresses:** HC11 registers on page 0 (`$00–$3F`; SCSR=`$2E`, SCDR=`$2F`), zero-page variables `$40–$FF`, RX ring buffer at `$0228`, and **ten 128×32 screen buffers** from `$0F6D` up. ## Vectors | Vector | Target | Notes | |--------|--------|-------| | RESET | `$9059` | Init: registers, stack `$0227`, then main loop | | **SCI (serial rx)** | **`$B85C`** | Interrupt-driven receive → ring buffer | | COP watchdog | `$905E` | (re-inits) | | others | `$9059` | default → reset | ## Architecture 1. **SCI RX ISR (`$B85C`)** — on RDRF, reads `SCSR`/`SCDR`, stores the byte to a ring buffer at `$0228` (write ptr `$0228`, count `$022C`). No parsing here. 2. **Main-loop parser (`$B7E0–$B859`)** — pulls buffered bytes and runs an `ESC`-state machine (flag `$AF`: bit `$10` = ESC seen, bit `$20` = operand pending). Dispatch is **table-driven** (below). Printable chars in the current font's `[first,last]` range (`$62`/`$63`) go to the character renderer (`$9648`), which **enqueues** a glyph to a deferred rasterizer. 3. **Ten double-buffered screens** — descriptor table at **`$A4E0`** (10 × 6 bytes): each screen has a **draw** pointer (`$BE`) and a **display** pointer (`$BC`) into SRAM, 1 KB apart. `ESC I` sets the draw target, `ESC i` sets what's scanned to the glass → **page-flipping / double-buffering**. ## Command dispatch Two jump tables, indexed by byte: - **`ESC` + letter → command table at `$98AC`.** Valid letters `0x30`–`0x7E`; index = `letter − 0x30`; null entry = ignored. **58 commands populated.** - **Control bytes `0x08`–`0x14` → table at `$994C`.** index = `byte − 0x08`. Most command handlers share a prologue: first sighting of the letter sets the "operand pending" flag and returns; the **next byte is the 1-byte operand** (in `$C6`). Multi-operand commands (`ESC P/X/Y`) collect into a parameter block at `$0070`. ### Control characters (`$994C`) | Byte | Handler | Meaning | |------|---------|---------| | `0x08` BS | `$99AF` | cursor left | | `0x09` HT | `$9966` | tab | | `0x0A` LF | `$99F3` | line feed | | `0x0B` VT | `$9A30` | vertical tab | | `0x0D` CR | `$9A55` | carriage return | | `0x11`–`0x14` DC1–DC4 | `$9A5C`/`$9B34`/`$9C09`/`$9CFC` | device controls (TBD) | | `0x0C` FF, `0x0E`–`0x10` | — | no handler | ### ESC commands (`$98AC`) — confirmed semantics | Cmd | Handler | Meaning | |-----|---------|---------| | `ESC @` | `$9F26` | **Clear** the active draw buffer (512 bytes = 128×32÷8) | | `ESC G n` | `$A42B` | **Cursor mode**, low nibble of `$B4` (n = 0–7) | | `ESC H n` | `$A44C` | **Text attributes**, low 4 bits of `$B1` (intensity/underline/reverse/flash) | | `ESC K n` | `$A3EA` | **Font select** (n = 0–9; 8 real fonts) | | `ESC L` | `$A556` | **Home** cursor (X=0, Y=0) | | `ESC Q n` | `$A51C` | **Set cursor row Y** (range-checked 0–31) | | `ESC R n` | `$A539` | **Set cursor column X** (range-checked 0–127) | | `ESC I n` | `$A473` | **Select DRAW page** 0–9 (sets `$BE` from `$A4E0` table) | | `ESC i n` | `$A4A2` | **Select DISPLAY page** 0–9 (page-flip; sets `$BC`) | | `ESC P …` | `$AAF1` | **Graphics bitmap write** (multi-operand: screen,y,x,w,h,data) | | `ESC A`–`ESC F` | `$9FB6`–`$A13C` | **Vector/graphics primitives** (line/point/move; pen state `$B6`, coords `$58/$59`, line routine `$A16C`) | | `ESC X n` | `$A748` | **Set graphics pen X** (multi-op, 0–127) | | `ESC Y n` | `$A644` | **Set graphics pen Y** (multi-op) | | `ESC J` | `$A4D4` | **Toggle** mode bit `$B7.7` (orientation/display — TBD) | ### ESC commands — populated but not yet decoded `ESC 0`–`9` (`$9E27`+, set continuations — likely custom-char / numeric entry), `ESC : ; =` , `ESC < > W w _` (cluster `$AEBA–$AF00`), `ESC B C D E F` variants, `ESC M N O` (`$A5BD/$A5C5/$A5E0`), `ESC Z ^ z ~` (cluster `$AC73–$ACA1`), `ESC a`–`f`, `ESC h l n p q r x`. ~30 handlers remain to label — full list with addresses is dumped by the tooling below. ## Fonts Font-pointer table at **`$BC03`** (10 slots) → 12-byte descriptors. **8 real fonts** (slots 8–9 are junk pointers, matching the demo's "8 STORED CHARACTER FONTS"): | Font | First–Last | W×H | Notes | |------|-----------|-----|-------| | 0 | `0x20`–`0xFF` | 6×8 | base font, full range | | 1 | `0x40`–`0x7F` | 6×8 | uppercase-only | | 2 | `0x20`–`0xFF` | 6×10 | | | 3 | `0x40`–`0x7F` | 6×10 | | | 4 | `0x20`–`0x7F` | 12×16 | large | | 5 | `0x20`–`0x7F` | 12×20 | largest | | 6 | `0x20`–`0xFF` | 7×10 | | | 7 | `0x40`–`0x7F` | 7×10 | | Glyph bitmaps live in ROM (`~$C000–$DFFF`). **Exact glyph base + encoding pending** — the renderer at `$9648` enqueues to a deferred rasterizer; tracing that (or brute-forcing the 'A' pattern at the known stride) will extract the real glyphs to replace vPLASMA's public-domain 5×7 stand-in. ## Demo program Enabled by **jumper 6** (PD3) — confirms the [JP1 map](README.md). A 10-screen scripted demo; the pointer table at `$8000` (10 × 4-byte entries) points to each screen, and every screen is `[2-byte count][command stream]`. The player at `$BB60`/`$BBA4` loops the screens, feeding each byte through the command parser. **Extracted verbatim** into `src/VPlasma.Core/Device/PlasmaFirmwareDemo.cs` (all 10 screens as raw wire bytes); the standalone app replays it on jumper 6. Commands used: `@ G I K L Q R Z i` + text. `ESC I`/`ESC i` (draw/display page) are consumed by vPLASMA but not acted on (single-page); `ESC Z` (a rarely-used animation command, one all-zero use in screen 9) is left unimplemented. ## What this means **For vPLASMA (folded in 2026-07-16):** the recovered spec replaced the guessed behavior. vPLASMA now uses the **8 real ROM fonts** (extracted to `src/VPlasma.Core/Device/PlasmaFonts.cs`), a **pixel-addressed cursor** with the real `ESC Q` (row) / `ESC R` (column) positioning, `ESC K` 0–7 font select, and `ESC H` attributes as the low 4 bits. The standalone app also implements the functional JP1 jumpers — baud (1+2), **orientation (4: horizontal 128×32 / vertical 32×128)**, **display test (5: all-dot pattern)**, and demo (6). Verified: 27 unit tests + the self-test pages render the real glyphs. Still deferred (documented, single-page model retained): the 10 double-buffered pages (`ESC I`/`ESC i`) and the vector-graphics primitives (`ESC A`–`F`). **For the replica:** this *is* the spec. The firmware confirms a clean model — a byte-stream command parser, a 512-byte-per-page frame buffer, 10 pages with page-flip, 8 fonts, text attributes as 4 flags, plus vector graphics. All of it ports directly onto a modern MCU. The one artifact still to extract is the glyph bitmaps. ## Reproduce ```sh python hc11dis.py # disassemble from a CPU address # e.g. python hc11dis.py B7E0 70 # the command parser ``` Command/control tables are at `$98AC` / `$994C`; font table `$BC03`; screen table `$A4E0`.