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
@@ -1,261 +0,0 @@
|
||||
#include "EL512Panel.h"
|
||||
|
||||
#include <driver/periph_ctrl.h>
|
||||
#include <esp32/rom/lldesc.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_rom_gpio.h>
|
||||
#include <soc/gpio_sig_map.h>
|
||||
#include <soc/i2s_reg.h>
|
||||
#include <soc/i2s_struct.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// I2S0 clock source when clka_en = 0: PLL_D2_CLK, 160 MHz whatever the CPU clock.
|
||||
constexpr uint32_t I2S_SOURCE_CLOCK_HZ = 160000000;
|
||||
constexpr uint32_t I2S_BYTE_RATE_HZ = EL512Panel::PIXEL_CLOCK_HZ * EL512Panel::BYTES_PER_CLOCK;
|
||||
constexpr uint32_t I2S_CLKM_DIV = I2S_SOURCE_CLOCK_HZ / I2S_BYTE_RATE_HZ;
|
||||
static_assert(I2S_SOURCE_CLOCK_HZ % I2S_BYTE_RATE_HZ == 0,
|
||||
"byte rate must divide 160 MHz exactly");
|
||||
static_assert(I2S_CLKM_DIV >= 2 && I2S_CLKM_DIV <= 255, "clkm_div_num out of range");
|
||||
|
||||
// In 8-bit LCD mode I2S0 puts its byte on DATA_OUT16..23 (bitluni ESP32Lib).
|
||||
constexpr uint32_t I2S0_DATA_SIGNAL_BASE = I2S0O_DATA_OUT16_IDX;
|
||||
|
||||
// BCK runs at half the byte rate (measured: 2.5 MHz BCK at 5 MHz bytes), so each
|
||||
// VCLK period spans two identical bytes. Uninverted, the rising edge landed on the
|
||||
// byte-pair boundary and the image jittered; inverted, it sits mid-pair (verified
|
||||
// on the panel).
|
||||
constexpr bool INVERT_VCLK = true;
|
||||
|
||||
// One DMA descriptor per stream chunk (descriptor limit 4095 bytes).
|
||||
static_assert(EL512Panel::LINES_PER_CHUNK * EL512Panel::ROW_BYTES <= 4092, "descriptor too long");
|
||||
static_assert(EL512Panel::ROW_BYTES % 4 == 0, "rows must be whole DMA words");
|
||||
|
||||
void resetI2S0() {
|
||||
const uint32_t lcReset = I2S_IN_RST_M | I2S_OUT_RST_M | I2S_AHBM_RST_M | I2S_AHBM_FIFO_RST_M;
|
||||
I2S0.lc_conf.val |= lcReset;
|
||||
I2S0.lc_conf.val &= ~lcReset;
|
||||
const uint32_t confReset =
|
||||
I2S_RX_RESET_M | I2S_RX_FIFO_RESET_M | I2S_TX_RESET_M | I2S_TX_FIFO_RESET_M;
|
||||
I2S0.conf.val |= confReset;
|
||||
I2S0.conf.val &= ~confReset;
|
||||
}
|
||||
|
||||
void routeOutput(int8_t pin, uint32_t signal, bool invert) {
|
||||
if (pin < 0) return;
|
||||
pinMode(pin, OUTPUT);
|
||||
esp_rom_gpio_connect_out_signal(pin, signal, invert, false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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 * 544 = ~142 KB of DMA-capable internal RAM, in chunks.
|
||||
// (A line ring buffer refilled from the DMA EOF interrupt would cut this
|
||||
// to a few KB if RAM gets tight.)
|
||||
bool ok = _fb != nullptr;
|
||||
for (uint16_t i = 0; ok && i < CHUNK_COUNT; i++) {
|
||||
_chunks[i] = (uint8_t*)heap_caps_malloc((size_t)chunkLines(i) * ROW_BYTES,
|
||||
MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||||
ok = _chunks[i] != nullptr;
|
||||
}
|
||||
if (!ok) {
|
||||
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;
|
||||
for (uint8_t*& chunk : _chunks) {
|
||||
heap_caps_free(chunk);
|
||||
chunk = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t EL512Panel::chunkLines(uint16_t chunk) {
|
||||
const uint16_t first = chunk * LINES_PER_CHUNK;
|
||||
return min<uint16_t>(LINES_PER_CHUNK, LINES_PER_FRAME - first);
|
||||
}
|
||||
|
||||
uint8_t* EL512Panel::linePtr(uint16_t line) const {
|
||||
return _chunks[line / LINES_PER_CHUNK] + (size_t)(line % LINES_PER_CHUNK) * ROW_BYTES;
|
||||
}
|
||||
|
||||
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 Data Input).
|
||||
// VS rises 1104 clocks after the previous HS fall (T4 >= 1 VCLK) and 272
|
||||
// clocks = 54.4 us before the next HS rise (T5 >= 3 us); check on a scope.
|
||||
const uint8_t vs = (line == LINES_PER_FRAME - 1) ? BIT_VS : 0;
|
||||
|
||||
if (line >= HEIGHT) {
|
||||
memset(out, vs, ROW_BYTES);
|
||||
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 x = 2c and 2c+1. The datasheet numbers columns from 1, so
|
||||
// x = 2c is column 2c+1 (odd -> TVID) and x = 2c+1 is column 2c+2 (even -> VID).
|
||||
const uint8_t bits = (uint8_t)(src[c >> 2] << ((c & 3) * 2));
|
||||
uint8_t v = BIT_HS | vs;
|
||||
if (bits & 0x80) v |= BIT_TVID;
|
||||
if (bits & 0x40) v |= BIT_VID;
|
||||
// Two bytes per VCLK; 8-bit I2S LCD mode sends each 32-bit word as bytes 2, 3, 0, 1.
|
||||
const uint16_t i = c * BYTES_PER_CLOCK;
|
||||
out[i ^ 2] = v;
|
||||
out[(i + 1) ^ 2] = v;
|
||||
}
|
||||
// Blanking is uniform and starts on a word boundary, so it needs no swap.
|
||||
memset(out + DATA_CLOCKS_PER_ROW * BYTES_PER_CLOCK, vs, BLANK_CLOCKS_PER_ROW * BYTES_PER_CLOCK);
|
||||
}
|
||||
|
||||
void EL512Panel::show() {
|
||||
if (!_fb || !_chunks[CHUNK_COUNT - 1]) return;
|
||||
// TODO: double-buffer or sync to VS to avoid tearing.
|
||||
for (uint16_t line = 0; line < LINES_PER_FRAME; line++) {
|
||||
encodeRow(line, linePtr(line));
|
||||
}
|
||||
}
|
||||
|
||||
bool EL512Panel::startI2S() {
|
||||
// Circular descriptor chain over the stream, one descriptor per chunk.
|
||||
_desc = (lldesc_t*)heap_caps_calloc(CHUNK_COUNT, sizeof(lldesc_t), MALLOC_CAP_DMA);
|
||||
if (!_desc) return false;
|
||||
for (size_t i = 0; i < CHUNK_COUNT; i++) {
|
||||
lldesc_t& d = _desc[i];
|
||||
d.size = chunkLines(i) * ROW_BYTES;
|
||||
d.length = d.size;
|
||||
d.owner = 1;
|
||||
d.eof = (i == CHUNK_COUNT - 1); // one EOF per frame
|
||||
d.buf = _chunks[i];
|
||||
d.qe.stqe_next = &_desc[(i + 1) % CHUNK_COUNT];
|
||||
}
|
||||
|
||||
periph_module_enable(PERIPH_I2S0_MODULE);
|
||||
resetI2S0();
|
||||
|
||||
// 8-bit parallel LCD mode, one byte per BCK period (bitluni ESP32Lib setup;
|
||||
// the c ^ 2 byte order in encodeRow() depends on these FIFO/channel modes).
|
||||
I2S0.conf2.val = 0;
|
||||
I2S0.conf2.lcd_en = 1;
|
||||
I2S0.conf2.lcd_tx_wrx2_en = 1;
|
||||
I2S0.conf2.lcd_tx_sdx2_en = 0;
|
||||
|
||||
I2S0.sample_rate_conf.val = 0;
|
||||
I2S0.sample_rate_conf.tx_bits_mod = 8;
|
||||
I2S0.sample_rate_conf.tx_bck_div_num = 1;
|
||||
|
||||
// PLL_D2_CLK / (N + b/a) with b = 0: 160 MHz / 16 = 10 MHz bytes -> 5 MHz BCK.
|
||||
I2S0.clkm_conf.val = 0;
|
||||
I2S0.clkm_conf.clka_en = 0;
|
||||
I2S0.clkm_conf.clkm_div_num = I2S_CLKM_DIV;
|
||||
I2S0.clkm_conf.clkm_div_a = 1;
|
||||
I2S0.clkm_conf.clkm_div_b = 0;
|
||||
|
||||
I2S0.fifo_conf.val = 0;
|
||||
I2S0.fifo_conf.tx_fifo_mod_force_en = 1;
|
||||
I2S0.fifo_conf.tx_fifo_mod = 1;
|
||||
I2S0.fifo_conf.tx_data_num = 32;
|
||||
I2S0.fifo_conf.dscr_en = 1;
|
||||
|
||||
I2S0.conf1.val = 0;
|
||||
I2S0.conf1.tx_stop_en = 0;
|
||||
I2S0.conf1.tx_pcm_bypass = 1;
|
||||
|
||||
I2S0.conf_chan.val = 0;
|
||||
I2S0.conf_chan.tx_chan_mod = 1;
|
||||
|
||||
I2S0.conf.tx_right_first = 1;
|
||||
I2S0.conf.tx_msb_right = 0;
|
||||
I2S0.conf.tx_msb_shift = 0;
|
||||
I2S0.conf.tx_mono = 0;
|
||||
I2S0.conf.tx_short_sync = 0;
|
||||
I2S0.timing.val = 0;
|
||||
|
||||
// Stream bit n -> DATA_OUT(16 + n); BCK -> VCLK.
|
||||
const int8_t dataPins[] = {_pins.vid, _pins.tvid, _pins.hs, _pins.vs};
|
||||
for (uint32_t bit = 0; bit < sizeof(dataPins); bit++) {
|
||||
routeOutput(dataPins[bit], I2S0_DATA_SIGNAL_BASE + bit, false);
|
||||
}
|
||||
routeOutput(_pins.vclk, I2S0O_BCK_OUT_IDX, INVERT_VCLK);
|
||||
|
||||
// Start the DMA; it loops over the chain until stopI2S().
|
||||
I2S0.int_ena.val = 0;
|
||||
I2S0.int_clr.val = I2S0.int_raw.val;
|
||||
I2S0.lc_conf.val = I2S_OUT_DATA_BURST_EN_M | I2S_OUTDSCR_BURST_EN_M;
|
||||
I2S0.out_link.addr = (uint32_t)&_desc[0];
|
||||
I2S0.out_link.start = 1;
|
||||
I2S0.conf.tx_start = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EL512Panel::stopI2S() {
|
||||
if (!_desc) return; // never started
|
||||
|
||||
I2S0.conf.tx_start = 0;
|
||||
I2S0.out_link.stop = 1;
|
||||
resetI2S0();
|
||||
periph_module_disable(PERIPH_I2S0_MODULE);
|
||||
|
||||
// Hand the pins back to GPIO and park them low.
|
||||
const int8_t pins[] = {_pins.vclk, _pins.vid, _pins.tvid, _pins.hs, _pins.vs};
|
||||
for (int8_t pin : pins) {
|
||||
if (pin < 0) continue;
|
||||
esp_rom_gpio_connect_out_signal(pin, SIG_GPIO_OUT_IDX, false, false);
|
||||
digitalWrite(pin, LOW);
|
||||
}
|
||||
|
||||
heap_caps_free(_desc);
|
||||
_desc = nullptr;
|
||||
}
|
||||
+3
-107
@@ -1,109 +1,5 @@
|
||||
// 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. BCK (= VCLK) rises once per two stream bytes (measured
|
||||
// with PCNT), so every VCLK period is two identical bytes:
|
||||
//
|
||||
// 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 (panel samples data on the rising edge)
|
||||
//
|
||||
// Panel in two-bit parallel mode (PS1 jumper 2 removed, datasheet Table 3), 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.
|
||||
//
|
||||
// In 8-bit LCD mode the ESP32 I2S emits each 32-bit DMA word as bytes 2, 3, 0, 1,
|
||||
// so encodeRow() stores stream byte i at index i ^ 2 (as bitluni's ESP32Lib does).
|
||||
// The I2S byte rate is 2 * PIXEL_CLOCK_HZ = 10 MHz.
|
||||
// The panel driver core lives in components/el512/ so the ESPHome component and
|
||||
// this Arduino library share one copy (see platformio.ini: lib_extra_dirs).
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
struct lldesc_s; // ESP32 DMA descriptor (rom/lldesc.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;
|
||||
|
||||
// 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;
|
||||
|
||||
static constexpr size_t FRAMEBUFFER_BYTES = (size_t)WIDTH * HEIGHT / 8;
|
||||
static constexpr size_t STREAM_BYTES = (size_t)LINES_PER_FRAME * ROW_BYTES;
|
||||
|
||||
// The ~142 KB stream is too big for one DMA block, so it is allocated in
|
||||
// chunks of whole lines, one per DMA descriptor (7 * 544 = 3808 <= 4092 bytes).
|
||||
static constexpr uint16_t LINES_PER_CHUNK = 7;
|
||||
static constexpr uint16_t CHUNK_COUNT = (LINES_PER_FRAME + LINES_PER_CHUNK - 1) / LINES_PER_CHUNK;
|
||||
|
||||
explicit EL512Panel(const EL512Pins& pins);
|
||||
~EL512Panel();
|
||||
|
||||
// Allocates buffers, sets up control GPIOs and starts the 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 ROW_BYTES bytes, in DMA
|
||||
// order (stream byte i at index i ^ 2). 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();
|
||||
static uint16_t chunkLines(uint16_t chunk);
|
||||
uint8_t* linePtr(uint16_t line) const;
|
||||
|
||||
EL512Pins _pins;
|
||||
uint8_t* _fb = nullptr; // 1 bpp drawing buffer
|
||||
uint8_t* _chunks[CHUNK_COUNT] = {}; // stream, DMA-capable, LINES_PER_CHUNK lines each
|
||||
lldesc_s* _desc = nullptr; // circular DMA descriptor chain, one per chunk
|
||||
bool _running = false;
|
||||
};
|
||||
#include "el512_panel.h"
|
||||
Reference in new issue
Block a user