From f3212ea9695eb686e4458685a95bc6e6079ba819 Mon Sep 17 00:00:00 2001 From: MangaValk Date: Sun, 18 Oct 2026 20:52:10 +0200 Subject: [PATCH] el512: refresh health counters and quiet warnings The panel driver counts ring refills, the largest gap between refills, frame swaps, swap timeouts and I2S TX FIFO underruns (takeStats()). The ESPHome component checks them every 10 s and logs a warning only when the ESP32 side of the signal had a problem, plus a debug summary per minute. Measured with Wi-Fi on: ~4600 refills/s, max gap < 0.4 ms (lead 1.7 ms), no underruns or timeouts, so remaining blinks come from the signal being disturbed on the wires (Wi-Fi transmit bursts), not from the refresh. Co-Authored-By: Claude Opus 5.5 --- components/el512/el512_display.cpp | 37 +++++++++++++++++++++++++++++- components/el512/el512_display.h | 3 +++ components/el512/el512_panel.cpp | 23 +++++++++++++++++++ components/el512/el512_panel.h | 18 +++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/components/el512/el512_display.cpp b/components/el512/el512_display.cpp index 3b30aa5..73a58a6 100644 --- a/components/el512/el512_display.cpp +++ b/components/el512/el512_display.cpp @@ -1,6 +1,7 @@ #include "el512_display.h" #include +#include #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -22,7 +23,10 @@ void EL512Display::setup() { } void EL512Display::loop() { - if (this->still_alive_page_ == nullptr || this->panel_ == nullptr || !this->panel_->running()) + if (this->panel_ == nullptr || !this->panel_->running()) + return; + this->check_refresh_health_(); + if (this->still_alive_page_ == nullptr) return; const bool active = this->is_still_alive_playing(); const uint32_t now = millis(); @@ -44,6 +48,37 @@ void EL512Display::loop() { this->update(); } +// Refresh health, checked every 10 s: warn when the ESP32 side of the video +// signal had a problem (DMA FIFO underrun, a refill later than the ring's lead, +// or a frame swap that timed out); otherwise a debug summary once a minute. +// No warnings while the panel misbehaves means the signal left the ESP32 +// intact and was disturbed on the wires (e.g. by Wi-Fi transmit bursts). +void EL512Display::check_refresh_health_() { + const uint32_t now = millis(); + if (this->health_checked_ == 0) { + // Skip the start-up period (the FIFO is empty once while the DMA starts). + this->panel_->takeStats(); + this->health_checked_ = now; + return; + } + if (now - this->health_checked_ < 10000) + return; + const uint32_t period_ms = now - this->health_checked_; + this->health_checked_ = now; + const auto s = this->panel_->takeStats(); + const float max_gap_us = s.maxGapCycles / (float) (esp_clk_cpu_freq() / 1000000); + const float lead_us = 1e6f * EL512Panel::RING_LINES * EL512Panel::CLOCKS_PER_ROW / EL512Panel::PIXEL_CLOCK_HZ; + if (s.underruns > 0 || s.showTimeouts > 0 || max_gap_us > lead_us * 0.75f) { + ESP_LOGW(TAG, "Refresh problem in the last %us: %u FIFO underruns, %u swap timeouts, max refill gap %.0f us", + (unsigned) (period_ms / 1000), (unsigned) s.underruns, (unsigned) s.showTimeouts, max_gap_us); + } + if (now - this->health_logged_ >= 60000) { + this->health_logged_ = now; + ESP_LOGD(TAG, "Refresh OK: %.0f refills/s, max gap %.0f us, %u swaps", s.refills * 1000.0f / period_ms, + max_gap_us, (unsigned) s.swaps); + } +} + void EL512Display::play_still_alive() { if (this->still_alive_page_ == nullptr) return; diff --git a/components/el512/el512_display.h b/components/el512/el512_display.h index 132c64e..1369b3f 100644 --- a/components/el512/el512_display.h +++ b/components/el512/el512_display.h @@ -51,6 +51,7 @@ class EL512Display : public display::DisplayBuffer { protected: void draw_absolute_pixel_internal(int x, int y, Color color) override; + void check_refresh_health_(); int get_width_internal() override { return EL512Panel::WIDTH; } int get_height_internal() override { return EL512Panel::HEIGHT; } @@ -58,6 +59,8 @@ class EL512Display : public display::DisplayBuffer { EL512Panel *panel_{nullptr}; bool enabled_{true}; bool low_power_{false}; + uint32_t health_checked_{0}; + uint32_t health_logged_{0}; display::DisplayPage *still_alive_page_{nullptr}; display::DisplayPage *still_alive_return_{nullptr}; diff --git a/components/el512/el512_panel.cpp b/components/el512/el512_panel.cpp index 848a696..b162cf4 100644 --- a/components/el512/el512_panel.cpp +++ b/components/el512/el512_panel.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -176,6 +177,17 @@ void EL512Panel::show() { // The ISR copies _fb to _front just before it encodes line 0 of a frame. _swapPending = true; for (int i = 0; i < 40 && _swapPending; i++) vTaskDelay(1); + if (_swapPending) _showTimeouts++; +} + +EL512Panel::Stats EL512Panel::takeStats() { + Stats s{_refills, _maxGap, _swaps, _showTimeouts, _underruns}; + _underruns = 0; + _refills = 0; + _maxGap = 0; + _swaps = 0; + _showTimeouts = 0; + return s; } void IRAM_ATTR EL512Panel::encodeRow(uint16_t line, uint8_t* out) const { @@ -206,6 +218,7 @@ void IRAM_ATTR EL512Panel::fillChunk(uint16_t chunk) { uint32_t* d = (uint32_t*)_front; for (size_t w = 0; w < FRAMEBUFFER_BYTES / 4; w++) d[w] = s[w]; _swapPending = false; + _swaps = _swaps + 1; } encodeRow(line, out + (size_t)i * ROW_BYTES); line = (line + 1 == LINES_PER_FRAME) ? 0 : line + 1; @@ -214,6 +227,16 @@ void IRAM_ATTR EL512Panel::fillChunk(uint16_t chunk) { void IRAM_ATTR EL512Panel::isr(void* arg) { auto* self = static_cast(arg); + const uint32_t now = esp_cpu_get_cycle_count(); + if (self->_lastIsrCycle != 0 && now - self->_lastIsrCycle > self->_maxGap) self->_maxGap = now - self->_lastIsrCycle; + self->_lastIsrCycle = now; + self->_refills = self->_refills + 1; + // Health counters (see takeStats): TX FIFO ran empty / transmitter hung. + const uint32_t raw = I2S0.int_raw.val; + if (raw & (I2S_TX_REMPTY_INT_RAW_M | I2S_TX_HUNG_INT_RAW_M)) { + self->_underruns = self->_underruns + 1; + I2S0.int_clr.val = I2S_TX_REMPTY_INT_CLR_M | I2S_TX_HUNG_INT_CLR_M; + } const uint32_t status = I2S0.int_st.val; I2S0.int_clr.val = status; if (!(status & I2S_OUT_EOF_INT_ST_M)) return; diff --git a/components/el512/el512_panel.h b/components/el512/el512_panel.h index 2cc329e..69972d3 100644 --- a/components/el512/el512_panel.h +++ b/components/el512/el512_panel.h @@ -103,6 +103,18 @@ class EL512Panel { // ROW_BYTES bytes in DMA order. Lines >= HEIGHT produce blanking. void encodeRow(uint16_t line, uint8_t* out) const; + // Refresh health counters since the last call (then reset): ring refills, + // the largest gap between two refills in CPU cycles, frame swaps, and + // show() calls that gave up waiting for a swap. + struct Stats { + uint32_t refills; + uint32_t maxGapCycles; + uint32_t swaps; + uint32_t showTimeouts; + uint32_t underruns; // refills that saw the TX FIFO empty or hung + }; + Stats takeStats(); + // Frame refresh rate implied by the timing constants. static constexpr float refreshHz() { return (float)PIXEL_CLOCK_HZ / ((float)CLOCKS_PER_ROW * LINES_PER_FRAME); @@ -122,6 +134,12 @@ class EL512Panel { intr_handle_data_t* _intr = nullptr; uint16_t _chunkLine[CHUNK_COUNT] = {}; // first line currently in each chunk volatile bool _swapPending = false; + volatile uint32_t _refills = 0; + volatile uint32_t _swaps = 0; + volatile uint32_t _maxGap = 0; + volatile uint32_t _lastIsrCycle = 0; + uint32_t _showTimeouts = 0; + volatile uint32_t _underruns = 0; bool _running = false; bool _enabled = false; bool _lowPower = false;