| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071 |
- #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 <cstdint>
- #include <esp_task_wdt.h>
- #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<esphome::spi::BIT_ORDER_MSB_FIRST, esphome::spi::CLOCK_POLARITY_LOW,
- esphome::spi::CLOCK_PHASE_LEADING, esphome::spi::DATA_RATE_4MHZ> {
- 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)
|