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) <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-05 21:00:42 +02:00
commit 873bdcd23e
16 files changed
+555

No files matched your search

+11
View File
@@ -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": "*"
}
}
+5
View File
@@ -0,0 +1,5 @@
// Umbrella header for the EL512 library.
#pragma once
#include "EL512Panel.h"
#include "EL512_GFX.h"
+120
View File
@@ -0,0 +1,120 @@
#include "EL512Panel.h"
#include <esp_heap_caps.h>
#include <string.h>
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.
}
+92
View File
@@ -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 <Arduino.h>
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;
};
+29
View File
@@ -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); }
+25
View File
@@ -0,0 +1,25 @@
// EL512_GFX - Adafruit_GFX front-end for the EL512.256-H panel.
#pragma once
#include <Adafruit_GFX.h>
#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;
};