Public Access
Implement I2S0 8-bit LCD-mode DMA refresh
startI2S() sets up I2S0 in 8-bit parallel (LCD) mode at 5 MHz (PLL_D2 160 MHz / 32, BCK div 1), routes stream bits 0..3 to VID/TVID/HS/VS via I2S0O_DATA_OUT16+n and BCK to VCLK, and loops DMA over an 18-descriptor chain of whole lines. stopI2S() stops TX, returns the pins to GPIO low and frees the chain. encodeRow() now stores clock c at c ^ 2 to match the 8-bit mode byte order (bytes 2, 3, 0, 1 per word, as in bitluni's ESP32Lib). Builds with no warnings; not yet checked on hardware. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
873bdcd23e
commit
3640c7fb3f
5 files changed
+173
-29
No files matched your search
+138
-14
@@ -1,8 +1,56 @@
|
||||
#include "EL512Panel.h"
|
||||
|
||||
#include <driver/periph_ctrl.h>
|
||||
#include <esp32/rom/lldesc.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_rom_gpio.h>
|
||||
#include <soc/gpio_sig_map.h>
|
||||
#include <soc/i2s_reg.h>
|
||||
#include <soc/i2s_struct.h>
|
||||
#include <string.h>
|
||||
|
||||
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<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() {
|
||||
// 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;
|
||||
}
|
||||
@@ -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 <Arduino.h>
|
||||
|
||||
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;
|
||||
};
|
||||
Reference in new issue
Block a user