Files
mangavalkandClaude Opus 5.5 f3212ea969 el512: refresh health counters and quiet warnings
The panel driver counts ring refills, the largest gap between refills,
frame swaps, swap timeouts and I2S TX FIFO underruns (takeStats()). The
ESPHome component checks them every 10 s and logs a warning only when the
ESP32 side of the signal had a problem, plus a debug summary per minute.

Measured with Wi-Fi on: ~4600 refills/s, max gap < 0.4 ms (lead 1.7 ms),
no underruns or timeouts, so remaining blinks come from the signal being
disturbed on the wires (Wi-Fi transmit bursts), not from the refresh.

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

147 lines
5.5 KiB
C++

// 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;
// Refresh health counters since the last call (then reset): ring refills,
// the largest gap between two refills in CPU cycles, frame swaps, and
// show() calls that gave up waiting for a swap.
struct Stats {
uint32_t refills;
uint32_t maxGapCycles;
uint32_t swaps;
uint32_t showTimeouts;
uint32_t underruns; // refills that saw the TX FIFO empty or hung
};
Stats takeStats();
// 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;
volatile uint32_t _refills = 0;
volatile uint32_t _swaps = 0;
volatile uint32_t _maxGap = 0;
volatile uint32_t _lastIsrCycle = 0;
uint32_t _showTimeouts = 0;
volatile uint32_t _underruns = 0;
bool _running = false;
bool _enabled = false;
bool _lowPower = false;
};