Move PlasmaNew to the TeslaRel410 repository

As a hardware replication project it belongs with the other hardware
restoration efforts: now at restoration/PlasmaNew in TeslaRel410
(imported there as 4391ddd3, including the uncommitted button-macro fix
and power-on cursor rendering). Doc references updated to point at the
new home. vPLASMA (src/VPlasma.Core) and tools/VPlasma.Wire remain here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-22 17:21:25 -05:00
co-authored by Claude Fable 5
parent 0ed6d9495e
commit a048b7c69b
20 changed files with 7 additions and 2264 deletions
-160
View File
@@ -1,160 +0,0 @@
# 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` DC1DC4 | `$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 = 07) |
| `ESC H n` | `$A44C` | **Text attributes**, low 4 bits of `$B1` (intensity/underline/reverse/flash) |
| `ESC K n` | `$A3EA` | **Font select** (n = 09; 8 real fonts) |
| `ESC L` | `$A556` | **Home** cursor (X=0, Y=0) |
| `ESC Q n` | `$A51C` | **Set cursor row Y** (range-checked 031) |
| `ESC R n` | `$A539` | **Set cursor column X** (range-checked 0127) |
| `ESC I n` | `$A473` | **Select DRAW page** 09 (sets `$BE` from `$A4E0` table) |
| `ESC i n` | `$A4A2` | **Select DISPLAY page** 09 (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, 0127) |
| `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 89 are junk pointers, matching the demo's "8 STORED CHARACTER
FONTS"):
| Font | FirstLast | 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` 07 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 <hexaddr> <count> # 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`.
Binary file not shown.
-211
View File
@@ -1,211 +0,0 @@
# PlasmaNew — reverse-engineering the real cockpit plasma display
Working notes and reference material for the cockpit plasma display.
**End goal: a hardware replica.** The original Babcock plasma panels are
starting to fail and are effectively irreplaceable. The plan is to drive a
modern **128 × 32 LED array** with a **modern microcontroller** that reads
the same RS-232 serial bus and speaks the same command protocol as the
original PD01D221 — a drop-in replacement, functionally identical from the
host's side, with none of the plasma physics or high voltage.
[vPLASMA](../src/VPlasma.App/) (the C# app in this repo) is the software
counterpart and serves the replica directly: it is an **executable
specification** of the display's behavior and a **test oracle**. Every
command semantic pinned down in `VPlasmaDevice` ports straight to the
replica's firmware, and the same differential-test rig (real panel vs.
vPLASMA) validates the replica. vPLASMA today is built from *observed
traffic* (the game's driver + a factory test tool); grounding it in the
*actual hardware* — protocol, fonts, and timing — feeds both the emulator
and the replacement firmware.
## What the display is
A **commercial off-the-shelf Babcock Display Products Division PD01D221**
"128 × 32 dot-matrix, gas-plasma display with controller and DC-DC
converter," with an RS-232C serial interface and a dedicated microprocessor
for refresh and the user interface. Built by **Cherry** (PCB assembly
**4317-C**, Made in Taiwan, © 1994). See [`PD01D221.pdf`](PD01D221.pdf)
(Babcock doc 9200-0109 Rev A).
Product family (the suffix letter = how much is on the board):
| Model | Contents |
|-------|----------|
| PD01**B**22B | 128×32 panel + driver electronics only (host refreshes it) |
| PD01**F**221 | + on-board DC-DC converter |
| PD01**D**221 | **+ controller: RS-232C, dedicated microprocessor** ← this unit |
**VWE used it stock — no custom fonts or bitmaps were installed.** So the
display's behavior is entirely the standard Babcock PD-series firmware, and
the `ESC P` "graphics" the game drew were rendered at runtime by the game,
not preloaded. Nothing on the display is VWE-specific.
## Board inventory
Chip IDs read from the photos below.
| Ref | Part | Role |
|-----|------|------|
| U1 | **Motorola MC68HC11D0** (44-pin QFP, mask 1C17F, wk 28/94) | ROMless HC11 MCU — the controller. Runs from external bus in expanded mode. |
| U3 | **TI TMS27PC512** (PLCC-32, 150 ns, Singapore) | **64 KB OTP EPROM = the firmware** (stock Babcock code + fonts). Standard 27C512. |
| U2 | QFP ~100-pin, label **"35GWP004 REV A 3994"** | Custom Cherry display/scan **ASIC** (wk 39/94). Drives the HV stage. *Not* the firmware. |
| U4 | **Mosel MS62256L-10** | 32 KB SRAM — frame buffer / scratch. |
| U7 | **Supertex HV7708** | 32-channel high-voltage plasma driver (more HV off-frame). |
| U5 | **Maxim MAX202CWE** | RS-232 transceiver — the serial interface. |
| — | **MAX707** | Reset / watchdog supervisor. |
| Y1 | **7.3728 MHz** crystal | E-clock = 1.8432 MHz; gives exact standard baud rates. |
Memory picture: ROMless HC11 + external 64 KB EPROM (code + fonts) + 32 KB
SRAM + custom scan ASIC + HV drivers. A 64 KB program EPROM for a 128×32
panel implies far more feature set than the game ever used.
## Reference photos
| File | Shows |
|------|-------|
| [`mpul-2026-07-07-152834.jpeg`](mpul-2026-07-07-152834.jpeg) | Controller overview: MC68HC11D0 (U1), the "35GWP004" ASIC (U2), HV7708 (U7), MAX202, MAX707. |
| [`silkscreenl-2026-07-07-152841.jpeg`](silkscreenl-2026-07-07-152841.jpeg) | Cherry silkscreen: PCB **4317-C**, © 1994, "Made in Taiwan". |
| [`unknown-2026-07-07-153818.jpeg`](unknown-2026-07-07-153818.jpeg) | The **TMS27PC512 EPROM** (U3, initially unidentified), Mosel SRAM (U4), HC11. |
| [`jumpers-2026-07-07-163733.jpeg`](jumpers-2026-07-07-163733.jpeg) | The **JP1** config header next to the HC11. |
## Datasheet-confirmed facts (`PD01D221.pdf`, doc 9200-0109 Rev A)
- Serial format **8N1**, baud **jumper-selectable 4800 / 9600 / 19.2K /
38.4K** (the game uses 9600).
- "Choice of standard fonts and styles" (= `ESC K` / `ESC H`); "program
custom characters" (a custom-char download command — **exists but VWE
didn't use it**); "graphic input commands / overlays" (= `ESC P`).
- Serial is **bidirectional**. Connector **J1**: pin 2 TxD (display→host),
pin 3 RxD (host→display), pin 4 CTS, pin 8 DTR ("display ready"), pin 5
GND. The game drove it write-only (flow control disabled, TxD ignored),
so vPLASMA's listen-only model is faithful.
- Also carries an 8-bit **parallel** port (J2), unused by the game.
- **The datasheet does *not* contain the `ESC` command table.** That's a
separate Babcock programming/user manual, which is **not available online**
(checked general web, datasheetarchive, bitsavers, archive.org, resellers;
only this datasheet was ever digitized). Sources for it: ask Babcock
directly (La Mirada CA, (714) 994-6500, babcockinc.com), or reconstruct it
from the dump + the sources we already have.
## Command protocol recovered so far
From the game driver (`TeslaRel410\CODE\RP\MUNGA_L4\L4PLASMA.CPP`) and the
factory test tool (`…\VWETEST\VGLTEST\PLASMA.EXE`). Full grammar lives in
[`../src/VPlasma.Core/Protocol/PlasmaProtocol.cs`](../src/VPlasma.Core/Protocol/PlasmaProtocol.cs).
| Bytes | Meaning |
|-------|---------|
| `ESC @` | Clear screen, reset text state |
| `ESC L` | Home cursor |
| `ESC G n` | Cursor mode (00/FF hidden, 01 steady, 03 flashing) |
| `ESC K n` | Font select (07; FF = default) |
| `ESC H n` | Text attributes (intensity / underline / reverse / flash) |
| `ESC P s y x w h data…` | Graphics write: MSB = leftmost pixel |
| BS / HT / LF / VT / CR | Cursor motion |
The Babcock manual (or a firmware dump) would fill in exact operand
encodings, tab stops, the `ESC P` "screen" byte, and any commands the game
never used.
## JP1 configuration header
Traced pin-by-pin (see the jumper photo). **JP1 is firmware-read
configuration, not CPU mode select** — each shunt ties a GP port pin the
firmware polls at boot. Shunt to GND = logic 0.
All decoded from the firmware's boot config routine at `$9190` (installed =
grounded = logic 0):
| JP1 pos | HC11 pin | Function | Firmware |
|---------|----------|----------|----------|
| 1 | pin 24 / PA0 | Baud select bit 0 | `$9193` → SCI baud reg `$2B` |
| 2 | pin 22 / PA2 | Baud select bit 1 | `$9197` → SCI baud reg `$2B` |
| 3 | pin 21 / PA3 | HW config line | `$91CA` sets flag `$B7.2`, which drives output PA5 to a fixed level (board control line; exact effect board-dependent) |
| 4 | pin 15 / PD5 | Display orientation | `$90AE`: installed = horizontal 128×32, removed = vertical 32×128 (`$B3.0`; the demo's "HORIZONTAL OR VERTICAL ORIENTATION") |
| 5 | pin 14 / PD4 | Display/pixel test | `$91C3` installed → `$B888` writes a walking-bit test pattern (dead-dot check) |
| 6 | pin 13 / PD3 | Demonstration program | `$91DB` installed → `$BB60` runs the built-in demo |
| 7 | J2 SEL → +5 V | Parallel interface select | (board interface mux) |
Baud straps 1+2 pick SCI baud register (`$2B`) values `$13`/`$11`/`$12`/`$10`
= 4800 / 9600 / 19.2K / 38.4K (more zeros grounded ⇒ slower).
HC11 pin map cross-checked while tracing: PD0PD5 = pins 1015, PA0PA7 =
pins 2417 (descending).
**MODA/MODB are hardwired high (expanded mode) through a diode to +5 V — not
jumper-selectable.** So bootstrap mode cannot be entered by moving a jumper;
it needs a mode-pin override. (Exact diode circuit still to be characterized.)
## Firmware-dump plan
Goal: get the 64 KB EPROM image, disassemble the HC11 code to recover the
full command table + font bitmaps + timing, then differential-test vPLASMA
against the real panel on identical byte streams. The recovered spec feeds
**both** vPLASMA and the replacement firmware.
1. **Free, no-solder — hunt for a diagnostic mode.** Capture J1 TxD while
power-cycling normally (may emit a banner/version), then step the four
unknown config jumpers (PA3, PD5, PD4, PD3) through combinations watching
TxD for a factory self-test or ROM dump.
2. **Serial bootstrap (conditional).** Bootstrap needs MODA = MODB = 0 at the
reset edge; they're pulled to +5 V via a diode. If that circuit has a
series resistor (or a diode-OR node), pull both low during a reset pulse
and run the standard **Motorola AN1060** dump loader out J1 — no cutting.
If hard-tied, a single trace cut/lift is needed. *Blocked on the diode
details.*
3. **Reliable fallback — read the EPROM directly.** PLCC-32 test clip on U3
with the HC11 held in reset, or hot-air U3 off and read it in a 27C512
adapter. Guaranteed image.
Safety: the panel runs on a few hundred volts from the on-board DC-DC. Keep
all work in the logic corner (HC11 / EPROM / MAX202); never probe the HV
section or the panel connector while powered.
## Open items
- Characterize the MODA/MODB diode circuit → decide if serial bootstrap is a
tack-a-wire job or needs a trace cut.
- Capture J1 TxD across config-jumper combinations (path 1).
- Obtain the Babcock PD01D programming manual, **or** dump the U3 EPROM.
- Once we have the command table + fonts: fold into `VPlasmaDevice`, replace
the public-domain 5×7 stand-in with the real Babcock glyphs, and
differential-test against the hardware.
- **Prototype the replica.** A modern MCU (RP2040 / ESP32 / Teensy) reads the
command stream into the same command parser and drives a 128×32 LED matrix
from the same frame buffer — the per-pixel lit / half-intensity / flash
flags in `VPlasmaDevice` map directly onto PWM brightness + blink. An amber
matrix best mimics the neon-orange plasma; for a true cockpit swap, match
the original active area (~12.75" × 3.15", ~0.1" pitch = 128×32).
## Replica interface — USB, not RS-232
The cockpit PCs are now **Win x64**, so the replica likely needs **no real
serial port**: a native-USB MCU presenting as a **USB CDC virtual COM port**
is transparent — the host opens `COMx` and can't tell it isn't a UART. This
deletes the RS-232 transceiver and connector from the BOM. Consequences:
- **Baud is cosmetic** over USB CDC (the 9600/… setting is accepted as a
no-op; the two baud-select jumpers need no hardware equivalent).
- **Timing becomes instant** rather than ~1 ms/byte — harmless for a display,
and vPLASMA can still throttle to mimic the original for differential tests.
- **Pin the COM number** the host expects (original was COM2) in Device
Manager so it drops in with no host-side config change.
- **DTR/RTS still cross** the CDC link if any host logic ever needs them (the
game didn't use flow control).
- **Power gotcha:** USB alone can't drive the LED array at full brightness —
use USB for data + a **separate DC feed** for the LEDs (or USB-C PD).
Transparency assumes the host reaches the display as a Windows `COMx`
endpoint — e.g. DOSBox-X `serial2=directserial realport:COMx`, which a USB
CDC port satisfies perfectly. Confirm the current drive path.
## Status
**Firmware dump received — analysis underway.** The U3 EPROM was dumped
(`tms27pc512.BIN`); see [`FIRMWARE.md`](FIRMWARE.md) for the disassembly
findings: memory map, the full command dispatch tables (58 `ESC` commands +
control chars), the 10-page double-buffered architecture, 8 fonts, and
decoded command semantics. Remaining: extract the glyph bitmaps and label the
~30 not-yet-decoded commands. The Babcock programming manual is no longer on
the critical path — the firmware is the authoritative spec.
-148
View File
@@ -1,148 +0,0 @@
#!/usr/bin/env python3
"""Minimal-but-practical Motorola 68HC11 disassembler for the PD01D221 ROM.
CPU address == ROM offset for $8000-$FFFF (EPROM upper 32KB maps 1:1)."""
import sys
# addressing modes and their extra operand length (beyond opcode)
INH='inh'; IMM8='imm8'; IMM16='imm16'; DIR='dir'; EXT='ext'; IDX='idx'; IDY='idy'; REL='rel'
BITDIR='bitdir'; BITIDX='bitidx'; BITIDY='bitidy' # BSET/BCLR (mask)
BRDIR='brdir'; BRIDX='bridx'; BRIDY='bridy' # BRSET/BRCLR (mask+rel)
# page 0
P0 = {
0x00:('TEST',INH),0x01:('NOP',INH),0x02:('IDIV',INH),0x03:('FDIV',INH),
0x04:('LSRD',INH),0x05:('ASLD',INH),0x06:('TAP',INH),0x07:('TPA',INH),
0x08:('INX',INH),0x09:('DEX',INH),0x0A:('CLV',INH),0x0B:('SEV',INH),
0x0C:('CLC',INH),0x0D:('SEC',INH),0x0E:('CLI',INH),0x0F:('SEI',INH),
0x10:('SBA',INH),0x11:('CBA',INH),0x12:('BRSET',BRDIR),0x13:('BRCLR',BRDIR),
0x14:('BSET',BITDIR),0x15:('BCLR',BITDIR),0x16:('TAB',INH),0x17:('TBA',INH),
0x19:('DAA',INH),0x1B:('ABA',INH),
0x1C:('BSET',BITIDX),0x1D:('BCLR',BITIDX),0x1E:('BRSET',BRIDX),0x1F:('BRCLR',BRIDX),
0x20:('BRA',REL),0x21:('BRN',REL),0x22:('BHI',REL),0x23:('BLS',REL),
0x24:('BCC',REL),0x25:('BCS',REL),0x26:('BNE',REL),0x27:('BEQ',REL),
0x28:('BVC',REL),0x29:('BVS',REL),0x2A:('BPL',REL),0x2B:('BMI',REL),
0x2C:('BGE',REL),0x2D:('BLT',REL),0x2E:('BGT',REL),0x2F:('BLE',REL),
0x30:('TSX',INH),0x31:('INS',INH),0x32:('PULA',INH),0x33:('PULB',INH),
0x34:('DES',INH),0x35:('TXS',INH),0x36:('PSHA',INH),0x37:('PSHB',INH),
0x38:('PULX',INH),0x39:('RTS',INH),0x3A:('ABX',INH),0x3B:('RTI',INH),
0x3C:('PSHX',INH),0x3D:('MUL',INH),0x3E:('WAI',INH),0x3F:('SWI',INH),
0x40:('NEGA',INH),0x43:('COMA',INH),0x44:('LSRA',INH),0x46:('RORA',INH),
0x47:('ASRA',INH),0x48:('ASLA',INH),0x49:('ROLA',INH),0x4A:('DECA',INH),
0x4C:('INCA',INH),0x4D:('TSTA',INH),0x4F:('CLRA',INH),
0x50:('NEGB',INH),0x53:('COMB',INH),0x54:('LSRB',INH),0x56:('RORB',INH),
0x57:('ASRB',INH),0x58:('ASLB',INH),0x59:('ROLB',INH),0x5A:('DECB',INH),
0x5C:('INCB',INH),0x5D:('TSTB',INH),0x5F:('CLRB',INH),
0x60:('NEG',IDX),0x63:('COM',IDX),0x64:('LSR',IDX),0x66:('ROR',IDX),
0x67:('ASR',IDX),0x68:('ASL',IDX),0x69:('ROL',IDX),0x6A:('DEC',IDX),
0x6C:('INC',IDX),0x6D:('TST',IDX),0x6E:('JMP',IDX),0x6F:('CLR',IDX),
0x70:('NEG',EXT),0x73:('COM',EXT),0x74:('LSR',EXT),0x76:('ROR',EXT),
0x77:('ASR',EXT),0x78:('ASL',EXT),0x79:('ROL',EXT),0x7A:('DEC',EXT),
0x7C:('INC',EXT),0x7D:('TST',EXT),0x7E:('JMP',EXT),0x7F:('CLR',EXT),
0x80:('SUBA',IMM8),0x81:('CMPA',IMM8),0x82:('SBCA',IMM8),0x83:('SUBD',IMM16),
0x84:('ANDA',IMM8),0x85:('BITA',IMM8),0x86:('LDAA',IMM8),0x88:('EORA',IMM8),
0x89:('ADCA',IMM8),0x8A:('ORAA',IMM8),0x8B:('ADDA',IMM8),0x8C:('CPX',IMM16),
0x8D:('BSR',REL),0x8E:('LDS',IMM16),0x8F:('XGDX',INH),
0x90:('SUBA',DIR),0x91:('CMPA',DIR),0x92:('SBCA',DIR),0x93:('SUBD',DIR),
0x94:('ANDA',DIR),0x95:('BITA',DIR),0x96:('LDAA',DIR),0x97:('STAA',DIR),
0x98:('EORA',DIR),0x99:('ADCA',DIR),0x9A:('ORAA',DIR),0x9B:('ADDA',DIR),
0x9C:('CPX',DIR),0x9D:('JSR',DIR),0x9E:('LDS',DIR),0x9F:('STS',DIR),
0xA0:('SUBA',IDX),0xA1:('CMPA',IDX),0xA2:('SBCA',IDX),0xA3:('SUBD',IDX),
0xA4:('ANDA',IDX),0xA5:('BITA',IDX),0xA6:('LDAA',IDX),0xA7:('STAA',IDX),
0xA8:('EORA',IDX),0xA9:('ADCA',IDX),0xAA:('ORAA',IDX),0xAB:('ADDA',IDX),
0xAC:('CPX',IDX),0xAD:('JSR',IDX),0xAE:('LDS',IDX),0xAF:('STS',IDX),
0xB0:('SUBA',EXT),0xB1:('CMPA',EXT),0xB2:('SBCA',EXT),0xB3:('SUBD',EXT),
0xB4:('ANDA',EXT),0xB5:('BITA',EXT),0xB6:('LDAA',EXT),0xB7:('STAA',EXT),
0xB8:('EORA',EXT),0xB9:('ADCA',EXT),0xBA:('ORAA',EXT),0xBB:('ADDA',EXT),
0xBC:('CPX',EXT),0xBD:('JSR',EXT),0xBE:('LDS',EXT),0xBF:('STS',EXT),
0xC0:('SUBB',IMM8),0xC1:('CMPB',IMM8),0xC2:('SBCB',IMM8),0xC3:('ADDD',IMM16),
0xC4:('ANDB',IMM8),0xC5:('BITB',IMM8),0xC6:('LDAB',IMM8),0xC8:('EORB',IMM8),
0xC9:('ADCB',IMM8),0xCA:('ORAB',IMM8),0xCB:('ADDB',IMM8),0xCC:('LDD',IMM16),
0xCE:('LDX',IMM16),0xCF:('STOP',INH),
0xD0:('SUBB',DIR),0xD1:('CMPB',DIR),0xD2:('SBCB',DIR),0xD3:('ADDD',DIR),
0xD4:('ANDB',DIR),0xD5:('BITB',DIR),0xD6:('LDAB',DIR),0xD7:('STAB',DIR),
0xD8:('EORB',DIR),0xD9:('ADCB',DIR),0xDA:('ORAB',DIR),0xDB:('ADDB',DIR),
0xDC:('LDD',DIR),0xDD:('STD',DIR),0xDE:('LDX',DIR),0xDF:('STX',DIR),
0xE0:('SUBB',IDX),0xE1:('CMPB',IDX),0xE2:('SBCB',IDX),0xE3:('ADDD',IDX),
0xE4:('ANDB',IDX),0xE5:('BITB',IDX),0xE6:('LDAB',IDX),0xE7:('STAB',IDX),
0xE8:('EORB',IDX),0xE9:('ADCB',IDX),0xEA:('ORAB',IDX),0xEB:('ADDB',IDX),
0xEC:('LDD',IDX),0xED:('STD',IDX),0xEE:('LDX',IDX),0xEF:('STX',IDX),
0xF0:('SUBB',EXT),0xF1:('CMPB',EXT),0xF2:('SBCB',EXT),0xF3:('ADDD',EXT),
0xF4:('ANDB',EXT),0xF5:('BITB',EXT),0xF6:('LDAB',EXT),0xF7:('STAB',EXT),
0xF8:('EORB',EXT),0xF9:('ADCB',EXT),0xFA:('ORAB',EXT),0xFB:('ADDB',EXT),
0xFC:('LDD',EXT),0xFD:('STD',EXT),0xFE:('LDX',EXT),0xFF:('STX',EXT),
}
# page 1 ($18): Y versions
P1 = {
0x08:('INY',INH),0x09:('DEY',INH),0x1C:('BSET',BITIDY),0x1D:('BCLR',BITIDY),
0x1E:('BRSET',BRIDY),0x1F:('BRCLR',BRIDY),0x30:('TSY',INH),0x35:('TYS',INH),
0x38:('PULY',INH),0x3A:('ABY',INH),0x3C:('PSHY',INH),
0x60:('NEG',IDY),0x63:('COM',IDY),0x64:('LSR',IDY),0x66:('ROR',IDY),
0x67:('ASR',IDY),0x68:('ASL',IDY),0x69:('ROL',IDY),0x6A:('DEC',IDY),
0x6C:('INC',IDY),0x6D:('TST',IDY),0x6E:('JMP',IDY),0x6F:('CLR',IDY),
0x8C:('CPY',IMM16),0x8F:('XGDY',INH),0x9C:('CPY',DIR),
0xA0:('SUBA',IDY),0xA1:('CMPA',IDY),0xA2:('SBCA',IDY),0xA3:('SUBD',IDY),
0xA4:('ANDA',IDY),0xA5:('BITA',IDY),0xA6:('LDAA',IDY),0xA7:('STAA',IDY),
0xA8:('EORA',IDY),0xA9:('ADCA',IDY),0xAA:('ORAA',IDY),0xAB:('ADDA',IDY),
0xAC:('CPY',IDY),0xAD:('JSR',IDY),0xAE:('LDS',IDY),0xAF:('STS',IDY),
0xBC:('CPY',EXT),0xCE:('LDY',IMM16),0xDE:('LDY',DIR),0xDF:('STY',DIR),
0xE0:('SUBB',IDY),0xE1:('CMPB',IDY),0xE2:('SBCB',IDY),0xE3:('ADDD',IDY),
0xE4:('ANDB',IDY),0xE5:('BITB',IDY),0xE6:('LDAB',IDY),0xE7:('STAB',IDY),
0xE8:('EORB',IDY),0xE9:('ADCB',IDY),0xEA:('ORAB',IDY),0xEB:('ADDB',IDY),
0xEC:('LDD',IDY),0xED:('STD',IDY),0xEE:('LDY',IDY),0xEF:('STY',IDY),
0xFE:('LDY',EXT),0xFF:('STY',EXT),
}
# page 2 ($1A): CPD, and X/Y cross indexed
P2 = {0x83:('CPD',IMM16),0x93:('CPD',DIR),0xA3:('CPD',IDX),0xB3:('CPD',EXT),
0xAC:('CPY',IDX),0xEE:('LDY',IDX),0xEF:('STY',IDX)}
# page 4 ($CD): X/Y cross indexed
P4 = {0xA3:('CPD',IDY),0xAC:('CPX',IDY),0xEE:('LDX',IDY),0xEF:('STX',IDY)}
def disasm(rom, addr, count):
out=[]
for _ in range(count):
start=addr
op=rom[addr]; addr+=1
tab=P0; pfx=''
if op==0x18: pfx='18 '; op=rom[addr]; addr+=1; tab=P1
elif op==0x1A: pfx='1A '; op=rom[addr]; addr+=1; tab=P2
elif op==0xCD: pfx='CD '; op=rom[addr]; addr+=1; tab=P4
ent=tab.get(op)
if ent is None:
out.append((start,f".byte ${op:02X}",'?')); continue
mn,mode=ent; operand=''; tgt=None
if mode==INH: pass
elif mode==IMM8: operand=f"#${rom[addr]:02X}"; addr+=1
elif mode==IMM16: operand=f"#${(rom[addr]<<8)|rom[addr+1]:04X}"; addr+=2
elif mode==DIR: operand=f"${rom[addr]:02X}"; addr+=1
elif mode==EXT:
v=(rom[addr]<<8)|rom[addr+1]; addr+=2; operand=f"${v:04X}"; tgt=v
elif mode==IDX: operand=f"${rom[addr]:02X},X"; addr+=1
elif mode==IDY: operand=f"${rom[addr]:02X},Y"; addr+=1
elif mode==REL:
rel=rom[addr]; addr+=1; d=rel-256 if rel>127 else rel
tgt=(addr+d)&0xFFFF; operand=f"${tgt:04X}"
elif mode==BITDIR: operand=f"${rom[addr]:02X} #${rom[addr+1]:02X}"; addr+=2
elif mode==BITIDX: operand=f"${rom[addr]:02X},X #${rom[addr+1]:02X}"; addr+=2
elif mode==BITIDY: operand=f"${rom[addr]:02X},Y #${rom[addr+1]:02X}"; addr+=2
elif mode==BRDIR:
dd=rom[addr]; mk=rom[addr+1]; rel=rom[addr+2]; addr+=3
s=rel-256 if rel>127 else rel; tgt=(addr+s)&0xFFFF
operand=f"${dd:02X} #${mk:02X} ${tgt:04X}"
elif mode==BRIDX:
dd=rom[addr]; mk=rom[addr+1]; rel=rom[addr+2]; addr+=3
s=rel-256 if rel>127 else rel; tgt=(addr+s)&0xFFFF
operand=f"${dd:02X},X #${mk:02X} ${tgt:04X}"
elif mode==BRIDY:
dd=rom[addr]; mk=rom[addr+1]; rel=rom[addr+2]; addr+=3
s=rel-256 if rel>127 else rel; tgt=(addr+s)&0xFFFF
operand=f"${dd:02X},Y #${mk:02X} ${tgt:04X}"
raw=' '.join(f'{rom[b]:02X}' for b in range(start,addr))
out.append((start,f"{pfx}{mn} {operand}".strip(), raw, mn, tgt))
return out
if __name__=='__main__':
rom=open(__import__('os').path.join(__import__('os').path.dirname(__file__),'tms27pc512.BIN'),'rb').read()
start=int(sys.argv[1],16); count=int(sys.argv[2]) if len(sys.argv)>2 else 40
for row in disasm(rom,start,count):
a=row[0]; txt=row[1]; raw=row[2]
print(f"{a:04X}: {raw:<12} {txt}")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 675 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

@@ -1,171 +0,0 @@
// MatrixPortalPlasma — a hardware replica of the Babcock PD01D221 cockpit
// plasma display, for the Adafruit Matrix Portal S3 driving two chained
// Adafruit 64x32 HUB75 RGB panels (= 128x32).
//
// It enumerates as a USB CDC serial port; point the game's plasma output at it
// (DOSBox-X: serial2=directserial realport:COMx) and it speaks the device side
// of the PD01D221 protocol, rendered in neon-orange to mimic the plasma. The
// command parser + fonts are ported verbatim from vRIO's vPLASMA emulator
// (the reference oracle) — see PlasmaNew/FIRMWARE.md.
//
// Onboard buttons (no host needed): UP = run the built-in firmware demo,
// DOWN = panel test pattern (all dots). BOOT/reset re-runs the power-on splash.
//
// Libraries: Adafruit Protomatter (+ Adafruit GFX). Board: "Adafruit Matrix
// Portal S3" (ESP32-S3). USB CDC On Boot: Enabled.
#include <Adafruit_Protomatter.h>
#include "PlasmaDisplay.h"
#include "demo_screens.h"
// ---- HUB75 pins for the Adafruit Matrix Portal S3 ------------------------
// Adafruit's published Matrix Portal S3 pins (Protomatter examples), except:
// these panels have the green and blue LEDs swapped (manufacturer-confirmed),
// so the G and B pins are exchanged here. Order is R1,G1,B1,R2,G2,B2.
static uint8_t rgbPins[] = {42, 40, 41, 38, 37, 39};
static uint8_t addrPins[] = {45, 36, 48, 35}; // A,B,C,D — 4 lines for 32-high (1/16 scan)
static uint8_t clockPin = 2;
static uint8_t latchPin = 47;
static uint8_t oePin = 14;
// 128 wide (two 64-wide panels chained), 4-bit color, one chain, double-buffered.
Adafruit_Protomatter matrix(128, 4, 1, rgbPins, 4, addrPins,
clockPin, latchPin, oePin, true);
PlasmaDisplay display;
// Plasma-orange palette (RGB565). Full-intensity, half-intensity, off.
static uint16_t COLOR_LIT, COLOR_HALF;
// Blink phase for flashing text / cursor (~3.8 Hz, matched to vPLASMA's 266 ms).
static const uint32_t BLINK_MS = 266;
static uint32_t lastBlink = 0;
static bool blinkPhase = true;
// Demo playback (UP button): loop the 10 firmware screens.
static bool demoRunning = false;
static int demoScreen = 0;
static uint32_t lastDemoStep = 0;
static const uint32_t DEMO_MS = 2800;
// Panel test (DOWN button): cycle the diagnostic patterns.
static bool testRunning = false;
static int testPattern = 0;
static uint32_t lastTestStep = 0;
static const uint32_t TEST_MS = 1200;
// ---- helpers -------------------------------------------------------------
static bool frameHasFlash = false;
static void renderFrame() {
const uint8_t *px = display.frame();
bool anyFlash = false;
for (int y = 0; y < PlasmaDisplay::HEIGHT; ++y) {
for (int x = 0; x < PlasmaDisplay::WIDTH; ++x) {
uint8_t dot = px[y * PlasmaDisplay::WIDTH + x];
uint16_t color = 0;
if (dot & PlasmaDisplay::PIX_LIT) {
if (dot & PlasmaDisplay::PIX_FLASH) {
anyFlash = true;
color = blinkPhase ? ((dot & PlasmaDisplay::PIX_HALF) ? COLOR_HALF : COLOR_LIT) : 0;
} else {
color = (dot & PlasmaDisplay::PIX_HALF) ? COLOR_HALF : COLOR_LIT;
}
}
matrix.drawPixel(x, y, color);
}
}
matrix.show();
frameHasFlash = anyFlash;
}
static void feedDemoScreen(int i) {
const PlasmaDemoScreen &s = plasmaDemo[i];
for (uint16_t j = 0; j < s.len; ++j)
display.feed(pgm_read_byte(&s.data[j]));
}
// ---- Arduino ------------------------------------------------------------
void setup() {
Serial.begin(9600); // USB CDC — the baud is cosmetic over USB
ProtomatterStatus status = matrix.begin();
// If begin() fails the wiring/pins are wrong; blink the onboard LED forever.
if (status != PROTOMATTER_OK) {
pinMode(LED_BUILTIN, OUTPUT);
for (;;) { digitalWrite(LED_BUILTIN, HIGH); delay(120); digitalWrite(LED_BUILTIN, LOW); delay(120); }
}
COLOR_LIT = matrix.color565(255, 96, 0); // neon orange
COLOR_HALF = matrix.color565(110, 40, 0); // dim orange
#ifdef BUTTON_UP
pinMode(BUTTON_UP, INPUT_PULLUP);
#endif
#ifdef BUTTON_DOWN
pinMode(BUTTON_DOWN, INPUT_PULLUP);
#endif
// Power-on splash: light every dot for ~1 s (confirms both panels), then clear.
display.showTestPattern();
renderFrame();
delay(1000);
display.reset();
renderFrame();
}
void loop() {
// 1) Drain the USB serial into the parser.
while (Serial.available() > 0)
display.feed((uint8_t)Serial.read());
// 2) Buttons (edge-detected).
#ifdef BUTTON_UP
static bool upPrev = HIGH;
bool up = digitalRead(BUTTON_UP);
if (upPrev == HIGH && up == LOW) { // pressed
demoRunning = !demoRunning;
testRunning = false;
if (demoRunning) { demoScreen = 0; feedDemoScreen(0); demoScreen = 1; lastDemoStep = millis(); }
else { display.reset(); }
}
upPrev = up;
#endif
#ifdef BUTTON_DOWN
static bool downPrev = HIGH;
bool down = digitalRead(BUTTON_DOWN);
if (downPrev == HIGH && down == LOW) { // pressed
testRunning = !testRunning;
demoRunning = false;
if (testRunning) { testPattern = 0; display.showTestPattern(0); lastTestStep = millis(); }
else { display.reset(); }
}
downPrev = down;
#endif
// 3) Advance the demo / test on their timers.
uint32_t now = millis();
if (demoRunning && now - lastDemoStep >= DEMO_MS) {
feedDemoScreen(demoScreen);
demoScreen = (demoScreen + 1) % plasmaDemoCount;
lastDemoStep = now;
}
if (testRunning && now - lastTestStep >= TEST_MS) {
testPattern = (testPattern + 1) % PlasmaDisplay::TEST_PATTERN_COUNT;
display.showTestPattern(testPattern);
lastTestStep = now;
}
// 4) Blink phase for flashing pixels.
if (now - lastBlink >= BLINK_MS) {
blinkPhase = !blinkPhase;
lastBlink = now;
if (frameHasFlash) renderFrame(); // only re-render if something blinks
}
// 5) Repaint when the parser changed the frame.
if (display.takeDirty())
renderFrame();
}
@@ -1,225 +0,0 @@
#include "PlasmaDisplay.h"
// Command bytes (see PlasmaProtocol.cs / FIRMWARE.md).
static const uint8_t ESC = 0x1B;
static const uint8_t BS = 0x08, HT = 0x09, LF = 0x0A, VT = 0x0B, CR = 0x0D;
static const uint8_t CMD_CLEAR = '@', CMD_CURSOR = 'G', CMD_ATTR = 'H';
static const uint8_t CMD_FONT = 'K', CMD_HOME = 'L', CMD_GRAPHICS = 'P';
static const uint8_t CMD_SETROW = 'Q', CMD_SETCOL = 'R';
static const uint8_t CMD_DRAWPAGE = 'I', CMD_DISPPAGE = 'i';
PlasmaDisplay::PlasmaDisplay() { reset(); }
void PlasmaDisplay::reset() {
memset(pixels_, 0, sizeof(pixels_));
cx_ = cy_ = 0;
font_ = 0;
face_ = &plasmaFonts[0];
attrs_ = 0;
cursorMode_ = CURSOR_STEADY; // power-on default; the game hides it
orient_ = HORIZONTAL;
state_ = TEXT;
headerFill_ = 0;
dataIndex_ = dataLength_ = 0;
dirty_ = true;
}
void PlasmaDisplay::setOrientation(Orientation o) {
if (orient_ == o) return;
orient_ = o;
memset(pixels_, 0, sizeof(pixels_));
cx_ = cy_ = 0;
dirty_ = true;
}
// The real display's power-on test (JP1 jumper 5, firmware $B888) runs through
// several drawing sequences — solid, border+grid, and more — to expose dead
// dots and addressing faults. These stand in for that; the sketch cycles them.
static bool testDot(int index, int x, int y) {
switch (index) {
case 0: return true; // all on
case 1: return x == 0 || x == PlasmaDisplay::WIDTH - 1 || // frame
y == 0 || y == PlasmaDisplay::HEIGHT - 1 ||
(x % 16) == 0 || (y % 8) == 0; // + grid
case 2: return ((y / 2) % 2) == 0; // horizontal stripes
case 3: return ((x / 4) % 2) == 0; // vertical stripes
default: return (((x / 8) + (y / 8)) % 2) == 0; // checkerboard
}
}
void PlasmaDisplay::showTestPattern(int index) {
for (int y = 0; y < HEIGHT; ++y)
for (int x = 0; x < WIDTH; ++x)
pixels_[y * WIDTH + x] = testDot(index, x, y) ? PIX_LIT : 0;
dirty_ = true;
}
// One logical dot → the physical 128x32 buffer, rotated per orientation.
void PlasmaDisplay::plot(int lx, int ly, uint8_t flags) {
if ((unsigned)lx >= (unsigned)logicalW() || (unsigned)ly >= (unsigned)logicalH()) return;
int px, py;
if (orient_ == HORIZONTAL) {
px = lx;
py = ly;
} else {
px = ly;
py = HEIGHT - 1 - lx; // 90° rotation onto landscape glass
}
pixels_[py * WIDTH + px] = flags;
}
void PlasmaDisplay::feed(const uint8_t *buf, size_t n) {
for (size_t i = 0; i < n; ++i) feed(buf[i]);
}
void PlasmaDisplay::feed(uint8_t b) {
switch (state_) {
case TEXT: stepText(b); break;
case ESCAPE: stepEscape(b); break;
case OPERAND:
state_ = TEXT;
applyOperand(pendingCmd_, b);
break;
case GHEADER:
header_[headerFill_++] = b;
if (headerFill_ == 5) beginGraphics();
break;
case GDATA: stepGraphics(b); break;
}
}
void PlasmaDisplay::stepText(uint8_t b) {
switch (b) {
case ESC: state_ = ESCAPE; return;
case BS: cx_ = max(0, cx_ - face_->width); dirty_ = true; return;
case HT: advanceCursor(); dirty_ = true; return;
case LF: nextLine(); dirty_ = true; return;
case VT:
cy_ -= face_->height;
if (cy_ < 0) cy_ = max(0, logicalH() - face_->height);
dirty_ = true;
return;
case CR: cx_ = 0; dirty_ = true; return;
}
if (b < 0x20) return; // any other control byte: swallow (matches firmware)
drawChar(b);
}
void PlasmaDisplay::stepEscape(uint8_t b) {
state_ = TEXT;
switch (b) {
case CMD_CLEAR:
memset(pixels_, 0, sizeof(pixels_));
cx_ = cy_ = 0;
font_ = 0;
face_ = &plasmaFonts[0];
attrs_ = 0;
dirty_ = true;
break;
case CMD_HOME:
cx_ = cy_ = 0;
dirty_ = true;
break;
case CMD_CURSOR:
case CMD_FONT:
case CMD_ATTR:
case CMD_SETROW:
case CMD_SETCOL:
case CMD_DRAWPAGE:
case CMD_DISPPAGE:
pendingCmd_ = b;
state_ = OPERAND;
break;
case CMD_GRAPHICS:
headerFill_ = 0;
state_ = GHEADER;
break;
default: break; // unknown command: ignored (does not consume an operand)
}
}
void PlasmaDisplay::applyOperand(uint8_t cmd, uint8_t op) {
switch (cmd) {
case CMD_CURSOR:
cursorMode_ = (op == 0x00 || op == 0xFF) ? CURSOR_HIDDEN
: (op & 0x02) ? CURSOR_FLASHING
: CURSOR_STEADY;
dirty_ = true;
break;
case CMD_FONT:
if (op < 8) { // 8 real fonts; firmware ignores larger operands
font_ = op;
face_ = &plasmaFonts[op];
if (cx_ > logicalW() - 1) cx_ = logicalW() - 1;
if (cy_ > logicalH() - 1) cy_ = logicalH() - 1;
dirty_ = true;
}
break;
case CMD_ATTR:
attrs_ = op & 0x0F; // low 4 bits: half/underline/reverse/flash
break;
case CMD_SETROW:
if (op < logicalH()) { cy_ = op; dirty_ = true; }
break;
case CMD_SETCOL:
if (op < logicalW()) { cx_ = op; dirty_ = true; }
break;
case CMD_DRAWPAGE:
case CMD_DISPPAGE:
break; // page select: consumed but single-page (see FIRMWARE.md)
}
}
void PlasmaDisplay::beginGraphics() {
int w = header_[3], h = header_[4];
dataLength_ = w * h;
dataIndex_ = 0;
state_ = dataLength_ > 0 ? GDATA : TEXT;
}
void PlasmaDisplay::stepGraphics(uint8_t b) {
int w = header_[3];
int rowOfBlock = dataIndex_ / w;
int byteOfRow = dataIndex_ % w;
int y = header_[1] + rowOfBlock;
int baseX = (header_[2] + byteOfRow) * 8;
// MSB = leftmost pixel (L4PLASMA.CPP packs 0x80 first).
for (int bit = 0; bit < 8; ++bit)
plot(baseX + bit, y, (b & (0x80 >> bit)) ? PIX_LIT : 0);
dirty_ = true;
if (++dataIndex_ >= dataLength_) state_ = TEXT;
}
void PlasmaDisplay::drawChar(uint8_t code) {
if (!faceHas(code)) return; // firmware ignores out-of-range chars
int w = face_->width, h = face_->height;
bool reverse = attrs_ & 0x04;
bool underline = attrs_ & 0x02;
uint8_t litFlags = PIX_LIT;
if (attrs_ & 0x01) litFlags |= PIX_HALF;
if (attrs_ & 0x08) litFlags |= PIX_FLASH;
for (int row = 0; row < h; ++row) {
uint16_t bits = faceRow(code, row); // bit15 = leftmost pixel
for (int col = 0; col < w; ++col) {
bool on = bits & (0x8000 >> col);
if (underline && row == h - 1) on = true;
if (reverse) on = !on;
plot(cx_ + col, cy_ + row, on ? litFlags : 0);
}
}
dirty_ = true;
advanceCursor();
}
void PlasmaDisplay::advanceCursor() {
cx_ += face_->width;
if (cx_ > logicalW() - face_->width) nextLine();
}
void PlasmaDisplay::nextLine() {
cx_ = 0;
cy_ += face_->height;
if (cy_ > logicalH() - face_->height) cy_ = 0; // wrap to top; no scroll
}
@@ -1,90 +0,0 @@
// PlasmaDisplay — the Babcock PD01D221 command parser + 128x32 framebuffer,
// ported line-for-line from vRIO's C# VPlasmaDevice (src/VPlasma.Core/Device/
// VPlasmaDevice.cs). Keep the two in sync: vPLASMA is the reference oracle.
//
// Feed received wire bytes to feed(); read the framebuffer with frame() and
// render it however the panel wants. Each pixel is a flag byte (PIX_LIT /
// PIX_HALF / PIX_FLASH), so the renderer can dim half-intensity dots and blink
// flashing ones. See PlasmaNew/FIRMWARE.md for the recovered command set.
#pragma once
#include <Arduino.h>
#include "plasma_fonts.h"
class PlasmaDisplay {
public:
static const int WIDTH = 128;
static const int HEIGHT = 32;
// Per-pixel flag bits in the framebuffer.
static const uint8_t PIX_LIT = 0x01;
static const uint8_t PIX_HALF = 0x02;
static const uint8_t PIX_FLASH = 0x04;
enum Orientation { HORIZONTAL, VERTICAL };
enum CursorMode { CURSOR_HIDDEN, CURSOR_STEADY, CURSOR_FLASHING };
PlasmaDisplay();
static const int TEST_PATTERN_COUNT = 5; // JP1 jumper-5 diagnostic patterns
void reset(); // power-on: dark glass, home cursor
void feed(uint8_t b); // one received wire byte
void feed(const uint8_t *buf, size_t n);
void showTestPattern() { showTestPattern(0); } // all dots lit
void showTestPattern(int index); // JP1 jumper 5: solid/grid/stripes/checker
void setOrientation(Orientation o); // JP1 jumper 4
const uint8_t *frame() const { return pixels_; }
bool takeDirty() { // true (and clears) if the frame changed
bool d = dirty_;
dirty_ = false;
return d;
}
CursorMode cursorMode() const { return cursorMode_; }
int cursorX() const { return cx_; }
int cursorY() const { return cy_; }
int fontWidth() const { return face_->width; }
int fontHeight() const { return face_->height; }
Orientation orientation() const { return orient_; }
private:
uint8_t pixels_[WIDTH * HEIGHT]; // always physical 128x32
// Text-mode state.
int font_;
const PlasmaFace *face_;
uint8_t attrs_; // low 4 bits: 1=half 2=underline 4=reverse 8=flash
int cx_, cy_; // cursor, logical pixels
CursorMode cursorMode_;
Orientation orient_;
// Parser state.
enum State { TEXT, ESCAPE, OPERAND, GHEADER, GDATA };
State state_;
uint8_t pendingCmd_;
uint8_t header_[5]; // screen, y, x, w, h
int headerFill_;
int dataIndex_, dataLength_;
bool dirty_;
int logicalW() const { return orient_ == HORIZONTAL ? WIDTH : HEIGHT; }
int logicalH() const { return orient_ == HORIZONTAL ? HEIGHT : WIDTH; }
void plot(int lx, int ly, uint8_t flags);
void stepText(uint8_t b);
void stepEscape(uint8_t b);
void applyOperand(uint8_t cmd, uint8_t op);
void beginGraphics();
void stepGraphics(uint8_t b);
void drawChar(uint8_t code);
void advanceCursor();
void nextLine();
bool faceHas(uint8_t code) const { return code >= face_->first && code <= face_->last; }
uint16_t faceRow(uint8_t code, int row) const {
if (!faceHas(code) || (unsigned)row >= (unsigned)face_->height) return 0;
return pgm_read_word(&face_->rows[(code - face_->first) * face_->height + row]);
}
};
@@ -1,88 +0,0 @@
// AUTO-GENERATED from the PD01D221 firmware (tms27pc512.BIN): the real
// 10-screen demonstration program (the display's built-in demo, JP1
// jumper 6), extracted from the demo pointer table at $8000. In PROGMEM.
#pragma once
#include <Arduino.h>
static const uint8_t PROGMEM plasmaDemo0[] = {
0x1B,0x47,0x00,0x1B,0x40,0x1B,0x4C,0x1B,0x52,0x04,0x59,0x4F,0x55,0x20,0x48,0x41,
0x56,0x45,0x20,0x45,0x4E,0x41,0x42,0x4C,0x45,0x44,0x20,0x54,0x48,0x45,0x1B,0x52,
0x25,0x50,0x44,0x30,0x31,0x2D,0x44,0x32,0x32,0x31,0x0A,0x1B,0x52,0x01,0x44,0x45,
0x4D,0x4F,0x4E,0x53,0x54,0x52,0x41,0x54,0x49,0x4F,0x4E,0x20,0x50,0x52,0x4F,0x47,
0x52,0x41,0x4D
};
static const uint8_t PROGMEM plasmaDemo1[] = {
0x1B,0x49,0x02,0x1B,0x40,0x1B,0x4C,0x1B,0x52,0x02,0x49,0x46,0x20,0x59,0x4F,0x55,
0x20,0x44,0x4F,0x20,0x4E,0x4F,0x54,0x20,0x57,0x49,0x53,0x48,0x20,0x54,0x4F,0x1B,
0x52,0x0A,0x56,0x49,0x45,0x57,0x20,0x54,0x48,0x49,0x53,0x20,0x50,0x52,0x4F,0x47,
0x52,0x41,0x4D,0x2C,0x0A,0x1B,0x52,0x07,0x52,0x45,0x4D,0x4F,0x56,0x45,0x20,0x4A,
0x55,0x4D,0x50,0x45,0x52,0x20,0x36,0x20,0x41,0x4E,0x44,0x0A,0x1B,0x52,0x0D,0x52,
0x45,0x53,0x45,0x54,0x20,0x54,0x48,0x45,0x20,0x44,0x49,0x53,0x50,0x4C,0x41,0x59,
0x1B,0x69,0x02
};
static const uint8_t PROGMEM plasmaDemo2[] = {
0x1B,0x49,0x01,0x1B,0x40,0x1B,0x4B,0x04,0x1B,0x51,0x00,0x1B,0x52,0x0A,0x50,0x4C,
0x41,0x53,0x4D,0x41,0x44,0x4F,0x54,0x1B,0x52,0x0A,0x50,0x44,0x30,0x31,0x2D,0x44,
0x32,0x32,0x31,0x1B,0x69,0x01
};
static const uint8_t PROGMEM plasmaDemo3[] = {
0x1B,0x49,0x00,0x1B,0x40,0x1B,0x4B,0x06,0x1B,0x51,0x00,0x1B,0x52,0x1D,0x41,0x20,
0x43,0x4F,0x4D,0x50,0x4C,0x45,0x54,0x45,0x0A,0x1B,0x52,0x0F,0x44,0x49,0x53,0x50,
0x4C,0x41,0x59,0x20,0x53,0x59,0x53,0x54,0x45,0x4D,0x0A,0x1B,0x52,0x0C,0x49,0x4E,
0x20,0x4F,0x4E,0x45,0x20,0x50,0x41,0x43,0x4B,0x41,0x47,0x45,0x21,0x1B,0x69,0x00
};
static const uint8_t PROGMEM plasmaDemo4[] = {
0x1B,0x49,0x01,0x1B,0x40,0x1B,0x4B,0x06,0x1B,0x4C,0x46,0x45,0x41,0x54,0x55,0x52,
0x45,0x53,0x20,0x49,0x4E,0x43,0x4C,0x55,0x44,0x45,0x20,0x2D,0x1B,0x4B,0x03,0x1B,
0x51,0x0C,0x1B,0x52,0x0A,0x4F,0x1B,0x4B,0x02,0x20,0x53,0x45,0x52,0x49,0x41,0x4C,
0x20,0x49,0x4E,0x54,0x45,0x52,0x46,0x41,0x43,0x45,0x0A,0x1B,0x4B,0x03,0x1B,0x52,
0x0A,0x4F,0x1B,0x4B,0x02,0x20,0x50,0x41,0x52,0x41,0x4C,0x4C,0x45,0x4C,0x20,0x50,
0x4F,0x52,0x54,0x1B,0x69,0x01
};
static const uint8_t PROGMEM plasmaDemo5[] = {
0x1B,0x49,0x02,0x1B,0x40,0x1B,0x4B,0x06,0x1B,0x4C,0x46,0x45,0x41,0x54,0x55,0x52,
0x45,0x53,0x20,0x49,0x4E,0x43,0x4C,0x55,0x44,0x45,0x20,0x2D,0x1B,0x51,0x0C,0x1B,
0x4B,0x03,0x1B,0x52,0x0A,0x4F,0x1B,0x4B,0x02,0x20,0x50,0x4F,0x57,0x45,0x52,0x20,
0x53,0x55,0x50,0x50,0x4C,0x59,0x0A,0x1B,0x4B,0x03,0x1B,0x52,0x0A,0x4F,0x1B,0x4B,
0x02,0x20,0x4B,0x45,0x59,0x50,0x41,0x44,0x20,0x49,0x4E,0x54,0x45,0x52,0x46,0x41,
0x43,0x45,0x1B,0x69,0x02
};
static const uint8_t PROGMEM plasmaDemo6[] = {
0x1B,0x49,0x00,0x1B,0x40,0x1B,0x4B,0x02,0x1B,0x51,0x00,0x1B,0x52,0x00,0x4D,0x49,
0x58,0x20,0x54,0x45,0x58,0x54,0x20,0x41,0x4E,0x44,0x20,0x47,0x52,0x41,0x50,0x48,
0x49,0x43,0x53,0x1B,0x52,0x18,0x55,0x53,0x49,0x4E,0x47,0x20,0x38,0x20,0x53,0x54,
0x4F,0x52,0x45,0x44,0x0A,0x1B,0x52,0x0D,0x43,0x48,0x41,0x52,0x41,0x43,0x54,0x45,
0x52,0x20,0x46,0x4F,0x4E,0x54,0x53,0x20,0x2D,0x1B,0x69,0x00
};
static const uint8_t PROGMEM plasmaDemo7[] = {
0x1B,0x49,0x01,0x1B,0x40,0x1B,0x4B,0x02,0x1B,0x4C,0x49,0x4E,0x54,0x45,0x4C,0x4C,
0x49,0x47,0x45,0x4E,0x54,0x20,0x49,0x4E,0x54,0x45,0x52,0x46,0x41,0x43,0x45,0x1B,
0x52,0x19,0x48,0x41,0x53,0x20,0x44,0x4F,0x5A,0x45,0x4E,0x53,0x20,0x4F,0x46,0x0A,
0x1B,0x52,0x25,0x43,0x4F,0x4D,0x4D,0x41,0x4E,0x44,0x53,0x2E,0x1B,0x69,0x01
};
static const uint8_t PROGMEM plasmaDemo8[] = {
0x1B,0x49,0x02,0x1B,0x40,0x1B,0x4B,0x02,0x1B,0x4C,0x1B,0x52,0x19,0x43,0x48,0x4F,
0x4F,0x53,0x45,0x20,0x45,0x49,0x54,0x48,0x45,0x52,0x0A,0x1B,0x52,0x19,0x48,0x4F,
0x52,0x49,0x5A,0x4F,0x4E,0x54,0x41,0x4C,0x20,0x4F,0x52,0x0A,0x1B,0x52,0x04,0x56,
0x45,0x52,0x54,0x49,0x43,0x41,0x4C,0x20,0x4F,0x52,0x49,0x45,0x4E,0x54,0x41,0x54,
0x49,0x4F,0x4E,0x1B,0x69,0x02
};
static const uint8_t PROGMEM plasmaDemo9[] = {
0x1B,0x49,0x00,0x1B,0x40,0x1B,0x5A,0x00,0x00,0x00,0x00,0x1B,0x69,0x00
};
struct PlasmaDemoScreen { const uint8_t *data; uint16_t len; };
static const PlasmaDemoScreen plasmaDemo[10] = {
{ plasmaDemo0, 67 },
{ plasmaDemo1, 99 },
{ plasmaDemo2, 38 },
{ plasmaDemo3, 64 },
{ plasmaDemo4, 86 },
{ plasmaDemo5, 85 },
{ plasmaDemo6, 76 },
{ plasmaDemo7, 63 },
{ plasmaDemo8, 70 },
{ plasmaDemo9, 14 },
};
static const int plasmaDemoCount = 10;
File diff suppressed because it is too large Load Diff
-147
View File
@@ -1,147 +0,0 @@
# Plasma display replica — Adafruit Matrix Portal S3 + HUB75
A hardware replacement for the failing Babcock **PD01D221** cockpit plasma
display ([../README.md](../README.md), [../FIRMWARE.md](../FIRMWARE.md)). A
modern microcontroller reads the same serial command stream the game sends and
renders it to a modern LED matrix — a drop-in from the host's point of view,
with none of the plasma physics or high voltage.
The firmware's command parser and fonts are **ported verbatim from vRIO's
vPLASMA emulator** (`src/VPlasma.Core`), which is the reference oracle. Feed
the replica and vPLASMA the same byte stream and they produce the same frame.
## Bill of materials
| Part | Notes |
|------|-------|
| **Adafruit Matrix Portal S3** | ESP32-S3 controller; plugs into the HUB75 header; native USB-C = the virtual COM port |
| **2 × Adafruit 64×32 RGB LED Matrix** (P-pitch to taste; 1/16 scan) | chained → **128×32**, the panel's native resolution |
| **5 V power supply, ≥ 4 A** | the panels are the load; USB cannot power them |
| USB-C cable | data (and logic power) from the host PC |
An amber-leaning pitch and diffuser best mimic the neon-orange plasma; the
firmware already renders in orange (`255,96,0` full / `110,40,0` half).
## Wiring
1. **Chain the panels**: panel A `OUT` → panel B `IN`, left-to-right, so the
pair reads as one 128-wide canvas (x 063 = panel A, 64127 = panel B). If
the image comes out swapped, reverse the chain order.
2. **Mount the Matrix Portal S3** onto panel A's `IN` HUB75 header.
3. **Power**: 5 V ≥ 4 A into the Matrix Portal's screw terminals; run the
panels' power pigtails from the same 5 V. USB-C carries data (and powers the
S3 logic) — do **not** rely on USB for panel current.
## Build & flash
Arduino IDE (or `arduino-cli`):
1. **Boards Manager** → install **esp32** (Espressif). Select board **"Adafruit
Matrix Portal S3"**. Set **USB CDC On Boot: Enabled** (so `Serial` is the
USB port the game opens).
2. **Library Manager** → install **Adafruit Protomatter** (pulls in Adafruit
GFX / BusIO).
3. Open `MatrixPortalPlasma/MatrixPortalPlasma.ino` (keep `PlasmaDisplay.*`,
`plasma_fonts.h`, `demo_screens.h` beside it) and Upload.
The HUB75 pin arrays at the top of the `.ino` are Adafruit's published Matrix
Portal S3 values — except the G and B pins, which are exchanged because these
panels have the green and blue LEDs swapped (manufacturer-confirmed; symptom
was orange rendering as purple). If a different panel garbles, verify the
arrays against your installed Protomatter version.
## Using it with the game
The Matrix Portal enumerates as a **USB CDC COM port**. In Windows Device
Manager you can pin it to the COM number the host expects. Point the game's
plasma output at it — under the DOSBox-X fork:
```
serial2 = directserial realport:COMx
```
Baud is cosmetic over USB CDC (the `9600` line-coding is accepted as a no-op),
so the display keeps up regardless. The link is one-way (the game writes, the
display listens), exactly as the cockpit drove the real panel.
## No-host testing (onboard buttons)
- **UP** — toggle the built-in **firmware demonstration** (the real 10-screen
PLASMADOT demo, `demo_screens.h`, extracted from the ROM).
- **DOWN** — toggle the **panel test**: cycles diagnostic patterns (solid,
border+grid, horizontal/vertical stripes, checkerboard) to expose dead dots
and addressing faults — the multi-pattern sequence the real firmware runs.
- **Power-on** briefly lights every dot (confirms both panels), then clears.
## Files
| File | |
|------|--|
| `MatrixPortalPlasma/MatrixPortalPlasma.ino` | sketch: Protomatter init, USB serial, render loop, buttons |
| `MatrixPortalPlasma/PlasmaDisplay.h/.cpp` | the PD01D221 parser + 128×32 framebuffer — a C++ port of `VPlasmaDevice` |
| `MatrixPortalPlasma/plasma_fonts.h` | the 8 real ROM fonts (PROGMEM), generated from `tms27pc512.BIN` |
| `MatrixPortalPlasma/demo_screens.h` | the 10 firmware demo screens (PROGMEM) |
## Keeping it faithful — the differential test
`PlasmaDisplay` is a line-for-line port of `VPlasmaDevice`; keep the two in
sync (same commands, fonts, orientation/`plot` mapping). Validate the replica
by sending **identical byte streams** to the replica, to vPLASMA, and to the
real panel, then comparing the glass.
The **`VPlasma.Wire`** tool (`tools/VPlasma.Wire`, `dotnet run --project
tools/VPlasma.Wire -- …`) drives this:
```sh
# 1) Build a repeatable stream (or capture a real one — see below).
VPlasma.Wire synth --kind demo --out demo.bin
# 2) The vPLASMA golden image (pixel-exact reference PNG).
VPlasma.Wire render --in demo.bin --out demo-golden.png --scale 8
# 3) Replay the SAME bytes to each target, and photograph the glass:
VPlasma.Wire replay --in demo.bin --port COM3 # the real panel (RS-232)
VPlasma.Wire replay --in demo.bin --port COM7 # the Matrix Portal (USB-CDC)
```
Compare the two photos against `demo-golden.png`. `synth` kinds: `selftest`,
`demo`, `banner`, `charset`. `render` takes `--orient v` for vertical.
**Capture a real session** to build a test corpus non-intrusively — insert the
tool in the path (point the game at `COM12`, tool tees on to the real display
on `COM3`):
```sh
VPlasma.Wire capture --port COM12 --out session.bin --tee COM3
```
Then `render`/`replay` `session.bin` like any other stream.
**Auto-compare** a photo (cropped to the glass) against the golden image, or
two streams against each other — each is reduced to 128×32 and diffed:
```sh
VPlasma.Wire diff --a demo-golden.png --b panel-photo.png --out delta.png
# prints "differing dots=N/4096 match=XX%", writes a red-on-mismatch image,
# exits 1 if they differ. Inputs may be .bin (ESC P) / .png / .txt (bitmap=).
```
### Authoring plasma content (from TeslaSuite's plasma tools)
Generate display content from text or images and push it over the wire:
```sh
# Text in a Windows font, auto-sized to the panel → preview PNG + ESC P + hex:
VPlasma.Wire text --text "ALERT 12" --font "Arial" --png a.png --bin a.bin --hex a.txt
VPlasma.Wire replay --in a.bin --port COM7 # show it on the replica
# An image → the ESC P wire stream (and/or the game's bitmap= encoding):
VPlasma.Wire encode --in logo.png --bin logo.bin --hex logo.txt
# A bitmap= encoding → a preview PNG (and/or ESC P):
VPlasma.Wire decode --in logo.txt --png logo.png
```
Still deferred (as in vPLASMA, documented in `../FIRMWARE.md`): the 10
double-buffered pages (`ESC I`/`ESC i` are consumed but single-page) and the
vector-graphics primitives (`ESC A``F`).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

+1 -1
View File
@@ -164,7 +164,7 @@ recovered grammar lives in `src/VPlasma.Core/Protocol/PlasmaProtocol.cs`.
12×16 / 12×20. Attributes (`ESC H`, low 4 bits) — half-intensity
draws dimmer, underline/reverse render in the cell, flashing text (and
the flashing cursor) blink on the glass. Extracted from the U3 EPROM;
see [`PlasmaNew/FIRMWARE.md`](PlasmaNew/FIRMWARE.md). (Deferred, and
see `restoration/PlasmaNew/FIRMWARE.md` in the TeslaRel410 repo. (Deferred, and
documented there: the 10 double-buffered pages and vector graphics.)
- **Double-click** the glass to cycle three self-test pages (banner,
charset, graphics pattern) through the same parser the wire feeds —
+1 -1
View File
@@ -11,7 +11,7 @@ namespace VPlasma.App;
/// automatically.
///
/// <para>The panel mirrors the real PD01D221's <b>JP1 configuration jumpers</b>
/// (recovered in <c>PlasmaNew/FIRMWARE.md</c>): the baud straps (1+2) drive the
/// (recovered in <c>TeslaRel410/restoration/PlasmaNew/FIRMWARE.md</c>): the baud straps (1+2) drive the
/// COM baud, orientation (4), display test (5), and the demonstration program
/// (6) — which replays the real 10-screen firmware demo. Plus display
/// controls, live counters, and a wire log. Double-click the glass to cycle
@@ -1,6 +1,6 @@
// AUTO-GENERATED from the PD01D221 firmware (tms27pc512.BIN) by PlasmaNew
// tooling: the real 10-screen demonstration program (jumper 6), extracted
// from the demo pointer table at $8000. See PlasmaNew/FIRMWARE.md.
// from the demo pointer table at $8000. See TeslaRel410/restoration/PlasmaNew/FIRMWARE.md.
namespace VPlasma.Core.Device;
/// <summary>The display's built-in demonstration program — the 10 command
+2 -1
View File
@@ -1,6 +1,7 @@
// AUTO-GENERATED from the PD01D221 firmware (tms27pc512.BIN) by
// PlasmaNew tooling. The real Babcock character generator, all 8 fonts.
// Each glyph row is a ushort; bit 15 = leftmost pixel. See PlasmaNew/FIRMWARE.md.
// Each glyph row is a ushort; bit 15 = leftmost pixel.
// See TeslaRel410/restoration/PlasmaNew/FIRMWARE.md.
namespace VPlasma.Core.Device;
/// <summary>The display's 8 built-in fonts, extracted from ROM.</summary>
+2 -2
View File
@@ -23,7 +23,7 @@ public enum PlasmaAttributes : byte
/// <summary>
/// Display orientation — the JP1 jumper-4 (PD5) strap the firmware reads at
/// boot (<c>PlasmaNew/README.md</c>). Horizontal is the normal 128×32
/// boot (<c>TeslaRel410/restoration/PlasmaNew/README.md</c>). Horizontal is the normal 128×32
/// landscape; Vertical treats the panel as 32×128 and rotates content onto
/// the physical glass (for a portrait-mounted panel).
/// </summary>
@@ -39,7 +39,7 @@ public enum PlasmaOrientation
/// COM2. Feed raw wire bytes to <see cref="OnReceived"/>; the parser is a
/// state machine, so commands may arrive split across any chunk boundaries.
///
/// <para>Grounded in the real firmware (<c>PlasmaNew/FIRMWARE.md</c>): the
/// <para>Grounded in the real firmware (<c>TeslaRel410/restoration/PlasmaNew/FIRMWARE.md</c>): the
/// cursor is a <b>pixel</b> position — <c>ESC Q</c> sets its row (Y),
/// <c>ESC R</c> its column (X) — and glyphs are drawn there and advance X by
/// the font's width. The eight fonts are the real ROM character generator