Public Access
Fix VCLK at half rate; chunk the DMA stream; cycle test images
PCNT measurement showed I2S0 BCK (VCLK) rising once per two bytes in 8-bit LCD mode, so the panel saw every other byte. Each VCLK is now two identical bytes at a 10 MHz byte rate (VCLK 5 MHz). The doubled ~142 KB stream is allocated as 38 per-descriptor chunks. The test sketch now cycles through 12 labelled test images. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e5b8dd9c50
commit
df78b5e72c
6 files changed
+235
-91
No files matched your search
@@ -13,25 +13,23 @@ 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");
|
||||
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 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.
|
||||
// BCK runs at half the byte rate (measured: 2.5 MHz BCK at 5 MHz bytes), so each
|
||||
// VCLK period spans two identical bytes. Unverified which BCK edge lines up with
|
||||
// the byte pair boundary: if pixels look shifted or noisy, 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;
|
||||
// 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;
|
||||
@@ -64,11 +62,16 @@ bool EL512Panel::begin() {
|
||||
setLowPower(false);
|
||||
|
||||
_fb = (uint8_t*)calloc(FRAMEBUFFER_BYTES, 1);
|
||||
// Full-frame stream: 261 * 272 = ~71 KB of DMA-capable internal RAM.
|
||||
// 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.)
|
||||
_stream = (uint8_t*)heap_caps_malloc(STREAM_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
||||
if (!_fb || !_stream) {
|
||||
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;
|
||||
}
|
||||
@@ -89,8 +92,19 @@ void EL512Panel::end() {
|
||||
_running = false;
|
||||
free(_fb);
|
||||
_fb = nullptr;
|
||||
heap_caps_free(_stream);
|
||||
_stream = 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<uint16_t>(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); }
|
||||
@@ -124,7 +138,7 @@ void EL512Panel::encodeRow(uint16_t line, uint8_t* out) const {
|
||||
const uint8_t vs = (line == LINES_PER_FRAME - 1) ? BIT_VS : 0;
|
||||
|
||||
if (line >= HEIGHT) {
|
||||
memset(out, vs, CLOCKS_PER_ROW);
|
||||
memset(out, vs, ROW_BYTES);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,35 +150,35 @@ void EL512Panel::encodeRow(uint16_t line, uint8_t* out) const {
|
||||
uint8_t v = BIT_HS | vs;
|
||||
if (bits & 0x80) v |= BIT_TVID;
|
||||
if (bits & 0x40) v |= BIT_VID;
|
||||
// 8-bit I2S LCD mode sends each 32-bit word as bytes 2, 3, 0, 1.
|
||||
out[c ^ 2] = v;
|
||||
// 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, vs, BLANK_CLOCKS_PER_ROW);
|
||||
memset(out + DATA_CLOCKS_PER_ROW * BYTES_PER_CLOCK, vs, BLANK_CLOCKS_PER_ROW * BYTES_PER_CLOCK);
|
||||
}
|
||||
|
||||
void EL512Panel::show() {
|
||||
if (!_fb || !_stream) return;
|
||||
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, _stream + (size_t)line * CLOCKS_PER_ROW);
|
||||
encodeRow(line, linePtr(line));
|
||||
}
|
||||
}
|
||||
|
||||
bool EL512Panel::startI2S() {
|
||||
// Circular descriptor chain over the stream, whole lines per descriptor.
|
||||
_desc = (lldesc_t*)heap_caps_calloc(DESC_COUNT, sizeof(lldesc_t), MALLOC_CAP_DMA);
|
||||
// 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 < 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);
|
||||
for (size_t i = 0; i < CHUNK_COUNT; i++) {
|
||||
lldesc_t& d = _desc[i];
|
||||
d.size = lines * CLOCKS_PER_ROW;
|
||||
d.size = chunkLines(i) * ROW_BYTES;
|
||||
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];
|
||||
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);
|
||||
@@ -181,7 +195,7 @@ bool EL512Panel::startI2S() {
|
||||
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.
|
||||
// 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;
|
||||
|
||||
Reference in new issue
Block a user