PlasmaNew/replica: Matrix Portal S3 firmware for the hardware replica
The hardware replacement for the failing Babcock PD01D221: an Adafruit Matrix Portal S3 (ESP32-S3) reads the game's serial command stream over its native USB-CDC port and renders it to two chained Adafruit 64x32 HUB75 RGB panels (= 128x32) in neon-orange, via Adafruit Protomatter. - PlasmaDisplay.h/.cpp — the PD01D221 command parser + 128x32 framebuffer, ported line-for-line from vPLASMA's VPlasmaDevice (the reference oracle): the full ESC command set, pixel-addressed cursor, attributes, orientation, and the rotate-onto-glass plot() mapping. - plasma_fonts.h — the 8 real ROM fonts (PROGMEM), generated from tms27pc512.BIN. - demo_screens.h — the 10 firmware demo screens (PROGMEM). - MatrixPortalPlasma.ino — Protomatter init (128x32), USB serial input, the orange/dim/blink render loop, and onboard buttons (UP = firmware demo, DOWN = panel test) for no-host bring-up. - README.md — BOM, wiring, build/flash, DOSBox-X serial config. Parser verified on the host with g++ (font-0 'H', ESC Q/R positioning, demo replay, test pattern) against a minimal Arduino shim. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// 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 ------------------------
|
||||
// These are Adafruit's published Matrix Portal S3 pins (Protomatter examples).
|
||||
// Verify against your installed library version if the panel misbehaves.
|
||||
static uint8_t rgbPins[] = {42, 41, 40, 38, 39, 37};
|
||||
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;
|
||||
|
||||
// ---- 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;
|
||||
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
|
||||
demoRunning = false;
|
||||
display.showTestPattern();
|
||||
}
|
||||
downPrev = down;
|
||||
#endif
|
||||
|
||||
// 3) Advance the demo on its timer.
|
||||
uint32_t now = millis();
|
||||
if (demoRunning && now - lastDemoStep >= DEMO_MS) {
|
||||
feedDemoScreen(demoScreen);
|
||||
demoScreen = (demoScreen + 1) % plasmaDemoCount;
|
||||
lastDemoStep = 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,208 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
void PlasmaDisplay::showTestPattern() {
|
||||
for (int i = 0; i < WIDTH * HEIGHT; ++i) pixels_[i] = PIX_LIT;
|
||||
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,87 @@
|
||||
// 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();
|
||||
|
||||
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(); // all dots lit (JP1 jumper 5)
|
||||
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,91 @@
|
||||
# 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 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; if the panel garbles, verify them 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** — the **panel test pattern** (all dots lit; a dead-dot check).
|
||||
- **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
|
||||
|
||||
`PlasmaDisplay` is a line-for-line port of `VPlasmaDevice`; keep the two in
|
||||
sync (same commands, fonts, orientation/`plot` mapping). To validate the
|
||||
replica, run the **differential test**: send identical byte streams to the
|
||||
hardware and to vPLASMA and compare the glass. The real panel still works, so
|
||||
it can be the third reference.
|
||||
|
||||
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`).
|
||||
Reference in New Issue
Block a user