#pragma once // Minimal SPIClass/SPISettings substitute so GxEPD2 compiles under ESP-IDF, // backed by esphome::spi::SPIDevice underneath. GxEPD2 always drives // chip-select itself around each transfer, so this only needs to own the // clock/data lines - which is what lets a gxepd2 instance share one spi: // bus with other devices, each with its own SPIDevice registration. #include #include #include "esphome/core/hal.h" #include "esphome/components/spi/spi.h" class SPISettings { public: SPISettings() = default; SPISettings(uint32_t clock, uint8_t bit_order, uint8_t data_mode) {} }; #define MSBFIRST 0 #define SPI_MODE0 0 class SPIClass : public esphome::spi::SPIDevice { public: void begin() {} void end() {} void beginTransaction(SPISettings settings) { this->enable(); } void endTransaction() { this->disable(); } // GxEPD2 panels (all but the IT8951-based ones, which aren't supported - // see display.py) never read anything back over MISO; e-paper wiring // normally leaves it unconnected, and esphome's spi: bus then registers // this device as write-only. transfer_byte()/delegate transfer() ask for // a real bidirectional exchange regardless, which a write-only device // refuses outright - it doesn't just skip the read, it drops the write // too (see SPIDelegateHw::transfer in spi_esp_idf.cpp), silently bricking // every panel command. write_byte() only ever asks to write, so it works // whether or not MISO is wired. uint8_t transfer(uint8_t data) { // A full-frame refresh calls this thousands of times back-to-back (a // 4-color panel's write loop alone is tens of thousands of bytes), with // no yield anywhere in GxEPD2's own per-byte loops, and ESP-IDF's // polling-mode SPI transaction doesn't yield the scheduler while it // waits either. Left unchecked, that starves this core's idle task // long enough to trip ITS watchdog - a hard crash, not just a stall. // Resetting our own task's watchdog subscription doesn't stop the // idle-task starvation on its own, and a plain yield() just hands the // CPU right back to us since nothing else made us not-ready - only a // real blocking delay actually lets a lower-priority task like idle // run, so force one periodically. esp_task_wdt_reset() (not // esphome::App.feed_wdt(), whose rate-limiting is global and gets // satisfied by the main loop task's own frequent calls regardless of // whether this task's subscription has actually been reset) keeps // this task itself from tripping the watchdog across a long burst. if ((this->transfer_count_++ & 0xFF) == 0) { esp_task_wdt_reset(); esphome::delay(1); } this->write_byte(data); return 0; } private: uint32_t transfer_count_{0}; }; // Unused: every gxepd2 instance overrides this via selectSPI() with its own // SPIClass member - see gxepd2_display.h. inline SPIClass SPI; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)