// 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 #include 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; // 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; bool _running = false; bool _enabled = false; bool _lowPower = false; };