#include "EL512Panel.h" #include #include #include #include #include #include #include #include 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; // One DMA descriptor per stream chunk (descriptor limit 4095 bytes). 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"); 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; pinMode(pin, OUTPUT); esp_rom_gpio_connect_out_signal(pin, signal, invert, false); } } // namespace EL512Panel::EL512Panel(const EL512Pins& pins) : _pins(pins) {} EL512Panel::~EL512Panel() { end(); } bool EL512Panel::begin() { if (_running) return true; pinMode(_pins.enable, OUTPUT); pinMode(_pins.lowpwr_n, OUTPUT); setEnabled(false); setLowPower(false); _fb = (uint8_t*)calloc(FRAMEBUFFER_BYTES, 1); // Full-frame stream: 261 * 544 = ~142 KB of DMA-capable internal RAM, in chunks. // (A line ring buffer refilled from the DMA EOF interrupt would cut this // to a few KB if RAM gets tight.) bool ok = _fb != nullptr; for (uint16_t i = 0; ok && i < CHUNK_COUNT; i++) { _chunks[i] = (uint8_t*)heap_caps_malloc((size_t)chunkLines(i) * ROW_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); ok = _chunks[i] != nullptr; } if (!ok) { end(); return false; } show(); if (!startI2S()) { end(); return false; } _running = true; setEnabled(true); return true; } void EL512Panel::end() { setEnabled(false); stopI2S(); _running = false; free(_fb); _fb = nullptr; for (uint8_t*& chunk : _chunks) { heap_caps_free(chunk); chunk = nullptr; } } uint16_t EL512Panel::chunkLines(uint16_t chunk) { const uint16_t first = chunk * LINES_PER_CHUNK; return min(LINES_PER_CHUNK, LINES_PER_FRAME - first); } uint8_t* EL512Panel::linePtr(uint16_t line) const { return _chunks[line / LINES_PER_CHUNK] + (size_t)(line % LINES_PER_CHUNK) * ROW_BYTES; } void EL512Panel::setEnabled(bool on) { digitalWrite(_pins.enable, on ? HIGH : LOW); } void EL512Panel::setLowPower(bool on) { digitalWrite(_pins.lowpwr_n, on ? LOW : HIGH); } 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::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). // VS rises 1104 clocks after the previous HS fall (T4 >= 1 VCLK) and 272 // clocks = 54.4 us before the next HS rise (T5 >= 3 us); check on a scope. const uint8_t vs = (line == LINES_PER_FRAME - 1) ? BIT_VS : 0; if (line >= HEIGHT) { memset(out, vs, ROW_BYTES); return; } const uint8_t* src = _fb + (size_t)line * (WIDTH / 8); for (uint16_t c = 0; c < DATA_CLOCKS_PER_ROW; 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)(src[c >> 2] << ((c & 3) * 2)); uint8_t v = BIT_HS | vs; if (bits & 0x80) v |= BIT_TVID; if (bits & 0x40) v |= BIT_VID; // Two bytes per VCLK; 8-bit I2S LCD mode sends each 32-bit word as bytes 2, 3, 0, 1. const uint16_t i = c * BYTES_PER_CLOCK; out[i ^ 2] = v; out[(i + 1) ^ 2] = v; } // Blanking is uniform and starts on a word boundary, so it needs no swap. memset(out + DATA_CLOCKS_PER_ROW * BYTES_PER_CLOCK, vs, BLANK_CLOCKS_PER_ROW * BYTES_PER_CLOCK); } void EL512Panel::show() { if (!_fb || !_chunks[CHUNK_COUNT - 1]) return; // TODO: double-buffer or sync to VS to avoid tearing. for (uint16_t line = 0; line < LINES_PER_FRAME; line++) { encodeRow(line, linePtr(line)); } } bool EL512Panel::startI2S() { // Circular descriptor chain over the stream, one descriptor per chunk. _desc = (lldesc_t*)heap_caps_calloc(CHUNK_COUNT, sizeof(lldesc_t), MALLOC_CAP_DMA); if (!_desc) return false; for (size_t i = 0; i < CHUNK_COUNT; i++) { lldesc_t& d = _desc[i]; d.size = chunkLines(i) * ROW_BYTES; d.length = d.size; d.owner = 1; d.eof = (i == CHUNK_COUNT - 1); // one EOF per frame d.buf = _chunks[i]; d.qe.stqe_next = &_desc[(i + 1) % CHUNK_COUNT]; } periph_module_enable(PERIPH_I2S0_MODULE); resetI2S0(); // 8-bit parallel LCD mode, one byte per BCK period (bitluni ESP32Lib setup; // the c ^ 2 byte order in encodeRow() 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); // Start the DMA; it loops over the chain until stopI2S(). I2S0.int_ena.val = 0; I2S0.int_clr.val = I2S0.int_raw.val; 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.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); digitalWrite(pin, LOW); } heap_caps_free(_desc); _desc = nullptr; }