2026-10-18 20:20:13 +02:00
|
|
|
// 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;
|
|
|
|
|
|
2026-10-18 20:52:10 +02:00
|
|
|
// 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();
|
|
|
|
|
|
2026-10-18 20:20:13 +02:00
|
|
|
// 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;
|
2026-10-18 20:52:10 +02:00
|
|
|
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;
|
2026-10-18 20:20:13 +02:00
|
|
|
bool _running = false;
|
|
|
|
|
bool _enabled = false;
|
|
|
|
|
bool _lowPower = false;
|
|
|
|
|
};
|