diff --git a/README.md b/README.md index 4c35b1e..1815a48 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ The panel has no frame memory and needs a continuous video stream. The ESP32's I peripheral in 8-bit parallel (LCD) mode streams a precomputed frame from DMA, so the CPU is free while the panel refreshes. -**Status:** project skeleton. Framebuffer, GFX layer and row encoder are in place; the -I2S/DMA start-up (`EL512Panel::startI2S()`) is still a stub. +**Status:** framebuffer, GFX layer, row encoder and I2S/DMA refresh are implemented and +build cleanly; the signal timing has not yet been checked on hardware. Updates are not +yet tear-free. ## Hardware @@ -22,7 +23,7 @@ I2S/DMA start-up (`EL512Panel::startI2S()`) is still a stub. | Panel signal | Function | ESP32 GPIO | DMA byte bit | |--------------|-----------------------------------------|-----------:|-------------:| -| VCLK | Video clock (rising edge) | 18 | I2S clock | +| VCLK | Video clock (rising edge) | 18 | I2S0 BCK | | VID | Pixel data, even columns | 19 | 0 | | TVID | Pixel data, odd columns (two-bit mode) | 21 | 1 | | HS | Horizontal sync | 22 | 2 | @@ -49,7 +50,7 @@ platformio.ini build config (esp32dev, Arduino, Adafruit GFX) include/pins.h pin map src/main.cpp test sketch: test pattern + text lib/EL512/ local library - src/EL512Panel.* framebuffer, row encoder, I2S DMA refresh (stub) + src/EL512Panel.* framebuffer, row encoder, I2S DMA refresh src/EL512_GFX.* Adafruit_GFX subclass test/ unit tests (placeholder) docs/ datasheet and notes diff --git a/docs/README.md b/docs/README.md index 81bc4e5..4f891b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,17 @@ is one VCLK period: | 2 | HS | | 3 | VS | -The I2S clock output drives VCLK. ENABLE (GPIO 25) and _LOWPWR (GPIO 26) are plain GPIO. +The I2S0 BCK output drives VCLK. ENABLE (GPIO 25) and _LOWPWR (GPIO 26) are plain GPIO. + +I2S0 setup (`EL512Panel::startI2S()`), following bitluni's ESP32Lib 8-bit mode: + +- Clock: PLL_D2_CLK 160 MHz / `clkm_div_num` 32, `tx_bck_div_num` 1 -> 5 MHz, one byte + per BCK period. +- `lcd_en`, `lcd_tx_wrx2_en`, `tx_bits_mod` 8, `tx_fifo_mod` 1, `tx_chan_mod` 1, + `tx_right_first`. With this setup each 32-bit DMA word goes out as bytes 2, 3, 0, 1, + so `encodeRow()` stores clock c at `c ^ 2`. +- Stream bits 0..3 -> `I2S0O_DATA_OUT16_IDX` + n; BCK -> VCLK (`INVERT_VCLK` flips it). +- 18 descriptors of up to 15 lines (4080 bytes) each, looped; `eof` set once per frame. ## From the datasheet @@ -28,9 +38,14 @@ The I2S clock output drives VCLK. ENABLE (GPIO 25) and _LOWPWR (GPIO 26) are pla ## Open items -- [ ] Implement `EL512Panel::startI2S()` (I2S0 LCD mode, clock dividers for 5 MHz, +- [x] Implement `EL512Panel::startI2S()` (I2S0 LCD mode, clock dividers for 5 MHz, GPIO matrix routing, circular `lldesc_t` chain). -- [ ] Handle the 8-bit LCD-mode byte-pair swap in `encodeRow()`. -- [ ] Check VS/HS setup and hold (T4/T5) and VCLK polarity on a scope. +- [x] Handle the 8-bit LCD-mode byte-pair swap in `encodeRow()`. +- [ ] Check on a scope: VCLK is 5 MHz on GPIO 18, HS is 256 clocks high / 16 low + (54.4 us period), VS is one line high per 14.2 ms frame, data changes on the + falling VCLK edge, VS/HS setup and hold (T4/T5). Unconfirmed until then: the + DATA_OUT16 base and the `^ 2` byte order. HS edges sit on word boundaries, so a + wrong order does not show on HS; on the panel it shuffles pixels in groups of 8 + (a 1-pixel line at x = 0 lands at x = 4). - [ ] Tear-free updates (double buffer or swap at VS). - [ ] Unit test for `encodeRow()`. diff --git a/include/pins.h b/include/pins.h index befe81f..1c82009 100644 --- a/include/pins.h +++ b/include/pins.h @@ -9,7 +9,7 @@ namespace el512pins { // I2S-driven signals -constexpr int8_t VCLK = 18; // I2S0 WS/clock out -> panel VCLK +constexpr int8_t VCLK = 18; // I2S0 BCK out -> panel VCLK constexpr int8_t VID = 19; // byte bit 0 constexpr int8_t TVID = 21; // byte bit 1 constexpr int8_t HS = 22; // byte bit 2 diff --git a/lib/EL512/src/EL512Panel.cpp b/lib/EL512/src/EL512Panel.cpp index 169ef5a..85b7df6 100644 --- a/lib/EL512/src/EL512Panel.cpp +++ b/lib/EL512/src/EL512Panel.cpp @@ -1,8 +1,56 @@ #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_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(); } @@ -86,12 +134,11 @@ void EL512Panel::encodeRow(uint16_t line, uint8_t* out) const { uint8_t v = BIT_HS | vs; if (bits & 0x80) v |= BIT_VID; if (bits & 0x40) v |= BIT_TVID; - out[c] = v; + // 8-bit I2S LCD mode sends each 32-bit word as bytes 2, 3, 0, 1. + out[c ^ 2] = v; } + // 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); - - // NOTE: in 8-bit I2S LCD mode the ESP32 emits bytes in swapped pairs - // (byte 1, byte 0, byte 3, byte 2, ...). Swap here once the DMA path is live. } void EL512Panel::show() { @@ -103,18 +150,95 @@ void EL512Panel::show() { } bool EL512Panel::startI2S() { - // TODO: configure I2S0 for 8-bit parallel LCD mode: - // - periph_module_enable(PERIPH_I2S0_MODULE), reset TX / FIFO / DMA - // - I2S0.conf2.lcd_en = 1, 8-bit TX, clock dividers -> PIXEL_CLOCK_HZ - // - route stream bits 0..3 to vid/tvid/hs/vs with gpio_matrix_out() - // (I2S0O_DATA_OUT8_IDX.. for 8-bit mode - verify on the core version) - // - route the I2S clock (I2S0O_WS_OUT_IDX) to vclk - // - build a circular lldesc_t chain over _stream (<= 4092 bytes each) - // - start TX via out_link.start - log_w("EL512Panel: I2S DMA refresh not implemented yet (stub)"); + // 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(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() { - // TODO: stop I2S0 TX and release the DMA descriptors. + 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; } diff --git a/lib/EL512/src/EL512Panel.h b/lib/EL512/src/EL512Panel.h index 8eb4271..cceab2e 100644 --- a/lib/EL512/src/EL512Panel.h +++ b/lib/EL512/src/EL512Panel.h @@ -8,18 +8,20 @@ // byte bit 1 = TVID (odd columns) // byte bit 2 = HS (high while the 256 data clocks of a row are sent) // byte bit 3 = VS (marks the top row) -// I2S clock -> VCLK (panel samples data on the rising edge) +// I2S BCK -> VCLK (panel samples data on the rising edge) // // Panel in two-bit parallel mode (jumper OFF, see datasheet), 5 MHz VCLK: // 272 clocks per row (256 data + 16 blanking), 261 rows per frame // (256 visible + 5 blanking) -> 5e6 / (272 * 261) ~= 70.4 Hz. // -// STATUS: the framebuffer and the row encoder are implemented; the I2S/DMA -// start-up in startI2S() is still a TODO (see EL512Panel.cpp). +// In 8-bit LCD mode the ESP32 I2S emits each 32-bit DMA word as bytes 2, 3, 0, 1, +// so encodeRow() stores clock c at index c ^ 2 (as bitluni's ESP32Lib does). #pragma once #include +struct lldesc_s; // ESP32 DMA descriptor (rom/lldesc.h) + struct EL512Pins { int8_t vclk; int8_t vid; @@ -55,7 +57,7 @@ class EL512Panel { explicit EL512Panel(const EL512Pins& pins); ~EL512Panel(); - // Allocates buffers, sets up control GPIOs and (TODO) starts I2S DMA refresh. + // Allocates buffers, sets up control GPIOs and starts the I2S DMA refresh. bool begin(); void end(); @@ -72,8 +74,9 @@ class EL512Panel { // Encode the framebuffer into the DMA stream (call after drawing). void show(); - // Encode one scan line (0..LINES_PER_FRAME-1) into CLOCKS_PER_ROW bytes. - // Lines >= HEIGHT produce blanking. Public so it can be unit-tested. + // Encode one scan line (0..LINES_PER_FRAME-1) into CLOCKS_PER_ROW bytes, in + // DMA order (clock c at index c ^ 2). Lines >= HEIGHT produce blanking. + // Public so it can be unit-tested. void encodeRow(uint16_t line, uint8_t* out) const; // Frame refresh rate implied by the timing constants. @@ -88,5 +91,6 @@ class EL512Panel { EL512Pins _pins; uint8_t* _fb = nullptr; // 1 bpp drawing buffer uint8_t* _stream = nullptr; // STREAM_BYTES, DMA-capable + lldesc_s* _desc = nullptr; // circular DMA descriptor chain over _stream bool _running = false; };