Public Access
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
963590cdfe
commit
f3212ea969
4 files changed
+80
-1
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
#include "el512_display.h"
|
#include "el512_display.h"
|
||||||
|
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
|
#include <esp_private/esp_clk.h>
|
||||||
|
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -22,7 +23,10 @@ void EL512Display::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EL512Display::loop() {
|
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;
|
return;
|
||||||
const bool active = this->is_still_alive_playing();
|
const bool active = this->is_still_alive_playing();
|
||||||
const uint32_t now = millis();
|
const uint32_t now = millis();
|
||||||
@@ -44,6 +48,37 @@ void EL512Display::loop() {
|
|||||||
this->update();
|
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() {
|
void EL512Display::play_still_alive() {
|
||||||
if (this->still_alive_page_ == nullptr)
|
if (this->still_alive_page_ == nullptr)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class EL512Display : public display::DisplayBuffer {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
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_width_internal() override { return EL512Panel::WIDTH; }
|
||||||
int get_height_internal() override { return EL512Panel::HEIGHT; }
|
int get_height_internal() override { return EL512Panel::HEIGHT; }
|
||||||
|
|
||||||
@@ -58,6 +59,8 @@ class EL512Display : public display::DisplayBuffer {
|
|||||||
EL512Panel *panel_{nullptr};
|
EL512Panel *panel_{nullptr};
|
||||||
bool enabled_{true};
|
bool enabled_{true};
|
||||||
bool low_power_{false};
|
bool low_power_{false};
|
||||||
|
uint32_t health_checked_{0};
|
||||||
|
uint32_t health_logged_{0};
|
||||||
|
|
||||||
display::DisplayPage *still_alive_page_{nullptr};
|
display::DisplayPage *still_alive_page_{nullptr};
|
||||||
display::DisplayPage *still_alive_return_{nullptr};
|
display::DisplayPage *still_alive_return_{nullptr};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
#include <esp_attr.h>
|
#include <esp_attr.h>
|
||||||
|
#include <esp_cpu.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <esp_intr_alloc.h>
|
#include <esp_intr_alloc.h>
|
||||||
#include <esp_rom_gpio.h>
|
#include <esp_rom_gpio.h>
|
||||||
@@ -176,6 +177,17 @@ void EL512Panel::show() {
|
|||||||
// The ISR copies _fb to _front just before it encodes line 0 of a frame.
|
// The ISR copies _fb to _front just before it encodes line 0 of a frame.
|
||||||
_swapPending = true;
|
_swapPending = true;
|
||||||
for (int i = 0; i < 40 && _swapPending; i++) vTaskDelay(1);
|
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 {
|
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;
|
uint32_t* d = (uint32_t*)_front;
|
||||||
for (size_t w = 0; w < FRAMEBUFFER_BYTES / 4; w++) d[w] = s[w];
|
for (size_t w = 0; w < FRAMEBUFFER_BYTES / 4; w++) d[w] = s[w];
|
||||||
_swapPending = false;
|
_swapPending = false;
|
||||||
|
_swaps = _swaps + 1;
|
||||||
}
|
}
|
||||||
encodeRow(line, out + (size_t)i * ROW_BYTES);
|
encodeRow(line, out + (size_t)i * ROW_BYTES);
|
||||||
line = (line + 1 == LINES_PER_FRAME) ? 0 : line + 1;
|
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) {
|
void IRAM_ATTR EL512Panel::isr(void* arg) {
|
||||||
auto* self = static_cast<EL512Panel*>(arg);
|
auto* self = static_cast<EL512Panel*>(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;
|
const uint32_t status = I2S0.int_st.val;
|
||||||
I2S0.int_clr.val = status;
|
I2S0.int_clr.val = status;
|
||||||
if (!(status & I2S_OUT_EOF_INT_ST_M)) return;
|
if (!(status & I2S_OUT_EOF_INT_ST_M)) return;
|
||||||
|
|||||||
@@ -103,6 +103,18 @@ class EL512Panel {
|
|||||||
// ROW_BYTES bytes in DMA order. Lines >= HEIGHT produce blanking.
|
// ROW_BYTES bytes in DMA order. Lines >= HEIGHT produce blanking.
|
||||||
void encodeRow(uint16_t line, uint8_t* out) const;
|
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.
|
// Frame refresh rate implied by the timing constants.
|
||||||
static constexpr float refreshHz() {
|
static constexpr float refreshHz() {
|
||||||
return (float)PIXEL_CLOCK_HZ / ((float)CLOCKS_PER_ROW * LINES_PER_FRAME);
|
return (float)PIXEL_CLOCK_HZ / ((float)CLOCKS_PER_ROW * LINES_PER_FRAME);
|
||||||
@@ -122,6 +134,12 @@ class EL512Panel {
|
|||||||
intr_handle_data_t* _intr = nullptr;
|
intr_handle_data_t* _intr = nullptr;
|
||||||
uint16_t _chunkLine[CHUNK_COUNT] = {}; // first line currently in each chunk
|
uint16_t _chunkLine[CHUNK_COUNT] = {}; // first line currently in each chunk
|
||||||
volatile bool _swapPending = false;
|
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 _running = false;
|
||||||
bool _enabled = false;
|
bool _enabled = false;
|
||||||
bool _lowPower = false;
|
bool _lowPower = false;
|
||||||
|
|||||||
Reference in new issue
Block a user