commit 873bdcd23e3ea230e68dc539198819b2c2caeedf Author: MangaValk Date: Mon Oct 5 21:00:42 2026 +0200 Set up PlatformIO project for EL512.256-H on ESP32 PlatformIO skeleton (esp32dev, Arduino, Adafruit GFX) with a local EL512 library: EL512Panel (1 bpp framebuffer, two-bit-mode row encoder, I2S DMA refresh stub) and EL512_GFX (Adafruit_GFX subclass). Adds a test sketch, pin map, README with wiring and timing, and moves the datasheet to docs/datasheet/. Co-Authored-By: Claude Opus 5.5 (1M context) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..38a1ce3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.pdf binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71242a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# PlatformIO +.pio/ +.pioenvs/ +.piolibdeps/ + +# VS Code (PlatformIO generates these per machine) +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Editors +*.swp +*~ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a91ca8f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack", + "pioarduino.pioarduino-ide" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c35b1e --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Planar EL512.256-H on ESP32 + +Drive a Planar EL512.256-H (512 × 256, monochrome electroluminescent) panel from an +ESP-WROOM-32 (classic ESP32), with an [Adafruit GFX](https://github.com/adafruit/Adafruit-GFX-Library) +compatible driver so the usual drawing, text and font APIs work. + +The panel has no frame memory and needs a continuous video stream. The ESP32's I2S0 +peripheral in 8-bit parallel (LCD) mode streams a precomputed frame from DMA, so the +CPU is free while the panel refreshes. + +**Status:** project skeleton. Framebuffer, GFX layer and row encoder are in place; the +I2S/DMA start-up (`EL512Panel::startI2S()`) is still a stub. + +## Hardware + +- ESP-WROOM-32 dev board (`esp32dev`) +- Planar EL512.256-H, configured for **two-bit parallel mode** (mode jumper OFF, see datasheet) +- Datasheet: [docs/datasheet/EL512.256-H3 FRB.pdf](docs/datasheet/EL512.256-H3%20FRB.pdf) +- Check the panel's logic-level and supply requirements in the datasheet before wiring. + +## Wiring + +| Panel signal | Function | ESP32 GPIO | DMA byte bit | +|--------------|-----------------------------------------|-----------:|-------------:| +| VCLK | Video clock (rising edge) | 18 | I2S clock | +| VID | Pixel data, even columns | 19 | 0 | +| TVID | Pixel data, odd columns (two-bit mode) | 21 | 1 | +| HS | Horizontal sync | 22 | 2 | +| VS | Vertical sync | 23 | 3 | +| ENABLE | Display enable | 25 | — | +| _LOWPWR | Low-power mode (active low) | 26 | — | +| GND | Ground | GND | — | + +The pin map lives in [include/pins.h](include/pins.h). + +## Timing + +| Parameter | Value | +|----------------------|-----------------------------------------| +| Pixel clock (VCLK) | 5 MHz, 2 pixels per clock | +| Clocks per row | 272 (256 data + 16 blanking) | +| Lines per frame | 261 (256 visible + 5 blanking) | +| Refresh | 5 MHz / (272 × 261) ≈ 70.4 Hz | + +## Layout + +``` +platformio.ini build config (esp32dev, Arduino, Adafruit GFX) +include/pins.h pin map +src/main.cpp test sketch: test pattern + text +lib/EL512/ local library + src/EL512Panel.* framebuffer, row encoder, I2S DMA refresh (stub) + src/EL512_GFX.* Adafruit_GFX subclass +test/ unit tests (placeholder) +docs/ datasheet and notes +``` + +## Build and flash + +Requires [PlatformIO](https://platformio.org/) (CLI or the VS Code extension). + +```bash +pio run +``` + +```bash +pio run -t upload +``` + +```bash +pio device monitor +``` + +## Using the driver + +```cpp +#include +#include "pins.h" + +EL512Panel panel({el512pins::VCLK, el512pins::VID, el512pins::TVID, el512pins::HS, + el512pins::VS, el512pins::ENABLE, el512pins::LOWPWR_N}); +EL512_GFX display(panel); + +void setup() { + display.begin(); + display.setTextColor(1); + display.print("Hello"); + display.display(); // push the drawing buffer to the refresh stream +} +``` + +See [docs/README.md](docs/README.md) for design notes and the plan. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..81bc4e5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,36 @@ +# Notes + +- Plan: https://claude.ai/code/artifact/3b9eda11-a87e-4eea-bbb9-e6454e558991 +- Datasheet: [datasheet/EL512.256-H3 FRB.pdf](datasheet/EL512.256-H3%20FRB.pdf) + +## Approach + +I2S0 in 8-bit parallel (LCD) mode with a circular DMA descriptor chain. Each DMA byte +is one VCLK period: + +| Bit | Signal | +|-----|--------| +| 0 | VID (even columns) | +| 1 | TVID (odd columns) | +| 2 | HS | +| 3 | VS | + +The I2S clock output drives VCLK. ENABLE (GPIO 25) and _LOWPWR (GPIO 26) are plain GPIO. + +## From the datasheet + +- Two-bit parallel mode is selected with the mode jumper OFF. Even columns on VID, + odd columns on TVID; 256 VCLKs per row. +- VCLK is active on the rising edge and should be kept running. +- HS is high while the pixels of a row are clocked in. The number of VCLKs during HS + high should be even. +- The topmost row is the first HS high time ending after the rising edge of VS. + +## Open items + +- [ ] Implement `EL512Panel::startI2S()` (I2S0 LCD mode, clock dividers for 5 MHz, + GPIO matrix routing, circular `lldesc_t` chain). +- [ ] Handle the 8-bit LCD-mode byte-pair swap in `encodeRow()`. +- [ ] Check VS/HS setup and hold (T4/T5) and VCLK polarity on a scope. +- [ ] Tear-free updates (double buffer or swap at VS). +- [ ] Unit test for `encodeRow()`. diff --git a/docs/datasheet/EL512.256-H3 FRB.pdf b/docs/datasheet/EL512.256-H3 FRB.pdf new file mode 100644 index 0000000..edab6a1 Binary files /dev/null and b/docs/datasheet/EL512.256-H3 FRB.pdf differ diff --git a/include/pins.h b/include/pins.h new file mode 100644 index 0000000..befe81f --- /dev/null +++ b/include/pins.h @@ -0,0 +1,22 @@ +// Pin map for the EL512.256-H panel on an ESP-WROOM-32 (esp32dev). +// +// VCLK, VID, TVID, HS and VS are driven by I2S0 in 8-bit parallel (LCD) mode, +// so their levels come from a DMA byte stream. ENABLE and _LOWPWR are plain GPIO. +#pragma once + +#include + +namespace el512pins { + +// I2S-driven signals +constexpr int8_t VCLK = 18; // I2S0 WS/clock out -> panel VCLK +constexpr int8_t VID = 19; // byte bit 0 +constexpr int8_t TVID = 21; // byte bit 1 +constexpr int8_t HS = 22; // byte bit 2 +constexpr int8_t VS = 23; // byte bit 3 + +// Static control lines +constexpr int8_t ENABLE = 25; // high = display enabled +constexpr int8_t LOWPWR_N = 26; // _LOWPWR, active low + +} // namespace el512pins diff --git a/lib/EL512/library.json b/lib/EL512/library.json new file mode 100644 index 0000000..857c373 --- /dev/null +++ b/lib/EL512/library.json @@ -0,0 +1,11 @@ +{ + "name": "EL512", + "version": "0.1.0", + "description": "Planar EL512.256-H electroluminescent panel driver for ESP32 (I2S parallel DMA) with Adafruit GFX support", + "keywords": "display, electroluminescent, planar, el512, esp32, i2s, gfx", + "frameworks": "arduino", + "platforms": "espressif32", + "dependencies": { + "adafruit/Adafruit GFX Library": "*" + } +} diff --git a/lib/EL512/src/EL512.h b/lib/EL512/src/EL512.h new file mode 100644 index 0000000..4a6222c --- /dev/null +++ b/lib/EL512/src/EL512.h @@ -0,0 +1,5 @@ +// Umbrella header for the EL512 library. +#pragma once + +#include "EL512Panel.h" +#include "EL512_GFX.h" diff --git a/lib/EL512/src/EL512Panel.cpp b/lib/EL512/src/EL512Panel.cpp new file mode 100644 index 0000000..169ef5a --- /dev/null +++ b/lib/EL512/src/EL512Panel.cpp @@ -0,0 +1,120 @@ +#include "EL512Panel.h" + +#include +#include + +EL512Panel::EL512Panel(const EL512Pins& pins) : _pins(pins) {} + +EL512Panel::~EL512Panel() { end(); } + +bool EL512Panel::begin() { + if (_running) return true; + + pinMode(_pins.enable, OUTPUT); + pinMode(_pins.lowpwr_n, OUTPUT); + setEnabled(false); + setLowPower(false); + + _fb = (uint8_t*)calloc(FRAMEBUFFER_BYTES, 1); + // Full-frame stream: 261 * 272 = ~71 KB of DMA-capable internal RAM. + // (A line ring buffer refilled from the DMA EOF interrupt would cut this + // to a few KB if RAM gets tight.) + _stream = (uint8_t*)heap_caps_malloc(STREAM_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + if (!_fb || !_stream) { + end(); + return false; + } + show(); + + if (!startI2S()) { + end(); + return false; + } + _running = true; + setEnabled(true); + return true; +} + +void EL512Panel::end() { + setEnabled(false); + stopI2S(); + _running = false; + free(_fb); + _fb = nullptr; + heap_caps_free(_stream); + _stream = nullptr; +} + +void EL512Panel::setEnabled(bool on) { digitalWrite(_pins.enable, on ? HIGH : LOW); } + +void EL512Panel::setLowPower(bool on) { digitalWrite(_pins.lowpwr_n, on ? LOW : HIGH); } + +void EL512Panel::setPixel(int16_t x, int16_t y, bool on) { + if (!_fb || x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) return; + uint8_t& b = _fb[(y * WIDTH + x) >> 3]; + const uint8_t mask = 0x80 >> (x & 7); + if (on) + b |= mask; + else + b &= ~mask; +} + +bool EL512Panel::getPixel(int16_t x, int16_t y) const { + if (!_fb || x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT) return false; + return _fb[(y * WIDTH + x) >> 3] & (0x80 >> (x & 7)); +} + +void EL512Panel::clear(bool on) { + if (_fb) memset(_fb, on ? 0xFF : 0x00, FRAMEBUFFER_BYTES); +} + +void EL512Panel::encodeRow(uint16_t line, uint8_t* out) const { + // VS is held high during the last blanking line so that the first HS high + // time ending after its rising edge is row 0 (datasheet: video input timing). + // TODO: verify VS/HS setup and hold (T4/T5) on hardware. + const uint8_t vs = (line == LINES_PER_FRAME - 1) ? BIT_VS : 0; + + if (line >= HEIGHT) { + memset(out, vs, CLOCKS_PER_ROW); + return; + } + + const uint8_t* src = _fb + (size_t)line * (WIDTH / 8); + for (uint16_t c = 0; c < DATA_CLOCKS_PER_ROW; c++) { + // Clock c carries columns 2c (VID, even) and 2c+1 (TVID, odd). + const uint8_t bits = (uint8_t)(src[c >> 2] << ((c & 3) * 2)); + uint8_t v = BIT_HS | vs; + if (bits & 0x80) v |= BIT_VID; + if (bits & 0x40) v |= BIT_TVID; + out[c] = v; + } + memset(out + DATA_CLOCKS_PER_ROW, vs, BLANK_CLOCKS_PER_ROW); + + // NOTE: in 8-bit I2S LCD mode the ESP32 emits bytes in swapped pairs + // (byte 1, byte 0, byte 3, byte 2, ...). Swap here once the DMA path is live. +} + +void EL512Panel::show() { + if (!_fb || !_stream) return; + // TODO: double-buffer or sync to VS to avoid tearing. + for (uint16_t line = 0; line < LINES_PER_FRAME; line++) { + encodeRow(line, _stream + (size_t)line * CLOCKS_PER_ROW); + } +} + +bool EL512Panel::startI2S() { + // TODO: configure I2S0 for 8-bit parallel LCD mode: + // - periph_module_enable(PERIPH_I2S0_MODULE), reset TX / FIFO / DMA + // - I2S0.conf2.lcd_en = 1, 8-bit TX, clock dividers -> PIXEL_CLOCK_HZ + // - route stream bits 0..3 to vid/tvid/hs/vs with gpio_matrix_out() + // (I2S0O_DATA_OUT8_IDX.. for 8-bit mode - verify on the core version) + // - route the I2S clock (I2S0O_WS_OUT_IDX) to vclk + // - build a circular lldesc_t chain over _stream (<= 4092 bytes each) + // - start TX via out_link.start + log_w("EL512Panel: I2S DMA refresh not implemented yet (stub)"); + return true; +} + +void EL512Panel::stopI2S() { + // TODO: stop I2S0 TX and release the DMA descriptors. +} diff --git a/lib/EL512/src/EL512Panel.h b/lib/EL512/src/EL512Panel.h new file mode 100644 index 0000000..8eb4271 --- /dev/null +++ b/lib/EL512/src/EL512Panel.h @@ -0,0 +1,92 @@ +// EL512Panel - low-level refresh layer for the Planar EL512.256-H. +// +// The panel has no frame memory of its own: it must be refreshed continuously. +// We drive it from I2S0 in 8-bit parallel (LCD) mode with a circular DMA +// descriptor chain, one byte per VCLK: +// +// byte bit 0 = VID (even columns) +// byte bit 1 = TVID (odd columns) +// byte bit 2 = HS (high while the 256 data clocks of a row are sent) +// byte bit 3 = VS (marks the top row) +// I2S clock -> VCLK (panel samples data on the rising edge) +// +// Panel in two-bit parallel mode (jumper OFF, see datasheet), 5 MHz VCLK: +// 272 clocks per row (256 data + 16 blanking), 261 rows per frame +// (256 visible + 5 blanking) -> 5e6 / (272 * 261) ~= 70.4 Hz. +// +// STATUS: the framebuffer and the row encoder are implemented; the I2S/DMA +// start-up in startI2S() is still a TODO (see EL512Panel.cpp). +#pragma once + +#include + +struct EL512Pins { + int8_t vclk; + int8_t vid; + int8_t tvid; + int8_t hs; + int8_t vs; + int8_t enable; + int8_t lowpwr_n; +}; + +class EL512Panel { + public: + static constexpr uint16_t WIDTH = 512; + static constexpr uint16_t HEIGHT = 256; + + // Timing (two-bit parallel mode) + static constexpr uint32_t PIXEL_CLOCK_HZ = 5000000; + static constexpr uint16_t DATA_CLOCKS_PER_ROW = WIDTH / 2; // 2 px per VCLK + static constexpr uint16_t CLOCKS_PER_ROW = 272; + static constexpr uint16_t BLANK_CLOCKS_PER_ROW = CLOCKS_PER_ROW - DATA_CLOCKS_PER_ROW; + static constexpr uint16_t LINES_PER_FRAME = 261; + static constexpr uint16_t BLANK_LINES = LINES_PER_FRAME - HEIGHT; + + // DMA stream byte bits + static constexpr uint8_t BIT_VID = 1 << 0; + static constexpr uint8_t BIT_TVID = 1 << 1; + static constexpr uint8_t BIT_HS = 1 << 2; + static constexpr uint8_t BIT_VS = 1 << 3; + + static constexpr size_t FRAMEBUFFER_BYTES = (size_t)WIDTH * HEIGHT / 8; + static constexpr size_t STREAM_BYTES = (size_t)LINES_PER_FRAME * CLOCKS_PER_ROW; + + explicit EL512Panel(const EL512Pins& pins); + ~EL512Panel(); + + // Allocates buffers, sets up control GPIOs and (TODO) starts I2S DMA refresh. + bool begin(); + void end(); + + // Display on/off (ENABLE) and low-power mode (_LOWPWR, active low). + void setEnabled(bool on); + void setLowPower(bool on); + + // 1 bpp framebuffer, row-major, MSB = leftmost pixel. + uint8_t* framebuffer() { return _fb; } + void setPixel(int16_t x, int16_t y, bool on); + bool getPixel(int16_t x, int16_t y) const; + void clear(bool on = false); + + // Encode the framebuffer into the DMA stream (call after drawing). + void show(); + + // Encode one scan line (0..LINES_PER_FRAME-1) into CLOCKS_PER_ROW bytes. + // Lines >= HEIGHT produce blanking. Public so it can be unit-tested. + void encodeRow(uint16_t line, uint8_t* out) const; + + // Frame refresh rate implied by the timing constants. + static constexpr float refreshHz() { + return (float)PIXEL_CLOCK_HZ / ((float)CLOCKS_PER_ROW * LINES_PER_FRAME); + } + + private: + bool startI2S(); + void stopI2S(); + + EL512Pins _pins; + uint8_t* _fb = nullptr; // 1 bpp drawing buffer + uint8_t* _stream = nullptr; // STREAM_BYTES, DMA-capable + bool _running = false; +}; diff --git a/lib/EL512/src/EL512_GFX.cpp b/lib/EL512/src/EL512_GFX.cpp new file mode 100644 index 0000000..5f4013c --- /dev/null +++ b/lib/EL512/src/EL512_GFX.cpp @@ -0,0 +1,29 @@ +#include "EL512_GFX.h" + +EL512_GFX::EL512_GFX(EL512Panel& panel) + : Adafruit_GFX(EL512Panel::WIDTH, EL512Panel::HEIGHT), _panel(panel) {} + +void EL512_GFX::drawPixel(int16_t x, int16_t y, uint16_t color) { + if (x < 0 || y < 0 || x >= width() || y >= height()) return; + + int16_t t; + switch (getRotation()) { + case 1: + t = x; + x = WIDTH - 1 - y; + y = t; + break; + case 2: + x = WIDTH - 1 - x; + y = HEIGHT - 1 - y; + break; + case 3: + t = x; + x = y; + y = HEIGHT - 1 - t; + break; + } + _panel.setPixel(x, y, color != 0); +} + +void EL512_GFX::fillScreen(uint16_t color) { _panel.clear(color != 0); } diff --git a/lib/EL512/src/EL512_GFX.h b/lib/EL512/src/EL512_GFX.h new file mode 100644 index 0000000..a85a91b --- /dev/null +++ b/lib/EL512/src/EL512_GFX.h @@ -0,0 +1,25 @@ +// EL512_GFX - Adafruit_GFX front-end for the EL512.256-H panel. +#pragma once + +#include + +#include "EL512Panel.h" + +class EL512_GFX : public Adafruit_GFX { + public: + explicit EL512_GFX(EL512Panel& panel); + + bool begin() { return _panel.begin(); } + + // Any non-zero color lights the pixel. + void drawPixel(int16_t x, int16_t y, uint16_t color) override; + void fillScreen(uint16_t color) override; + + // Encode the drawing buffer into the panel refresh stream. + void display() { _panel.show(); } + + EL512Panel& panel() { return _panel; } + + private: + EL512Panel& _panel; +}; diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..69354c4 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,12 @@ +; Planar EL512.256-H electroluminescent panel driver for ESP32 (ESP-WROOM-32) +; https://docs.platformio.org/page/projectconf.html + +[env:esp32dev] +platform = espressif32@6.10.0 +board = esp32dev +framework = arduino +monitor_speed = 115200 +lib_deps = + adafruit/Adafruit GFX Library +build_flags = + -Wall diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..d50bca7 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,63 @@ +// Test sketch: bring up the EL512.256-H and draw a test pattern with Adafruit GFX. +#include +#include + +#include "pins.h" + +static EL512Panel panel({ + el512pins::VCLK, + el512pins::VID, + el512pins::TVID, + el512pins::HS, + el512pins::VS, + el512pins::ENABLE, + el512pins::LOWPWR_N, +}); +static EL512_GFX display(panel); + +static void drawTestPattern() { + display.fillScreen(0); + display.drawRect(0, 0, display.width(), display.height(), 1); + display.drawLine(0, 0, display.width() - 1, display.height() - 1, 1); + display.drawLine(display.width() - 1, 0, 0, display.height() - 1, 1); + + // Checkerboard strip to check VID/TVID column ordering. + for (int16_t y = 8; y < 24; y++) + for (int16_t x = 8; x < 136; x++) + if ((x ^ y) & 1) display.drawPixel(x, y, 1); + + display.fillCircle(display.width() / 2, display.height() / 2, 40, 1); + display.setTextColor(1); + display.setTextSize(2); + display.setCursor(8, 32); + display.print("EL512.256-H"); + display.setTextSize(1); + display.setCursor(8, 56); + display.printf("%ux%u %.1f Hz", EL512Panel::WIDTH, EL512Panel::HEIGHT, + EL512Panel::refreshHz()); + display.display(); +} + +void setup() { + Serial.begin(115200); + Serial.println("EL512.256-H test"); + + if (!display.begin()) { + Serial.println("panel init failed (out of DMA memory?)"); + while (true) delay(1000); + } + drawTestPattern(); + Serial.printf("free heap: %u\n", ESP.getFreeHeap()); +} + +void loop() { + static uint32_t last = 0; + static uint32_t frames = 0; + if (millis() - last >= 1000) { + last = millis(); + display.fillRect(8, 72, 120, 8, 0); + display.setCursor(8, 72); + display.printf("uptime %lus", (unsigned long)frames++); + display.display(); + } +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..44b0a38 --- /dev/null +++ b/test/README @@ -0,0 +1,9 @@ +Unit tests (PlatformIO Unit Testing / Unity) go here. + +Planned first test: EL512Panel::encodeRow() - check that HS is high for exactly +256 clocks per visible row, VS appears only on the last blanking line, and that +even/odd columns land on VID/TVID. + +Run with: pio test -e esp32dev + +More info: https://docs.platformio.org/en/latest/advanced/unit-testing/index.html