Files
planar-EL512.256-H/components/el512/el512_display.cpp
T
mangavalkandClaude Opus 5.5 f3212ea969 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>
2026-10-18 20:52:10 +02:00

146 lines
5.1 KiB
C++

#include "el512_display.h"
#include <esp_heap_caps.h>
#include <esp_private/esp_clk.h>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::el512 {
static const char *const TAG = "el512";
void EL512Display::setup() {
this->panel_ = new EL512Panel(this->pins_); // NOLINT(cppcoreguidelines-owning-memory)
if (!this->panel_->begin()) {
ESP_LOGE(TAG, "Panel init failed (out of DMA memory or no free interrupt)");
this->mark_failed();
return;
}
// Apply states that switches may have restored before setup ran.
this->panel_->setLowPower(this->low_power_);
this->panel_->setEnabled(this->enabled_);
}
void EL512Display::loop() {
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();
if (active && !this->still_alive_was_active_) {
// Remember where we came from, then start the show from the top.
this->still_alive_return_ = this->previous_page_ != this->still_alive_page_ ? this->previous_page_ : nullptr;
this->still_alive_.start(now);
ESP_LOGI(TAG, "Still Alive started; start the song in %u ms", (unsigned) this->still_alive_song_start_ms());
}
this->still_alive_was_active_ = active;
if (!active)
return;
if (this->still_alive_.finished(now) && this->still_alive_return_ != nullptr) {
this->show_page(this->still_alive_return_);
this->update();
return;
}
if (this->still_alive_.update(now))
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;
if (this->is_still_alive_playing()) {
this->still_alive_.start(millis()); // already showing: just restart
} else {
this->show_page(this->still_alive_page_); // loop() starts it
}
}
void EL512Display::draw_still_alive() {
if (this->panel_ != nullptr)
this->still_alive_.draw(*this->panel_);
}
void EL512Display::update() {
if (this->panel_ == nullptr || !this->panel_->running())
return;
this->do_update_();
this->panel_->show();
}
void EL512Display::fill(Color color) {
// The panel clear is a memset; with clipping active, fall back to per-pixel.
if (this->panel_ == nullptr || this->get_clipping().is_set()) {
Display::fill(color);
return;
}
this->panel_->clear(color.is_on());
}
void EL512Display::draw_absolute_pixel_internal(int x, int y, Color color) {
if (this->panel_ != nullptr)
this->panel_->setPixel(x, y, color.is_on());
}
void EL512Display::set_enabled(bool on) {
this->enabled_ = on;
if (this->panel_ != nullptr)
this->panel_->setEnabled(on);
}
void EL512Display::set_low_power(bool on) {
this->low_power_ = on;
if (this->panel_ != nullptr)
this->panel_->setLowPower(on);
}
void EL512Display::dump_config() {
LOG_DISPLAY("", "EL512.256-H", this);
ESP_LOGCONFIG(TAG,
" Pins: VCLK %d, VID %d, TVID %d, HS %d, VS %d, ENABLE %d, _LOWPWR %d\n"
" Refresh: %.1f Hz, DMA ring %u bytes\n"
" Free internal heap: %u bytes",
this->pins_.vclk, this->pins_.vid, this->pins_.tvid, this->pins_.hs, this->pins_.vs,
this->pins_.enable, this->pins_.lowpwr_n, EL512Panel::refreshHz(),
(unsigned) EL512Panel::RING_BYTES,
(unsigned) heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
LOG_UPDATE_INTERVAL(this);
if (this->is_failed())
ESP_LOGE(TAG, " Init failed");
}
} // namespace esphome::el512