Files
planar-EL512.256-H/lib/EL512/src/EL512Panel.cpp
T

244 lines
7.7 KiB
C++
Raw Normal View History

#include "EL512Panel.h"
2026-10-05 21:08:36 +02:00
#include <driver/periph_ctrl.h>
#include <esp32/rom/lldesc.h>
#include <esp_heap_caps.h>
2026-10-05 21:08:36 +02:00
#include <esp_rom_gpio.h>
#include <soc/gpio_sig_map.h>
#include <soc/i2s_reg.h>
#include <soc/i2s_struct.h>
#include <string.h>
2026-10-05 21:08:36 +02:00
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_CLKM_DIV = I2S_SOURCE_CLOCK_HZ / EL512Panel::PIXEL_CLOCK_HZ;
static_assert(I2S_SOURCE_CLOCK_HZ % EL512Panel::PIXEL_CLOCK_HZ == 0,
"pixel clock 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 the byte rate. As in standard I2S, data should change on its falling
// edge, so the panel's rising-edge latch sits mid-bit. Unverified: if the scope
// shows data changing on the rising edge, set this to true.
constexpr bool INVERT_VCLK = false;
// Whole lines per DMA descriptor: 15 * 272 = 4080 bytes (limit 4095).
constexpr uint16_t LINES_PER_DESC = 15;
static_assert(LINES_PER_DESC * EL512Panel::CLOCKS_PER_ROW <= 4092, "descriptor too long");
static_assert(EL512Panel::CLOCKS_PER_ROW % 4 == 0, "rows must be whole DMA words");
constexpr size_t DESC_COUNT =
(EL512Panel::LINES_PER_FRAME + LINES_PER_DESC - 1) / LINES_PER_DESC;
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 * 272 = ~71 KB of DMA-capable internal RAM.
// (A line ring buffer refilled from the DMA EOF interrupt would cut this
// to a few KB if RAM gets tight.)
_stream = (uint8_t*)heap_caps_malloc(STREAM_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (!_fb || !_stream) {
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;
heap_caps_free(_stream);
_stream = nullptr;
}
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 input timing).
// TODO: verify VS/HS setup and hold (T4/T5) on hardware.
const uint8_t vs = (line == LINES_PER_FRAME - 1) ? BIT_VS : 0;
if (line >= HEIGHT) {
memset(out, vs, CLOCKS_PER_ROW);
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 columns 2c (VID, even) and 2c+1 (TVID, odd).
const uint8_t bits = (uint8_t)(src[c >> 2] << ((c & 3) * 2));
uint8_t v = BIT_HS | vs;
if (bits & 0x80) v |= BIT_VID;
if (bits & 0x40) v |= BIT_TVID;
2026-10-05 21:08:36 +02:00
// 8-bit I2S LCD mode sends each 32-bit word as bytes 2, 3, 0, 1.
out[c ^ 2] = v;
}
2026-10-05 21:08:36 +02:00
// Blanking is uniform and starts on a word boundary, so it needs no swap.
memset(out + DATA_CLOCKS_PER_ROW, vs, BLANK_CLOCKS_PER_ROW);
}
void EL512Panel::show() {
if (!_fb || !_stream) return;
// TODO: double-buffer or sync to VS to avoid tearing.
for (uint16_t line = 0; line < LINES_PER_FRAME; line++) {
encodeRow(line, _stream + (size_t)line * CLOCKS_PER_ROW);
}
}
bool EL512Panel::startI2S() {
2026-10-05 21:08:36 +02:00
// Circular descriptor chain over the stream, whole lines per descriptor.
_desc = (lldesc_t*)heap_caps_calloc(DESC_COUNT, sizeof(lldesc_t), MALLOC_CAP_DMA);
if (!_desc) return false;
for (size_t i = 0; i < DESC_COUNT; i++) {
const uint16_t first = i * LINES_PER_DESC;
const uint16_t lines = min<uint16_t>(LINES_PER_DESC, LINES_PER_FRAME - first);
lldesc_t& d = _desc[i];
d.size = lines * CLOCKS_PER_ROW;
d.length = d.size;
d.owner = 1;
d.eof = (i == DESC_COUNT - 1); // one EOF per frame
d.buf = _stream + (size_t)first * CLOCKS_PER_ROW;
d.qe.stqe_next = &_desc[(i + 1) % DESC_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 / 32 = 5 MHz.
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() {
2026-10-05 21:08:36 +02:00
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;
}