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;