|
|
@@ -1,5 +1,10 @@
|
|
|
#pragma once
|
|
|
|
|
|
+#include <atomic>
|
|
|
+
|
|
|
+#include <freertos/FreeRTOS.h>
|
|
|
+#include <freertos/task.h>
|
|
|
+
|
|
|
#include "esphome/core/component.h"
|
|
|
#include "esphome/core/helpers.h"
|
|
|
#include "esphome/components/display/display_buffer.h"
|
|
|
@@ -62,6 +67,9 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
}
|
|
|
|
|
|
~GxEPD2Display() {
|
|
|
+ if (this->update_task_ != nullptr) {
|
|
|
+ vTaskDelete(this->update_task_);
|
|
|
+ }
|
|
|
if (this->driver_ != nullptr) {
|
|
|
this->driver_->~DisplayT();
|
|
|
RAMAllocator<uint8_t>().deallocate(reinterpret_cast<uint8_t *>(this->driver_), 0);
|
|
|
@@ -93,26 +101,51 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
this->spi_.spi_setup();
|
|
|
this->driver_->epd2.selectSPI(this->spi_, SPISettings());
|
|
|
// GxEPD2's init() does a real panel clear/refresh, blocking on busy just
|
|
|
- // like update() does below - it runs during ESPHome's synchronous
|
|
|
- // setup() phase, before any component's dump_config() is called, which
|
|
|
- // is why the whole boot log can appear to stall here.
|
|
|
+ // like a full update does - it runs during ESPHome's synchronous setup()
|
|
|
+ // phase, before any component's dump_config() is called, which is why
|
|
|
+ // the whole boot log can appear to stall here.
|
|
|
uint32_t init_start = millis();
|
|
|
this->driver_->init(0);
|
|
|
ESP_LOGD(TAG, "GxEPD2 init took %u ms", (unsigned) (millis() - init_start));
|
|
|
+
|
|
|
+ // GxEPD2's own refresh cycle - draw callbacks, the SPI write burst, and
|
|
|
+ // above all the busy-pin wait - is synchronous top to bottom, with no
|
|
|
+ // yield points we can hook to turn it into the kind of chunked,
|
|
|
+ // resumable loop() state machine ESPHome components are expected to be
|
|
|
+ // (that's how esphome's own epaper_spi avoids ever blocking: its
|
|
|
+ // update() just arms a flag and returns, and loop() polls busy
|
|
|
+ // non-blockingly on every main-loop tick). Rewriting GxEPD2 itself to
|
|
|
+ // work that way is out of reach here, so instead the whole sequence
|
|
|
+ // runs on its own FreeRTOS task: update() just wakes this task and
|
|
|
+ // returns immediately, so the real several-seconds-long wait happens
|
|
|
+ // off the main loop entirely, and WiFi/API/every other component stay
|
|
|
+ // responsive through a refresh instead of stalling with it.
|
|
|
+ //
|
|
|
+ // Caveats accepted for now, matching this project's actual constraint
|
|
|
+ // (an update() that blocks the whole app is a non-starter, full stop):
|
|
|
+ // - The draw lambda (and anything it reads - sensors, globals, other
|
|
|
+ // components) now runs off the main loop's thread. ESPHome code is
|
|
|
+ // generally written assuming single-threaded access; simple POD
|
|
|
+ // reads (numbers, bools) are practically fine, but this is a real
|
|
|
+ // departure from the norm worth keeping in mind for anything richer.
|
|
|
+ // - A second update() call while one is still running is rejected
|
|
|
+ // outright (logged, not queued) via update_running_ below - no
|
|
|
+ // attempt to serialize or coalesce them.
|
|
|
+ // - The SPI bus is only actually held (bus acquired, CS asserted) for
|
|
|
+ // the write bursts, not for the busy-pin wait in between, so a
|
|
|
+ // shared spi: bus should mostly be fine - but that's about the
|
|
|
+ // write phase, not this task boundary, and hasn't been stress-tested
|
|
|
+ // against another device on the same bus mid-refresh.
|
|
|
+ xTaskCreate(&GxEPD2Display::update_task_trampoline_, "gxepd2_update", 4096, this, 1, &this->update_task_);
|
|
|
}
|
|
|
|
|
|
void update() override {
|
|
|
- // GxEPD2 blocks synchronously on the panel's busy pin for the whole
|
|
|
- // call below - this isn't peeking at gxepd2 internals, just timing the
|
|
|
- // one call from the esphome side, to make that stall visible in the log
|
|
|
- // instead of it looking like the main loop silently hung.
|
|
|
- uint32_t start = millis();
|
|
|
- this->driver_->setFullWindow();
|
|
|
- this->driver_->firstPage();
|
|
|
- do {
|
|
|
- this->do_update_();
|
|
|
- } while (this->driver_->nextPage());
|
|
|
- ESP_LOGD(TAG, "Display update took %u ms", (unsigned) (millis() - start));
|
|
|
+ bool expected = false;
|
|
|
+ if (!this->update_running_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
|
|
|
+ ESP_LOGW(TAG, "Skipping update: previous display refresh is still running");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ xTaskNotifyGive(this->update_task_);
|
|
|
}
|
|
|
|
|
|
void dump_config() override {
|
|
|
@@ -160,6 +193,29 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
int16_t cs2_{-1};
|
|
|
DisplayT *driver_{nullptr};
|
|
|
SPIClass spi_;
|
|
|
+
|
|
|
+ private:
|
|
|
+ static void update_task_trampoline_(void *param) { static_cast<GxEPD2Display *>(param)->update_task_loop_(); }
|
|
|
+
|
|
|
+ // Blocks (cooperatively - ulTaskNotifyTake sleeps this task, not the CPU)
|
|
|
+ // until update() wakes it, runs one full refresh, then goes back to
|
|
|
+ // sleep. Never returns; this is the task's entire body for its lifetime.
|
|
|
+ [[noreturn]] void update_task_loop_() {
|
|
|
+ while (true) {
|
|
|
+ ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
|
+ uint32_t start = millis();
|
|
|
+ this->driver_->setFullWindow();
|
|
|
+ this->driver_->firstPage();
|
|
|
+ do {
|
|
|
+ this->do_update_();
|
|
|
+ } while (this->driver_->nextPage());
|
|
|
+ ESP_LOGD(TAG, "Display update took %u ms", (unsigned) (millis() - start));
|
|
|
+ this->update_running_.store(false, std::memory_order_release);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ TaskHandle_t update_task_{nullptr};
|
|
|
+ std::atomic<bool> update_running_{false};
|
|
|
};
|
|
|
|
|
|
} // namespace esphome::gxepd2
|