Files

146 lines
5.5 KiB
C++
Raw Permalink Normal View History

// 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;
};