Files
mangavalkandClaude Opus 5.5 6d5e5315f0 Fix PlatformIO build; document ESPHome component and ring-buffer driver
- el512_panel.cpp: esp_cpu_get_cycle_count() exists only in ESP-IDF 5;
  use cpu_hal_get_cycle_count() on 4.4 (Arduino 2.x), which broke both
  PlatformIO environments.
- README: ring-buffer refresh, tear-free status, D1 mini ESP32, new layout
  (components/el512, esphome/, home_assistant/), ESPHome setup, Still Alive
  paths, and the Wi-Fi blanking findings with wiring advice.
- docs/README: ring, lookup table, double buffer, health counters; tear-free
  and memory items done; Wi-Fi blanking and boot blanking as open items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:05:06 +02:00

367 lines
12 KiB
C++

#include "el512_panel.h"
#include <driver/gpio.h>
#include <esp_attr.h>
#include <esp_idf_version.h>
#include <esp_heap_caps.h>
#include <esp_intr_alloc.h>
#include <esp_rom_gpio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <soc/gpio_sig_map.h>
#include <soc/i2s_reg.h>
#include <soc/i2s_struct.h>
#include <soc/periph_defs.h>
#include <string.h>
#if __has_include(<esp_private/periph_ctrl.h>)
#include <esp_private/periph_ctrl.h>
#else
#include <driver/periph_ctrl.h>
#endif
// CPU cycle counter: ESP-IDF 5 (ESPHome) and 4.4 (Arduino 2.x / PlatformIO).
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#include <esp_cpu.h>
#define EL512_CYCLES() esp_cpu_get_cycle_count()
#else
#include <hal/cpu_hal.h>
#define EL512_CYCLES() cpu_hal_get_cycle_count()
#endif
#if __has_include(<esp32/rom/lldesc.h>)
#include <esp32/rom/lldesc.h>
#else
#include <rom/lldesc.h>
#endif
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;
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");
static_assert(EL512Panel::RING_LINES < EL512Panel::LINES_PER_FRAME, "ring longer than a frame");
constexpr uint16_t FB_ROW_BYTES = EL512Panel::WIDTH / 8;
constexpr uint16_t DATA_BYTES = EL512Panel::DATA_CLOCKS_PER_ROW * EL512Panel::BYTES_PER_CLOCK;
// One framebuffer byte (8 pixels = 4 clocks) -> 8 stream bytes as two DMA words,
// already in I2S byte order. Built once in begin(); read from the ISR.
DRAM_ATTR uint32_t s_lut[256][2];
void buildLut() {
for (int b = 0; b < 256; b++) {
uint8_t bytes[8];
for (int c = 0; c < 4; 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)(b << (c * 2));
uint8_t v = EL512Panel::BIT_HS;
if (bits & 0x80) v |= EL512Panel::BIT_TVID;
if (bits & 0x40) v |= EL512Panel::BIT_VID;
bytes[(2 * c) ^ 2] = v;
bytes[(2 * c + 1) ^ 2] = v;
}
memcpy(s_lut[b], bytes, sizeof(bytes));
}
}
inline uint32_t IRAM_ATTR repeat4(uint8_t v) { return v * 0x01010101u; }
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;
esp_rom_gpio_pad_select_gpio(pin);
gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT);
esp_rom_gpio_connect_out_signal(pin, signal, invert, false);
}
void setupGpioOutput(int8_t pin, bool level) {
if (pin < 0) return;
esp_rom_gpio_pad_select_gpio(pin);
gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT);
gpio_set_level((gpio_num_t)pin, level);
}
} // namespace
EL512Panel::EL512Panel(const EL512Pins& pins) : _pins(pins) {}
EL512Panel::~EL512Panel() { end(); }
bool EL512Panel::begin() {
if (_running) return true;
setupGpioOutput(_pins.enable, false);
setupGpioOutput(_pins.lowpwr_n, true);
_enabled = false;
_lowPower = false;
buildLut();
// Both framebuffers are read from the ISR, so they must be internal RAM.
_fb = (uint8_t*)heap_caps_calloc(1, FRAMEBUFFER_BYTES, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
_front = (uint8_t*)heap_caps_calloc(1, FRAMEBUFFER_BYTES, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
_ring = (uint8_t*)heap_caps_malloc(RING_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (!_fb || !_front || !_ring || !startI2S()) {
end();
return false;
}
_running = true;
setEnabled(true);
return true;
}
void EL512Panel::end() {
if (_pins.enable >= 0) gpio_set_level((gpio_num_t)_pins.enable, 0);
_enabled = false;
stopI2S();
_running = false;
heap_caps_free(_fb);
_fb = nullptr;
heap_caps_free(_front);
_front = nullptr;
heap_caps_free(_ring);
_ring = nullptr;
}
void EL512Panel::setEnabled(bool on) {
_enabled = on;
if (_pins.enable >= 0) gpio_set_level((gpio_num_t)_pins.enable, on);
}
void EL512Panel::setLowPower(bool on) {
_lowPower = on;
if (_pins.lowpwr_n >= 0) gpio_set_level((gpio_num_t)_pins.lowpwr_n, !on);
}
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::show() {
if (!_fb || !_front) return;
if (!_running) {
memcpy(_front, _fb, FRAMEBUFFER_BYTES);
return;
}
// The ISR copies _fb to _front just before it encodes line 0 of a frame.
_swapPending = true;
for (int i = 0; i < 40 && _swapPending; i++) vTaskDelay(1);
if (_swapPending) _showTimeouts++;
}
EL512Panel::Stats EL512Panel::takeStats() {
Stats s{_refills, _maxGap, _swaps, _showTimeouts, _underruns};
_underruns = 0;
_refills = 0;
_maxGap = 0;
_swaps = 0;
_showTimeouts = 0;
return s;
}
void IRAM_ATTR 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).
const uint32_t vs = repeat4(line == LINES_PER_FRAME - 1 ? BIT_VS : 0);
uint32_t* o = (uint32_t*)out;
if (line >= HEIGHT) {
for (uint16_t i = 0; i < ROW_BYTES / 4; i++) o[i] = vs;
return;
}
const uint8_t* src = _front + (size_t)line * FB_ROW_BYTES;
for (uint16_t k = 0; k < FB_ROW_BYTES; k++) {
o[2 * k] = s_lut[src[k]][0];
o[2 * k + 1] = s_lut[src[k]][1];
}
// Blanking clocks: HS low. Uniform and word-aligned, so no byte swap needed.
for (uint16_t i = DATA_BYTES / 4; i < ROW_BYTES / 4; i++) o[i] = vs;
}
void IRAM_ATTR EL512Panel::fillChunk(uint16_t chunk) {
uint16_t line = _chunkLine[chunk];
uint8_t* out = _ring + (size_t)chunk * LINES_PER_CHUNK * ROW_BYTES;
for (uint16_t i = 0; i < LINES_PER_CHUNK; i++) {
if (line == 0 && _swapPending) {
const uint32_t* s = (const uint32_t*)_fb;
uint32_t* d = (uint32_t*)_front;
for (size_t w = 0; w < FRAMEBUFFER_BYTES / 4; w++) d[w] = s[w];
_swapPending = false;
_swaps = _swaps + 1;
}
encodeRow(line, out + (size_t)i * ROW_BYTES);
line = (line + 1 == LINES_PER_FRAME) ? 0 : line + 1;
}
}
void IRAM_ATTR EL512Panel::isr(void* arg) {
auto* self = static_cast<EL512Panel*>(arg);
const uint32_t now = EL512_CYCLES();
if (self->_lastIsrCycle != 0 && now - self->_lastIsrCycle > self->_maxGap) self->_maxGap = now - self->_lastIsrCycle;
self->_lastIsrCycle = now;
self->_refills = self->_refills + 1;
// Health counters (see takeStats): TX FIFO ran empty / transmitter hung.
const uint32_t raw = I2S0.int_raw.val;
if (raw & (I2S_TX_REMPTY_INT_RAW_M | I2S_TX_HUNG_INT_RAW_M)) {
self->_underruns = self->_underruns + 1;
I2S0.int_clr.val = I2S_TX_REMPTY_INT_CLR_M | I2S_TX_HUNG_INT_CLR_M;
}
const uint32_t status = I2S0.int_st.val;
I2S0.int_clr.val = status;
if (!(status & I2S_OUT_EOF_INT_ST_M)) return;
// The descriptor that just finished plays again RING_LINES lines later.
const lldesc_t* done = (const lldesc_t*)I2S0.out_eof_des_addr;
const uint16_t chunk = done - (const lldesc_t*)self->_desc;
if (chunk >= CHUNK_COUNT) return;
uint16_t next = self->_chunkLine[chunk] + RING_LINES;
if (next >= LINES_PER_FRAME) next -= LINES_PER_FRAME;
self->_chunkLine[chunk] = next;
self->fillChunk(chunk);
}
bool EL512Panel::startI2S() {
_desc = (lldesc_t*)heap_caps_calloc(CHUNK_COUNT, sizeof(lldesc_t), MALLOC_CAP_DMA);
if (!_desc) return false;
for (uint16_t i = 0; i < CHUNK_COUNT; i++) {
_chunkLine[i] = i * LINES_PER_CHUNK;
fillChunk(i);
lldesc_t& d = _desc[i];
d.size = LINES_PER_CHUNK * ROW_BYTES;
d.length = d.size;
d.owner = 1;
d.eof = 1; // EOF interrupt after every chunk
d.buf = _ring + (size_t)i * LINES_PER_CHUNK * ROW_BYTES;
d.qe.stqe_next = &_desc[(i + 1) % CHUNK_COUNT];
}
periph_module_enable(PERIPH_I2S0_MODULE);
resetI2S0();
// 8-bit parallel LCD mode (bitluni ESP32Lib setup; the i ^ 2 byte order in
// the lookup table 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);
// EOF interrupt refills the ring. IRAM so it keeps running during flash writes.
I2S0.int_ena.val = 0;
I2S0.int_clr.val = I2S0.int_raw.val;
if (esp_intr_alloc(ETS_I2S0_INTR_SOURCE, ESP_INTR_FLAG_IRAM, &EL512Panel::isr, this,
&_intr) != ESP_OK) {
_intr = nullptr;
return false;
}
I2S0.int_ena.out_eof = 1;
// Start the DMA; it loops over the chain until stopI2S().
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.int_ena.val = 0;
if (_intr) {
esp_intr_free(_intr);
_intr = nullptr;
}
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);
gpio_set_level((gpio_num_t)pin, 0);
}
heap_caps_free(_desc);
_desc = nullptr;
}