restoration: import PlasmaNew — the plasma display replica effort
Moved from the vRIO repository to live with the other hardware restoration work. Contains the PD01D221 reverse-engineering reference (FIRMWARE.md, recovered from the TMS27PC512 EPROM dump) and the working Matrix Portal S3 + HUB75 replica firmware (flashed and bench-tested 2026-07-22, including the G/B pin swap for these panels, the PIN_BUTTON_UP/DOWN fix, and power-on cursor rendering). The vPLASMA reference oracle (src/VPlasma.Core) and the VPlasma.Wire differential-test tool stay in the vRIO repo; PlasmaNew pre-move commit history is there too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# 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 <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.
@@ -0,0 +1,211 @@
|
||||
# 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 (0–7; 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: PD0–PD5 = pins 10–15, PA0–PA7 =
|
||||
pins 24–17 (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.
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/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.
|
After Width: | Height: | Size: 675 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1,184 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Underline cursor on the bottom dot-row of its cell (as PlasmaCanvas).
|
||||
PlasmaDisplay::CursorMode cm = display.cursorMode();
|
||||
if (cm == PlasmaDisplay::CURSOR_FLASHING) anyFlash = true;
|
||||
if (cm == PlasmaDisplay::CURSOR_STEADY ||
|
||||
(cm == PlasmaDisplay::CURSOR_FLASHING && blinkPhase)) {
|
||||
int cx = display.cursorX();
|
||||
int cy = display.cursorY() + display.fontHeight() - 1;
|
||||
if (cy < PlasmaDisplay::HEIGHT)
|
||||
for (int i = 0; i < display.fontWidth() && cx + i < PlasmaDisplay::WIDTH; ++i)
|
||||
matrix.drawPixel(cx + i, cy, COLOR_LIT);
|
||||
}
|
||||
|
||||
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 PIN_BUTTON_UP
|
||||
pinMode(PIN_BUTTON_UP, INPUT_PULLUP);
|
||||
#endif
|
||||
#ifdef PIN_BUTTON_DOWN
|
||||
pinMode(PIN_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 PIN_BUTTON_UP
|
||||
static bool upPrev = HIGH;
|
||||
bool up = digitalRead(PIN_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 PIN_BUTTON_DOWN
|
||||
static bool downPrev = HIGH;
|
||||
bool down = digitalRead(PIN_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();
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#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
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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]);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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
@@ -0,0 +1,149 @@
|
||||
# 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` in the vRIO repo), 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 0–63 = panel A, 64–127 = 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 (in the vRIO repo: `tools/VPlasma.Wire`,
|
||||
`dotnet run --project tools/VPlasma.Wire -- …` from that repo's root)
|
||||
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.
|
After Width: | Height: | Size: 896 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 943 KiB |
Reference in New Issue
Block a user