Public Access
Add ESPHome display component; ring-buffer driver shared with PlatformIO
Driver (components/el512/el512_panel.*, ESP-IDF only): - DMA ring of 8 x 4 lines (17 KB) refilled from the I2S EOF interrupt (IRAM) instead of a 142 KB full-frame stream; ~107 KB heap freed. - 256-entry lookup table encodes 8 pixels per step. - Back buffer + displayed buffer, swapped at the start of a frame. - PlatformIO builds it via lib_extra_dirs (EL512Core); lib/EL512 keeps the Adafruit GFX front end. ESPHome external component (platform: el512): - DisplayBuffer with optional ENABLE / _LOWPWR pins, set_enabled(), set_low_power(), helper functions for page lambdas. - still_alive_page: plays the Portal end-credits show, then returns to the previous page. Still Alive engine moved to components/el512/still_alive.* with a built-in 5x7 font (from Adafruit GFX, BSD); the PlatformIO demo uses the same code. The importer writes components/el512/still_alive_data.h and stops at 200 s (--until-ms), matching the hand-trimmed data. esphome/el512-panel.yaml: 8 pages (clock, forecast, agenda, dashboard, now playing, Plex, solar, Still Alive), page select, rotation, display and low-power switches, message text. home_assistant/packages/el512_panel.yaml: helper template sensors for forecast, agenda, now playing and Plex streams. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
410885de24
commit
963590cdfe
27 files changed
+2110
-828
No files matched your search
@@ -0,0 +1,128 @@
|
||||
// EL512Panel - refresh driver for the Planar EL512.256-H on an ESP32.
|
||||
//
|
||||
// Uses only ESP-IDF APIs, so the same code builds for Arduino/PlatformIO
|
||||
// (lib/EL512) and for the ESPHome component in this folder.
|
||||
//
|
||||
// The panel has no frame memory: it must be refreshed continuously. I2S0 in
|
||||
// 8-bit parallel (LCD) mode streams the signals by DMA from a small ring of
|
||||
// encoded lines (CHUNK_COUNT chunks of LINES_PER_CHUNK lines). When a chunk has
|
||||
// been sent, the DMA EOF interrupt re-encodes it with the lines that come
|
||||
// RING_LINES later, so the ring stays ~1.7 ms ahead of the panel.
|
||||
//
|
||||
// byte bit 0 = VID (even columns 2..512 = x 1, 3, ..., 511)
|
||||
// byte bit 1 = TVID (odd columns 1..511 = x 0, 2, ..., 510)
|
||||
// byte bit 2 = HS (high while the 256 data clocks of a row are sent)
|
||||
// byte bit 3 = VS (marks the top row)
|
||||
// I2S BCK -> VCLK (inverted, see the .cpp)
|
||||
//
|
||||
// BCK rises once per two stream bytes (measured with PCNT), so every VCLK
|
||||
// period is two identical bytes; the I2S byte rate is 10 MHz for a 5 MHz VCLK.
|
||||
// In 8-bit LCD mode the I2S emits each 32-bit DMA word as bytes 2, 3, 0, 1, so
|
||||
// stream byte i is stored at index i ^ 2.
|
||||
//
|
||||
// Panel in two-bit parallel mode (PS1 jumper 2 removed, datasheet Table 3):
|
||||
// 272 clocks per row (256 data + 16 blanking), 261 rows per frame
|
||||
// (256 visible + 5 blanking) -> 5e6 / (272 * 261) ~= 70.4 Hz.
|
||||
//
|
||||
// Drawing goes into a back buffer; show() copies it to the displayed buffer at
|
||||
// the start of the next frame, so a redraw never shows half-drawn.
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct lldesc_s; // ESP32 DMA descriptor
|
||||
struct intr_handle_data_t; // esp_intr_alloc handle
|
||||
|
||||
struct EL512Pins {
|
||||
int8_t vclk;
|
||||
int8_t vid;
|
||||
int8_t tvid;
|
||||
int8_t hs;
|
||||
int8_t vs;
|
||||
int8_t enable; // -1 if not connected (panel pulls it high)
|
||||
int8_t lowpwr_n; // -1 if not connected (panel pulls it high)
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// Stream bytes per VCLK period (BCK runs at half the I2S byte rate).
|
||||
static constexpr uint8_t BYTES_PER_CLOCK = 2;
|
||||
static constexpr uint16_t ROW_BYTES = CLOCKS_PER_ROW * BYTES_PER_CLOCK;
|
||||
|
||||
// DMA ring: CHUNK_COUNT descriptors of LINES_PER_CHUNK lines each.
|
||||
static constexpr uint16_t LINES_PER_CHUNK = 4;
|
||||
static constexpr uint16_t CHUNK_COUNT = 8;
|
||||
static constexpr uint16_t RING_LINES = LINES_PER_CHUNK * CHUNK_COUNT;
|
||||
static constexpr size_t RING_BYTES = (size_t)RING_LINES * ROW_BYTES;
|
||||
|
||||
static constexpr size_t FRAMEBUFFER_BYTES = (size_t)WIDTH * HEIGHT / 8;
|
||||
|
||||
explicit EL512Panel(const EL512Pins& pins);
|
||||
~EL512Panel();
|
||||
|
||||
// Allocates buffers, sets up control GPIOs and starts the I2S DMA refresh.
|
||||
bool begin();
|
||||
void end();
|
||||
bool running() const { return _running; }
|
||||
|
||||
// Display on/off (ENABLE) and low-power mode (_LOWPWR, active low).
|
||||
void setEnabled(bool on);
|
||||
void setLowPower(bool on);
|
||||
bool enabled() const { return _enabled; }
|
||||
bool lowPower() const { return _lowPower; }
|
||||
|
||||
// 1 bpp drawing buffer, 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);
|
||||
|
||||
// Hand the drawing buffer to the refresh. Copies it at the start of the next
|
||||
// frame and waits for that (at most one frame, ~14 ms).
|
||||
void show();
|
||||
|
||||
// Encode one scan line (0..LINES_PER_FRAME-1) of the displayed buffer into
|
||||
// ROW_BYTES bytes in DMA order. Lines >= HEIGHT produce blanking.
|
||||
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();
|
||||
void fillChunk(uint16_t chunk);
|
||||
static void isr(void* arg);
|
||||
|
||||
EL512Pins _pins;
|
||||
uint8_t* _fb = nullptr; // drawing buffer
|
||||
uint8_t* _front = nullptr; // buffer being displayed
|
||||
uint8_t* _ring = nullptr; // RING_BYTES, DMA-capable
|
||||
lldesc_s* _desc = nullptr; // CHUNK_COUNT descriptors, circular
|
||||
intr_handle_data_t* _intr = nullptr;
|
||||
uint16_t _chunkLine[CHUNK_COUNT] = {}; // first line currently in each chunk
|
||||
volatile bool _swapPending = false;
|
||||
bool _running = false;
|
||||
bool _enabled = false;
|
||||
bool _lowPower = false;
|
||||
};
|
||||
Reference in new issue
Block a user