From e558af37abf4d574f6b75166170adc6d8ec32cf8 Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Sun, 12 Jul 2026 21:07:03 -0700 Subject: [PATCH 1/9] Add display PM locks for auto light sleep --- .../FreeInkDisplay/src/FreeInkDisplay.cpp | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp index 4f1e35da..e4786c74 100644 --- a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp +++ b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp @@ -1,12 +1,16 @@ #include "FreeInkDisplay.h" #include +#include #include #ifndef ARDUINO #include #include #endif +#if defined(ARDUINO) && CONFIG_PM_ENABLE +#include +#endif #if FREEINK_FB_PSRAM #include @@ -51,6 +55,47 @@ RefreshMode toInternal(FreeInkDisplay::RefreshMode m) { default: return RefreshMode::Fast; } } + +#if defined(ARDUINO) && CONFIG_PM_ENABLE +esp_pm_lock_handle_t displayNoLightSleepLock() { + static esp_pm_lock_handle_t lock = nullptr; + static bool initialized = false; + if (!initialized) { + initialized = true; + if (esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "freeink-display", &lock) != ESP_OK) { + lock = nullptr; + } + } + return lock; +} + +class DisplayPmLock { + public: + DisplayPmLock() { + esp_pm_lock_handle_t lock = displayNoLightSleepLock(); + _lock = lock; + _acquired = lock != nullptr && esp_pm_lock_acquire(lock) == ESP_OK; + } + + ~DisplayPmLock() { + if (_acquired) esp_pm_lock_release(_lock); + } + + DisplayPmLock(const DisplayPmLock&) = delete; + DisplayPmLock& operator=(const DisplayPmLock&) = delete; + + private: + esp_pm_lock_handle_t _lock = nullptr; + bool _acquired = false; +}; +#else +class DisplayPmLock { + public: + DisplayPmLock() = default; + DisplayPmLock(const DisplayPmLock&) = delete; + DisplayPmLock& operator=(const DisplayPmLock&) = delete; +}; +#endif } // namespace FreeInkDisplay::FreeInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy) @@ -122,6 +167,7 @@ void FreeInkDisplay::selectDriver() { } void FreeInkDisplay::begin() { + DisplayPmLock pmLock; selectDriver(); // External-library drivers (e.g. M5GFX) own the SPI/display hardware; only @@ -321,18 +367,21 @@ bool FreeInkDisplay::hasSecondaryBuffer() const { return frameBufferActive != nu void FreeInkDisplay::syncPendingAsync() { if (!_asyncPending) return; + DisplayPmLock pmLock; _bus.waitBusy("async refresh"); _asyncPending = false; } bool FreeInkDisplay::refreshBusy() { if (!_asyncPending) return false; + DisplayPmLock pmLock; if (_bus.isBusy()) return true; _asyncPending = false; return false; } void FreeInkDisplay::displayBuffer(RefreshMode mode, bool turnOffScreen) { + DisplayPmLock pmLock; #if defined(SSD1677_PROBE_DEBUG) && SSD1677_PROBE_DEBUG Serial.printf("[EPD] displayBuffer mode=%d off=%d\n", (int)mode, (int)turnOffScreen); #endif @@ -349,6 +398,7 @@ void FreeInkDisplay::displayBuffer(RefreshMode mode, bool turnOffScreen) { } void FreeInkDisplay::displayBufferAsync(RefreshMode mode) { + DisplayPmLock pmLock; syncPendingAsync(); #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE if (_asyncShadow == nullptr) { @@ -373,6 +423,7 @@ void FreeInkDisplay::displayBufferAsync(RefreshMode mode) { } void FreeInkDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool turnOffScreen) { + DisplayPmLock pmLock; #if defined(SSD1677_PROBE_DEBUG) && SSD1677_PROBE_DEBUG Serial.printf("[EPD] displayWindow %u,%u %ux%u\n", x, y, w, h); #endif @@ -386,6 +437,7 @@ void FreeInkDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t } void FreeInkDisplay::displayGrayBuffer(bool turnOffScreen, const unsigned char* lut, bool factoryMode) { + DisplayPmLock pmLock; #if defined(SSD1677_PROBE_DEBUG) && SSD1677_PROBE_DEBUG Serial.printf("[EPD] displayGrayBuffer\n"); #endif @@ -397,30 +449,41 @@ void FreeInkDisplay::displayGrayBuffer(bool turnOffScreen, const unsigned char* void FreeInkDisplay::refreshDisplay(RefreshMode mode, bool turnOffScreen) { displayBuffer(mode, turnOffScreen); } void FreeInkDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { + DisplayPmLock pmLock; _driver->copyGrayscaleLsb(_bus, lsbBuffer); _driver->copyGrayscaleMsb(_bus, msbBuffer); } void FreeInkDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) { + DisplayPmLock pmLock; syncPendingAsync(); _shadowValid = false; _driver->displayGrayscaleBase(_bus, frameBuffer, toInternal(fallback), turnOffScreen); } void FreeInkDisplay::preconditionGrayscale() { + DisplayPmLock pmLock; _driver->preconditionGrayscale(_bus, 0, 0, getDisplayWidth(), getDisplayHeight()); } void FreeInkDisplay::preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + DisplayPmLock pmLock; _driver->preconditionGrayscale(_bus, x, y, w, h); } -void FreeInkDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { _driver->copyGrayscaleLsb(_bus, lsbBuffer); } +void FreeInkDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { + DisplayPmLock pmLock; + _driver->copyGrayscaleLsb(_bus, lsbBuffer); +} -void FreeInkDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { _driver->copyGrayscaleMsb(_bus, msbBuffer); } +void FreeInkDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { + DisplayPmLock pmLock; + _driver->copyGrayscaleMsb(_bus, msbBuffer); +} void FreeInkDisplay::writeGrayscalePlaneStrip(GrayPlane plane, const uint8_t* rows, uint16_t yStart, uint16_t numRows) { + DisplayPmLock pmLock; _driver->writeGrayscalePlaneStrip(_bus, plane == GRAY_PLANE_LSB ? freeink::GrayPlane::Lsb : freeink::GrayPlane::Msb, rows, yStart, numRows); } @@ -429,6 +492,7 @@ bool FreeInkDisplay::supportsStripGrayscale() const { return _driver && _driver- #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE void FreeInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { + DisplayPmLock pmLock; _driver->cleanupGrayscaleBuffers(_bus, bwBuffer); // Restore frameBuffer so subsequent BW draws paint onto a valid BW baseline // rather than the stale LSB/MSB grayscale plane data that was there before. @@ -437,6 +501,7 @@ void FreeInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { } #else void FreeInkDisplay::cleanupGrayscaleWithPreviousBuffer() { + DisplayPmLock pmLock; const uint8_t* baseline = frameBufferActive ? frameBufferActive : frameBuffer; _driver->cleanupGrayscaleBuffers(_bus, baseline); if (frameBuffer && baseline && frameBuffer != baseline) @@ -465,14 +530,17 @@ uint16_t FreeInkDisplay::fastRefreshCutoffMs() const { } void FreeInkDisplay::grayscaleRevert() { + DisplayPmLock pmLock; if (_driver) _driver->grayscaleRevert(_bus, frameBuffer); } void FreeInkDisplay::setCustomLUT(bool enabled, const unsigned char* lutData) { + DisplayPmLock pmLock; if (_driver) _driver->setCustomLut(_bus, enabled, lutData); } void FreeInkDisplay::deepSleep() { + DisplayPmLock pmLock; syncPendingAsync(); if (_driver) _driver->deepSleep(_bus); } From 39c98f40295083d7f64523e0a91974bb2e01c20a Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Sun, 12 Jul 2026 22:12:58 -0700 Subject: [PATCH 2/9] Add APB PM lock around EPD SPI transactions --- .../display/FreeInkDisplay/src/bus/EpdBus.cpp | 56 +++++++++++++++++++ libs/display/FreeInkDisplay/src/bus/EpdBus.h | 4 ++ 2 files changed, 60 insertions(+) diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp index dc17ab0f..aafd8f9d 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp @@ -1,15 +1,37 @@ #include "EpdBus.h" #include +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE +#include +#endif namespace freeink { +namespace { + +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE +esp_pm_lock_handle_t epdSpiApbLock() { + static esp_pm_lock_handle_t lock = nullptr; + static bool attempted = false; + if (!attempted) { + attempted = true; + if (esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, "freeink-epd-spi", &lock) != ESP_OK) { + lock = nullptr; + } + } + return lock; +} +#endif + +} // namespace + void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_t spiMiso, int8_t coCs) { _pins = pins; _spiHz = spiHz; _busy = busy; _coCs = coCs; _spi = SPISettings(spiHz, MSBFIRST, SPI_MODE0); + _spiApbLockHeld = false; // Power the EPD rail first (boards that gate it, e.g. Sticky's EP_PWR_EN), so the // panel is alive before SPI bring-up and the reset pulse. No-op when unassigned. @@ -49,34 +71,65 @@ void EpdBus::reset(uint16_t extraSettleMs) { } } +void EpdBus::acquireSpiPmLock() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + if (_spiApbLockHeld) { + return; + } + esp_pm_lock_handle_t lock = epdSpiApbLock(); + if (lock != nullptr && esp_pm_lock_acquire(lock) == ESP_OK) { + _spiApbLockHeld = true; + } +#endif +} + +void EpdBus::releaseSpiPmLock() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + if (!_spiApbLockHeld) { + return; + } + esp_pm_lock_handle_t lock = epdSpiApbLock(); + if (lock != nullptr && esp_pm_lock_release(lock) == ESP_OK) { + _spiApbLockHeld = false; + } +#endif +} + void EpdBus::cmd(uint8_t c) { + acquireSpiPmLock(); SPI.beginTransaction(_spi); digitalWrite(_pins.dc, LOW); digitalWrite(_pins.cs, LOW); SPI.transfer(c); digitalWrite(_pins.cs, HIGH); SPI.endTransaction(); + releaseSpiPmLock(); } void EpdBus::data(uint8_t d) { + acquireSpiPmLock(); SPI.beginTransaction(_spi); digitalWrite(_pins.dc, HIGH); digitalWrite(_pins.cs, LOW); SPI.transfer(d); digitalWrite(_pins.cs, HIGH); SPI.endTransaction(); + releaseSpiPmLock(); } void EpdBus::data(const uint8_t* d, uint16_t len) { + acquireSpiPmLock(); SPI.beginTransaction(_spi); digitalWrite(_pins.dc, HIGH); digitalWrite(_pins.cs, LOW); SPI.writeBytes(d, len); digitalWrite(_pins.cs, HIGH); SPI.endTransaction(); + releaseSpiPmLock(); } void EpdBus::cmdData(uint8_t c, const uint8_t* d, uint16_t len) { + acquireSpiPmLock(); SPI.beginTransaction(_spi); digitalWrite(_pins.cs, LOW); digitalWrite(_pins.dc, LOW); @@ -87,6 +140,7 @@ void EpdBus::cmdData(uint8_t c, const uint8_t* d, uint16_t len) { } digitalWrite(_pins.cs, HIGH); SPI.endTransaction(); + releaseSpiPmLock(); } void EpdBus::cmdData2(uint8_t c, uint8_t d0, uint8_t d1) { @@ -98,6 +152,7 @@ void EpdBus::beginTxn() { if (_coCs >= 0) { digitalWrite(_coCs, HIGH); } + acquireSpiPmLock(); SPI.beginTransaction(_spi); digitalWrite(_pins.cs, LOW); } @@ -105,6 +160,7 @@ void EpdBus::beginTxn() { void EpdBus::endTxn() { digitalWrite(_pins.cs, HIGH); SPI.endTransaction(); + releaseSpiPmLock(); } void EpdBus::rawCmd(uint8_t c) { diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.h b/libs/display/FreeInkDisplay/src/bus/EpdBus.h index 610a8636..372f1791 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.h +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.h @@ -121,11 +121,15 @@ class EpdBus { delay(fallbackDelayMs); } + void acquireSpiPmLock(); + void releaseSpiPmLock(); + EpdPins _pins{-1, -1, -1, -1, -1, -1}; SPISettings _spi; BusyPolarity _busy = BusyPolarity::ActiveHigh; uint32_t _spiHz = 40000000; int8_t _coCs = -1; + bool _spiApbLockHeld = false; }; } // namespace freeink From c760dc637b5f809fdefc6b9f778b07eef89bc547 Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Sun, 12 Jul 2026 22:52:10 -0700 Subject: [PATCH 3/9] Refactor EPD bus transactions to RAII --- .../display/FreeInkDisplay/src/bus/EpdBus.cpp | 208 ++++++++++++------ libs/display/FreeInkDisplay/src/bus/EpdBus.h | 57 ++++- .../src/driver/Ed2208M5Driver.cpp | 73 +++--- .../src/driver/Ed2208M5Driver.h | 2 +- .../src/driver/Uc8253MurphyDriver.cpp | 5 +- .../src/driver/Uc8253X3Driver.cpp | 6 +- 6 files changed, 227 insertions(+), 124 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp index aafd8f9d..9203eed4 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp @@ -21,17 +21,134 @@ esp_pm_lock_handle_t epdSpiApbLock() { } return lock; } + +esp_pm_lock_handle_t epdNoLightSleepLock() { + static esp_pm_lock_handle_t lock = nullptr; + static bool attempted = false; + if (!attempted) { + attempted = true; + if (esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "freeink-epd-bus", &lock) != ESP_OK) { + lock = nullptr; + } + } + return lock; +} #endif +class NoLightSleepLock { + public: + NoLightSleepLock() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + _lock = epdNoLightSleepLock(); + _acquired = _lock != nullptr && esp_pm_lock_acquire(_lock) == ESP_OK; +#endif + } + + ~NoLightSleepLock() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + if (_acquired) esp_pm_lock_release(_lock); +#endif + } + + NoLightSleepLock(const NoLightSleepLock&) = delete; + NoLightSleepLock& operator=(const NoLightSleepLock&) = delete; + + private: +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + esp_pm_lock_handle_t _lock = nullptr; +#endif + bool _acquired = false; +}; + } // namespace +EpdBus::SpiPmLock::SpiPmLock(bool acquireNow) { + if (acquireNow) { + acquire(); + } +} + +EpdBus::SpiPmLock::~SpiPmLock() { release(); } + +EpdBus::SpiPmLock::SpiPmLock(SpiPmLock&& other) noexcept { + _acquired = other._acquired; + other._acquired = false; +} + +EpdBus::SpiPmLock& EpdBus::SpiPmLock::operator=(SpiPmLock&& other) noexcept { + if (this != &other) { + release(); + _acquired = other._acquired; + other._acquired = false; + } + return *this; +} + +void EpdBus::SpiPmLock::acquire() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + if (_acquired) { + return; + } + esp_pm_lock_handle_t lock = epdSpiApbLock(); + _acquired = lock != nullptr && esp_pm_lock_acquire(lock) == ESP_OK; +#endif +} + +void EpdBus::SpiPmLock::release() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + if (_acquired) { + esp_pm_lock_release(epdSpiApbLock()); + _acquired = false; + } +#endif +} + +EpdBus::Transaction::Transaction(EpdBus& bus) : _bus(&bus), _pmLock(true), _active(true) { + _bus->beginRawTransaction(); +} + +EpdBus::Transaction::~Transaction() { end(); } + +EpdBus::Transaction::Transaction(Transaction&& other) noexcept + : _bus(other._bus), _pmLock(std::move(other._pmLock)), _active(other._active) { + other._bus = nullptr; + other._active = false; +} + +EpdBus::Transaction& EpdBus::Transaction::operator=(Transaction&& other) noexcept { + if (this != &other) { + end(); + _bus = other._bus; + _pmLock = std::move(other._pmLock); + _active = other._active; + other._bus = nullptr; + other._active = false; + } + return *this; +} + +void EpdBus::Transaction::end() { + if (!_active || _bus == nullptr) { + return; + } + _bus->endRawTransaction(); + _pmLock.release(); + _active = false; +} + +void EpdBus::Transaction::cmd(uint8_t c) { _bus->rawCmd(c); } + +void EpdBus::Transaction::data(uint8_t d) { _bus->rawData(d); } + +void EpdBus::Transaction::writeBytes(const uint8_t* d, uint16_t len) { _bus->rawWriteBytes(d, len); } + void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_t spiMiso, int8_t coCs) { + NoLightSleepLock noLightSleepLock; _pins = pins; _spiHz = spiHz; _busy = busy; _coCs = coCs; _spi = SPISettings(spiHz, MSBFIRST, SPI_MODE0); - _spiApbLockHeld = false; // Power the EPD rail first (boards that gate it, e.g. Sticky's EP_PWR_EN), so the // panel is alive before SPI bring-up and the reset pulse. No-op when unassigned. @@ -45,7 +162,10 @@ void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_ delay(100); } - SPI.begin(pins.sclk, spiMiso, pins.mosi, pins.cs); + { + SpiPmLock spiPmLock(true); + SPI.begin(pins.sclk, spiMiso, pins.mosi, pins.cs); + } pinMode(pins.cs, OUTPUT); pinMode(pins.dc, OUTPUT); @@ -60,6 +180,7 @@ void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_ } void EpdBus::reset(uint16_t extraSettleMs) { + NoLightSleepLock noLightSleepLock; digitalWrite(_pins.rst, HIGH); delay(20); digitalWrite(_pins.rst, LOW); @@ -71,76 +192,29 @@ void EpdBus::reset(uint16_t extraSettleMs) { } } -void EpdBus::acquireSpiPmLock() { -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE - if (_spiApbLockHeld) { - return; - } - esp_pm_lock_handle_t lock = epdSpiApbLock(); - if (lock != nullptr && esp_pm_lock_acquire(lock) == ESP_OK) { - _spiApbLockHeld = true; - } -#endif -} - -void EpdBus::releaseSpiPmLock() { -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE - if (!_spiApbLockHeld) { - return; - } - esp_pm_lock_handle_t lock = epdSpiApbLock(); - if (lock != nullptr && esp_pm_lock_release(lock) == ESP_OK) { - _spiApbLockHeld = false; - } -#endif -} +EpdBus::Transaction EpdBus::transaction() { return Transaction(*this); } void EpdBus::cmd(uint8_t c) { - acquireSpiPmLock(); - SPI.beginTransaction(_spi); - digitalWrite(_pins.dc, LOW); - digitalWrite(_pins.cs, LOW); - SPI.transfer(c); - digitalWrite(_pins.cs, HIGH); - SPI.endTransaction(); - releaseSpiPmLock(); + auto txn = transaction(); + txn.cmd(c); } void EpdBus::data(uint8_t d) { - acquireSpiPmLock(); - SPI.beginTransaction(_spi); - digitalWrite(_pins.dc, HIGH); - digitalWrite(_pins.cs, LOW); - SPI.transfer(d); - digitalWrite(_pins.cs, HIGH); - SPI.endTransaction(); - releaseSpiPmLock(); + auto txn = transaction(); + txn.data(d); } void EpdBus::data(const uint8_t* d, uint16_t len) { - acquireSpiPmLock(); - SPI.beginTransaction(_spi); - digitalWrite(_pins.dc, HIGH); - digitalWrite(_pins.cs, LOW); - SPI.writeBytes(d, len); - digitalWrite(_pins.cs, HIGH); - SPI.endTransaction(); - releaseSpiPmLock(); + auto txn = transaction(); + txn.writeBytes(d, len); } void EpdBus::cmdData(uint8_t c, const uint8_t* d, uint16_t len) { - acquireSpiPmLock(); - SPI.beginTransaction(_spi); - digitalWrite(_pins.cs, LOW); - digitalWrite(_pins.dc, LOW); - SPI.transfer(c); + auto txn = transaction(); + txn.cmd(c); if (len > 0 && d != nullptr) { - digitalWrite(_pins.dc, HIGH); - SPI.writeBytes(d, len); + txn.writeBytes(d, len); } - digitalWrite(_pins.cs, HIGH); - SPI.endTransaction(); - releaseSpiPmLock(); } void EpdBus::cmdData2(uint8_t c, uint8_t d0, uint8_t d1) { @@ -148,19 +222,17 @@ void EpdBus::cmdData2(uint8_t c, uint8_t d0, uint8_t d1) { cmdData(c, d, 2); } -void EpdBus::beginTxn() { +void EpdBus::beginRawTransaction() { if (_coCs >= 0) { digitalWrite(_coCs, HIGH); } - acquireSpiPmLock(); SPI.beginTransaction(_spi); digitalWrite(_pins.cs, LOW); } -void EpdBus::endTxn() { +void EpdBus::endRawTransaction() { digitalWrite(_pins.cs, HIGH); SPI.endTransaction(); - releaseSpiPmLock(); } void EpdBus::rawCmd(uint8_t c) { @@ -272,11 +344,10 @@ void EpdBus::writeMirroredPlane(const uint8_t* plane, uint16_t height, uint16_t void EpdBus::sendPlaneFlipped(uint8_t ramCmd, const uint8_t* plane, uint16_t height, uint16_t widthBytes) { cmd(ramCmd); // own CS pulse - beginTxn(); // single CS-low burst for the whole plane + auto txn = transaction(); // single CS-low burst for the whole plane for (int y = static_cast(height) - 1; y >= 0; y--) { - rawWriteBytes(plane + static_cast(y) * widthBytes, widthBytes); + txn.writeBytes(plane + static_cast(y) * widthBytes, widthBytes); } - endTxn(); } void EpdBus::fillPlane(uint8_t ramCmd, uint8_t fillByte, uint16_t height, uint16_t widthBytes) { @@ -284,11 +355,10 @@ void EpdBus::fillPlane(uint8_t ramCmd, uint8_t fillByte, uint16_t height, uint16 if (widthBytes > sizeof(row)) widthBytes = sizeof(row); memset(row, fillByte, widthBytes); cmd(ramCmd); - beginTxn(); + auto txn = transaction(); for (uint16_t y = 0; y < height; y++) { - rawWriteBytes(row, widthBytes); + txn.writeBytes(row, widthBytes); } - endTxn(); } } // namespace freeink diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.h b/libs/display/FreeInkDisplay/src/bus/EpdBus.h index 372f1791..54eda38f 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.h +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.h @@ -9,6 +9,7 @@ #include #include +#include namespace freeink { @@ -33,7 +34,47 @@ struct EpdPins { }; class EpdBus { + private: + class SpiPmLock { + public: + SpiPmLock() = default; + explicit SpiPmLock(bool acquireNow); + ~SpiPmLock(); + + SpiPmLock(const SpiPmLock&) = delete; + SpiPmLock& operator=(const SpiPmLock&) = delete; + SpiPmLock(SpiPmLock&& other) noexcept; + SpiPmLock& operator=(SpiPmLock&& other) noexcept; + + void acquire(); + void release(); + + private: + bool _acquired = false; + }; + public: + class Transaction { + public: + explicit Transaction(EpdBus& bus); + ~Transaction(); + + Transaction(const Transaction&) = delete; + Transaction& operator=(const Transaction&) = delete; + Transaction(Transaction&& other) noexcept; + Transaction& operator=(Transaction&& other) noexcept; + + void end(); + void cmd(uint8_t c); + void data(uint8_t d); + void writeBytes(const uint8_t* d, uint16_t len); + + private: + EpdBus* _bus = nullptr; + SpiPmLock _pmLock; + bool _active = false; + }; + // coCs: a co-resident chip-select (e.g. the SD card sharing the SPI bus on // M5 PaperColor) that must be held de-asserted during panel transactions. void begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_t spiMiso = -1, int8_t coCs = -1); @@ -50,12 +91,8 @@ class EpdBus { void cmdData(uint8_t c, const uint8_t* d, uint16_t len); void cmdData2(uint8_t c, uint8_t d0, uint8_t d1); - // Grouped transaction primitives (used by multi-step sequences, e.g. M5). - void beginTxn(); - void endTxn(); - void rawCmd(uint8_t c); // assumes a transaction is open - void rawData(uint8_t d); // assumes a transaction is open - void rawWriteBytes(const uint8_t* d, uint16_t len); // bulk data, transaction open + // Scoped grouped transaction (used by multi-step sequences, e.g. M5). + [[nodiscard]] Transaction transaction(); // Wait for a refresh/operation to finish using the configured (or given) polarity. void waitBusy(const char* tag = nullptr); @@ -121,15 +158,17 @@ class EpdBus { delay(fallbackDelayMs); } - void acquireSpiPmLock(); - void releaseSpiPmLock(); + void beginRawTransaction(); + void endRawTransaction(); + void rawCmd(uint8_t c); + void rawData(uint8_t d); + void rawWriteBytes(const uint8_t* d, uint16_t len); EpdPins _pins{-1, -1, -1, -1, -1, -1}; SPISettings _spi; BusyPolarity _busy = BusyPolarity::ActiveHigh; uint32_t _spiHz = 40000000; int8_t _coCs = -1; - bool _spiApbLockHeld = false; }; } // namespace freeink diff --git a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp index f81ccec7..49ab5487 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp @@ -93,23 +93,22 @@ void Ed2208M5Driver::initController(EpdBus& bus) { 0x84, 1, 0x01, }; - bus.beginTxn(); + auto txn = bus.transaction(); for (size_t i = 0; i < sizeof(initCommands);) { const uint8_t command = initCommands[i++]; const uint8_t length = initCommands[i++]; waitBusy(bus); - bus.rawCmd(command); + txn.cmd(command); for (uint8_t j = 0; j < length; ++j) { - bus.rawData(initCommands[i++]); + txn.data(initCommands[i++]); } } waitBusy(bus); - bus.rawCmd(0x61); - bus.rawData(static_cast((PANEL_WIDTH >> 8) & 0xFF)); - bus.rawData(static_cast(PANEL_WIDTH & 0xFF)); - bus.rawData(static_cast((PANEL_HEIGHT >> 8) & 0xFF)); - bus.rawData(static_cast(PANEL_HEIGHT & 0xFF)); - bus.endTxn(); + txn.cmd(0x61); + txn.data(static_cast((PANEL_WIDTH >> 8) & 0xFF)); + txn.data(static_cast(PANEL_WIDTH & 0xFF)); + txn.data(static_cast((PANEL_HEIGHT >> 8) & 0xFF)); + txn.data(static_cast(PANEL_HEIGHT & 0xFF)); } void Ed2208M5Driver::begin(EpdBus& bus) { @@ -139,8 +138,8 @@ void Ed2208M5Driver::writeFrame(EpdBus& bus, const uint8_t* fb) { #endif uint8_t packedRow[PANEL_WIDTH / 2]; - bus.beginTxn(); - bus.rawCmd(0x10); + auto txn = bus.transaction(); + txn.cmd(0x10); for (uint16_t panelY = 0; panelY < PANEL_HEIGHT; ++panelY) { for (uint16_t panelX = 0; panelX < PANEL_WIDTH; panelX += 2) { const uint16_t leftLogicalX = panelY; @@ -157,12 +156,11 @@ void Ed2208M5Driver::writeFrame(EpdBus& bus, const uint8_t* fb) { packedRow[panelX >> 1] = static_cast(((leftWhite ? EPD_WHITE : EPD_BLACK) << 4) | (rightWhite ? EPD_WHITE : EPD_BLACK)); } - bus.rawWriteBytes(packedRow, sizeof(packedRow)); + txn.writeBytes(packedRow, sizeof(packedRow)); } - bus.endTxn(); } -void Ed2208M5Driver::setPartialWindow(EpdBus& bus, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { +void Ed2208M5Driver::setPartialWindow(EpdBus::Transaction& txn, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { if (w == 0 || h == 0) return; const uint16_t xEnd = static_cast(x + w - 1); const uint16_t yEnd = static_cast(y + h); @@ -173,26 +171,24 @@ void Ed2208M5Driver::setPartialWindow(EpdBus& bus, uint16_t x, uint16_t y, uint1 static_cast(yEnd >> 8), static_cast(yEnd & 0xFF), 0x01, }; - bus.rawCmd(0x83); - for (uint8_t v : window) bus.rawData(v); + txn.cmd(0x83); + for (uint8_t v : window) txn.data(v); } void Ed2208M5Driver::powerOn(EpdBus& bus) { if (_panelPowerOn) return; - bus.beginTxn(); - bus.rawCmd(0x04); + auto txn = bus.transaction(); + txn.cmd(0x04); waitBusy(bus); - bus.endTxn(); _panelPowerOn = true; } void Ed2208M5Driver::powerOff(EpdBus& bus) { if (!_panelPowerOn) return; - bus.beginTxn(); - bus.rawCmd(0x02); - bus.rawData(0x00); + auto txn = bus.transaction(); + txn.cmd(0x02); + txn.data(0x00); waitBusy(bus); - bus.endTxn(); _panelPowerOn = false; } @@ -275,20 +271,20 @@ void Ed2208M5Driver::refresh(EpdBus& bus, uint16_t dirtyX, uint16_t dirtyY, uint _completeNextRefresh = false; powerOn(bus); - bus.beginTxn(); - bus.rawCmd(0x06); - bus.rawData(0x6F); - bus.rawData(0x1F); - bus.rawData(0x17); - bus.rawData(0x27); - setPartialWindow(bus, dirtyX, dirtyY, dirtyW, dirtyH); - bus.rawCmd(0x50); + auto txn = bus.transaction(); + txn.cmd(0x06); + txn.data(0x6F); + txn.data(0x1F); + txn.data(0x17); + txn.data(0x27); + setPartialWindow(txn, dirtyX, dirtyY, dirtyW, dirtyH); + txn.cmd(0x50); // Complete refresh uses the vendor VCOM/CDI for full contrast; the fast // interrupt path keeps the dark-hack value. - bus.rawData(completeWaveform ? 0x3F : DARK_DISPLAY_CTRL); - bus.rawCmd(0x12); - bus.rawData(0x00); - bus.endTxn(); + txn.data(completeWaveform ? 0x3F : DARK_DISPLAY_CTRL); + txn.cmd(0x12); + txn.data(0x00); + txn.end(); if (completeWaveform) { // Run the full OTP waveform to completion. waitBusy()'s generic 100 ms @@ -308,11 +304,10 @@ void Ed2208M5Driver::refresh(EpdBus& bus, uint16_t dirtyX, uint16_t dirtyY, uint delay(10); } delay(BUSY_SETTLE_MS); - bus.beginTxn(); - bus.rawCmd(0x02); // POWER_OFF - bus.rawData(0x00); + auto powerOffTxn = bus.transaction(); + powerOffTxn.cmd(0x02); // POWER_OFF + powerOffTxn.data(0x00); waitBusy(bus); - bus.endTxn(); _panelPowerOn = false; } else { interruptRefresh(bus); diff --git a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.h b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.h index 7113f44f..72ba3293 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.h +++ b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.h @@ -54,7 +54,7 @@ class Ed2208M5Driver : public PanelDriver { void initController(EpdBus& bus); void waitBusy(EpdBus& bus); void writeFrame(EpdBus& bus, const uint8_t* fb); - void setPartialWindow(EpdBus& bus, uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void setPartialWindow(EpdBus::Transaction& txn, uint16_t x, uint16_t y, uint16_t w, uint16_t h); void powerOn(EpdBus& bus); void powerOff(EpdBus& bus); void interruptRefresh(EpdBus& bus); diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index a1211284..a4002900 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -72,7 +72,7 @@ void Uc8253MurphyDriver::loadLut(EpdBus& bus, const Uc8253MurphyLutBank& bank) { // (240x416). Controller pixel (cx,cy) maps to framebuffer (srcX=cy, srcY=fbH-1-cx). void Uc8253MurphyDriver::writePlane(EpdBus& bus, uint8_t command, const uint8_t* fb) { bus.cmd(command); - bus.beginTxn(); + auto txn = bus.transaction(); uint8_t row[CTRL_WB]; for (uint16_t cy = 0; cy < CTRL_H; cy++) { const uint16_t srcX = cy; // 0..415 -> framebuffer column @@ -82,9 +82,8 @@ void Uc8253MurphyDriver::writePlane(EpdBus& bus, uint8_t command, const uint8_t* const uint8_t bit = (fb[srcY * _fbWb + (srcX >> 3)] >> (7 - (srcX & 7))) & 0x01; if (bit) row[cx >> 3] |= static_cast(1 << (7 - (cx & 7))); } - bus.rawWriteBytes(row, CTRL_WB); + txn.writeBytes(row, CTRL_WB); } - bus.endTxn(); } void Uc8253MurphyDriver::fillPlane(EpdBus& bus, uint8_t command, uint8_t fillByte) { diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp index 336ae774..8fd658c8 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp @@ -363,11 +363,11 @@ void Uc8253X3Driver::writeGrayscalePlaneStrip(EpdBus& bus, GrayPlane plane, cons bus.cmd(CMD_PARTIAL_IN); bus.cmdData(CMD_PARTIAL_WINDOW, win, 9); bus.cmd(ramCmd); - bus.beginTxn(); + auto txn = bus.transaction(); for (int r = static_cast(numRows) - 1; r >= 0; r--) { - bus.rawWriteBytes(rows + static_cast(r) * _wb, _wb); + txn.writeBytes(rows + static_cast(r) * _wb, _wb); } - bus.endTxn(); + txn.end(); bus.cmd(CMD_PARTIAL_OUT); if (plane == GrayPlane::Lsb) _grayState.lsbValid = true; } From d967a5f3ace069215f6f76382083239fbcf7fc56 Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Mon, 13 Jul 2026 08:52:22 -0700 Subject: [PATCH 4/9] Refine EPD bus transaction API --- .../display/FreeInkDisplay/src/bus/EpdBus.cpp | 216 +++++++----------- libs/display/FreeInkDisplay/src/bus/EpdBus.h | 94 ++++++-- .../src/driver/Ed2208M5Driver.cpp | 17 +- .../src/driver/Uc8253MurphyDriver.cpp | 3 +- .../src/driver/Uc8253X3Driver.cpp | 2 +- 5 files changed, 175 insertions(+), 157 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp index 9203eed4..9d2470d5 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp @@ -7,104 +7,8 @@ namespace freeink { -namespace { - -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE -esp_pm_lock_handle_t epdSpiApbLock() { - static esp_pm_lock_handle_t lock = nullptr; - static bool attempted = false; - if (!attempted) { - attempted = true; - if (esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, "freeink-epd-spi", &lock) != ESP_OK) { - lock = nullptr; - } - } - return lock; -} - -esp_pm_lock_handle_t epdNoLightSleepLock() { - static esp_pm_lock_handle_t lock = nullptr; - static bool attempted = false; - if (!attempted) { - attempted = true; - if (esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "freeink-epd-bus", &lock) != ESP_OK) { - lock = nullptr; - } - } - return lock; -} -#endif - -class NoLightSleepLock { - public: - NoLightSleepLock() { -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE - _lock = epdNoLightSleepLock(); - _acquired = _lock != nullptr && esp_pm_lock_acquire(_lock) == ESP_OK; -#endif - } - - ~NoLightSleepLock() { -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE - if (_acquired) esp_pm_lock_release(_lock); -#endif - } - - NoLightSleepLock(const NoLightSleepLock&) = delete; - NoLightSleepLock& operator=(const NoLightSleepLock&) = delete; - - private: -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE - esp_pm_lock_handle_t _lock = nullptr; -#endif - bool _acquired = false; -}; - -} // namespace - -EpdBus::SpiPmLock::SpiPmLock(bool acquireNow) { - if (acquireNow) { - acquire(); - } -} - -EpdBus::SpiPmLock::~SpiPmLock() { release(); } - -EpdBus::SpiPmLock::SpiPmLock(SpiPmLock&& other) noexcept { - _acquired = other._acquired; - other._acquired = false; -} - -EpdBus::SpiPmLock& EpdBus::SpiPmLock::operator=(SpiPmLock&& other) noexcept { - if (this != &other) { - release(); - _acquired = other._acquired; - other._acquired = false; - } - return *this; -} - -void EpdBus::SpiPmLock::acquire() { -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE - if (_acquired) { - return; - } - esp_pm_lock_handle_t lock = epdSpiApbLock(); - _acquired = lock != nullptr && esp_pm_lock_acquire(lock) == ESP_OK; -#endif -} - -void EpdBus::SpiPmLock::release() { -#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE - if (_acquired) { - esp_pm_lock_release(epdSpiApbLock()); - _acquired = false; - } -#endif -} - -EpdBus::Transaction::Transaction(EpdBus& bus) : _bus(&bus), _pmLock(true), _active(true) { - _bus->beginRawTransaction(); +EpdBus::Transaction::Transaction(EpdBus& bus) : _bus(&bus), _pmLock(bus), _active(true) { + SPI.beginTransaction(_bus->_spi); } EpdBus::Transaction::~Transaction() { end(); } @@ -131,19 +35,46 @@ void EpdBus::Transaction::end() { if (!_active || _bus == nullptr) { return; } - _bus->endRawTransaction(); + digitalWrite(_bus->_pins.cs, HIGH); + SPI.endTransaction(); _pmLock.release(); _active = false; } -void EpdBus::Transaction::cmd(uint8_t c) { _bus->rawCmd(c); } +uint8_t EpdBus::Transaction::transfer(uint8_t d) { return SPI.transfer(d); } + +void EpdBus::Transaction::cmd(uint8_t c) { + digitalWrite(_bus->_pins.dc, LOW); + SPI.transfer(c); + digitalWrite(_bus->_pins.dc, HIGH); +} -void EpdBus::Transaction::data(uint8_t d) { _bus->rawData(d); } +void EpdBus::Transaction::data(uint8_t d) { + digitalWrite(_bus->_pins.dc, HIGH); + SPI.transfer(d); +} -void EpdBus::Transaction::writeBytes(const uint8_t* d, uint16_t len) { _bus->rawWriteBytes(d, len); } +void EpdBus::Transaction::writeBytes(const uint8_t* d, uint16_t len) { + digitalWrite(_bus->_pins.dc, HIGH); + SPI.writeBytes(d, len); +} + +EpdBus::~EpdBus() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + if (_spiApbLock != nullptr) { + esp_pm_lock_delete(_spiApbLock); + _spiApbLock = nullptr; + } + if (_noLightSleepLock != nullptr) { + esp_pm_lock_delete(_noLightSleepLock); + _noLightSleepLock = nullptr; + } +#endif +} void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_t spiMiso, int8_t coCs) { - NoLightSleepLock noLightSleepLock; + createPmLocks(); + NoLightSleepLockGuard noLightSleepLock(*this); _pins = pins; _spiHz = spiHz; _busy = busy; @@ -163,7 +94,9 @@ void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_ } { - SpiPmLock spiPmLock(true); + // SPI.begin() configures the peripheral/dividers from the current APB clock; + // keep APB fixed here so the setup matches the requested display SPI rate. + SpiApbLockGuard spiPmLock(*this); SPI.begin(pins.sclk, spiMiso, pins.mosi, pins.cs); } @@ -180,7 +113,7 @@ void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_ } void EpdBus::reset(uint16_t extraSettleMs) { - NoLightSleepLock noLightSleepLock; + NoLightSleepLockGuard noLightSleepLock(*this); digitalWrite(_pins.rst, HIGH); delay(20); digitalWrite(_pins.rst, LOW); @@ -194,23 +127,48 @@ void EpdBus::reset(uint16_t extraSettleMs) { EpdBus::Transaction EpdBus::transaction() { return Transaction(*this); } +EpdBus::Transaction EpdBus::beginTxn() { + auto txn = transaction(); + if (_coCs >= 0) { + digitalWrite(_coCs, HIGH); + } + digitalWrite(_pins.cs, LOW); + return txn; +} + void EpdBus::cmd(uint8_t c) { auto txn = transaction(); + if (_coCs >= 0) { + digitalWrite(_coCs, HIGH); + } + digitalWrite(_pins.cs, LOW); txn.cmd(c); } void EpdBus::data(uint8_t d) { auto txn = transaction(); + if (_coCs >= 0) { + digitalWrite(_coCs, HIGH); + } + digitalWrite(_pins.cs, LOW); txn.data(d); } void EpdBus::data(const uint8_t* d, uint16_t len) { auto txn = transaction(); + if (_coCs >= 0) { + digitalWrite(_coCs, HIGH); + } + digitalWrite(_pins.cs, LOW); txn.writeBytes(d, len); } void EpdBus::cmdData(uint8_t c, const uint8_t* d, uint16_t len) { auto txn = transaction(); + if (_coCs >= 0) { + digitalWrite(_coCs, HIGH); + } + digitalWrite(_pins.cs, LOW); txn.cmd(c); if (len > 0 && d != nullptr) { txn.writeBytes(d, len); @@ -222,33 +180,19 @@ void EpdBus::cmdData2(uint8_t c, uint8_t d0, uint8_t d1) { cmdData(c, d, 2); } -void EpdBus::beginRawTransaction() { - if (_coCs >= 0) { - digitalWrite(_coCs, HIGH); +void EpdBus::createPmLocks() { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + if (_spiApbLock == nullptr) { + if (esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, "freeink-epd-spi", &_spiApbLock) != ESP_OK) { + _spiApbLock = nullptr; + } } - SPI.beginTransaction(_spi); - digitalWrite(_pins.cs, LOW); -} - -void EpdBus::endRawTransaction() { - digitalWrite(_pins.cs, HIGH); - SPI.endTransaction(); -} - -void EpdBus::rawCmd(uint8_t c) { - digitalWrite(_pins.dc, LOW); - SPI.transfer(c); - digitalWrite(_pins.dc, HIGH); -} - -void EpdBus::rawData(uint8_t d) { - digitalWrite(_pins.dc, HIGH); - SPI.transfer(d); -} - -void EpdBus::rawWriteBytes(const uint8_t* d, uint16_t len) { - digitalWrite(_pins.dc, HIGH); - SPI.writeBytes(d, len); + if (_noLightSleepLock == nullptr) { + if (esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "freeink-epd-bus", &_noLightSleepLock) != ESP_OK) { + _noLightSleepLock = nullptr; + } + } +#endif } void EpdBus::waitBusy(const char* tag) { waitBusy(_busy, tag); } @@ -345,9 +289,14 @@ void EpdBus::writeMirroredPlane(const uint8_t* plane, uint16_t height, uint16_t void EpdBus::sendPlaneFlipped(uint8_t ramCmd, const uint8_t* plane, uint16_t height, uint16_t widthBytes) { cmd(ramCmd); // own CS pulse auto txn = transaction(); // single CS-low burst for the whole plane + if (_coCs >= 0) { + digitalWrite(_coCs, HIGH); + } + digitalWrite(_pins.cs, LOW); for (int y = static_cast(height) - 1; y >= 0; y--) { txn.writeBytes(plane + static_cast(y) * widthBytes, widthBytes); } + txn.end(); } void EpdBus::fillPlane(uint8_t ramCmd, uint8_t fillByte, uint16_t height, uint16_t widthBytes) { @@ -356,9 +305,14 @@ void EpdBus::fillPlane(uint8_t ramCmd, uint8_t fillByte, uint16_t height, uint16 memset(row, fillByte, widthBytes); cmd(ramCmd); auto txn = transaction(); + if (_coCs >= 0) { + digitalWrite(_coCs, HIGH); + } + digitalWrite(_pins.cs, LOW); for (uint16_t y = 0; y < height; y++) { txn.writeBytes(row, widthBytes); } + txn.end(); } } // namespace freeink diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.h b/libs/display/FreeInkDisplay/src/bus/EpdBus.h index 54eda38f..4fbdcf19 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.h +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.h @@ -10,6 +10,9 @@ #include #include #include +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE +#include +#endif namespace freeink { @@ -35,27 +38,83 @@ struct EpdPins { class EpdBus { private: - class SpiPmLock { +#if defined(ARDUINO) && defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE + // Holds APB at its maximum while SPI timing is configured or bytes are on the bus. + // Use for SPI.begin() and every SPI transaction so DFS cannot change the APB clock mid-operation. + esp_pm_lock_handle_t _spiApbLock = nullptr; + + // Blocks automatic light sleep while panel rails, reset GPIO, or BUSY polling are active. + // Use around non-SPI panel control sequences that must not be paused by light sleep. + esp_pm_lock_handle_t _noLightSleepLock = nullptr; + + template + class PmLockGuard { public: - SpiPmLock() = default; - explicit SpiPmLock(bool acquireNow); - ~SpiPmLock(); + PmLockGuard() = default; + explicit PmLockGuard(EpdBus& bus) : _bus(&bus) { acquire(); } + ~PmLockGuard() { release(); } + + PmLockGuard(const PmLockGuard&) = delete; + PmLockGuard& operator=(const PmLockGuard&) = delete; + PmLockGuard(PmLockGuard&& other) noexcept : _bus(other._bus), _acquired(other._acquired) { + other._bus = nullptr; + other._acquired = false; + } + PmLockGuard& operator=(PmLockGuard&& other) noexcept { + if (this != &other) { + release(); + _bus = other._bus; + _acquired = other._acquired; + other._bus = nullptr; + other._acquired = false; + } + return *this; + } - SpiPmLock(const SpiPmLock&) = delete; - SpiPmLock& operator=(const SpiPmLock&) = delete; - SpiPmLock(SpiPmLock&& other) noexcept; - SpiPmLock& operator=(SpiPmLock&& other) noexcept; + void acquire() { + if (_acquired || _bus == nullptr) { + return; + } + const esp_pm_lock_handle_t lock = _bus->*Lock; + _acquired = lock != nullptr && esp_pm_lock_acquire(lock) == ESP_OK; + } - void acquire(); - void release(); + void release() { + if (!_acquired || _bus == nullptr) { + return; + } + const esp_pm_lock_handle_t lock = _bus->*Lock; + if (lock != nullptr) { + esp_pm_lock_release(lock); + } + _acquired = false; + } private: + EpdBus* _bus = nullptr; bool _acquired = false; }; + using SpiApbLockGuard = PmLockGuard<&EpdBus::_spiApbLock>; + using NoLightSleepLockGuard = PmLockGuard<&EpdBus::_noLightSleepLock>; +#else + class SpiApbLockGuard { + public: + SpiApbLockGuard() = default; + explicit SpiApbLockGuard(EpdBus&) {} + void release() {} + }; + + class NoLightSleepLockGuard { + public: + explicit NoLightSleepLockGuard(EpdBus&) {} + }; +#endif + public: class Transaction { public: + Transaction() = default; explicit Transaction(EpdBus& bus); ~Transaction(); @@ -67,16 +126,18 @@ class EpdBus { void end(); void cmd(uint8_t c); void data(uint8_t d); + uint8_t transfer(uint8_t d); void writeBytes(const uint8_t* d, uint16_t len); private: EpdBus* _bus = nullptr; - SpiPmLock _pmLock; + SpiApbLockGuard _pmLock; bool _active = false; }; // coCs: a co-resident chip-select (e.g. the SD card sharing the SPI bus on // M5 PaperColor) that must be held de-asserted during panel transactions. + ~EpdBus(); void begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_t spiMiso = -1, int8_t coCs = -1); // Hardware reset pulse; extraSettleMs adds a post-reset settle (X3 needs 50 ms). @@ -91,8 +152,8 @@ class EpdBus { void cmdData(uint8_t c, const uint8_t* d, uint16_t len); void cmdData2(uint8_t c, uint8_t d0, uint8_t d1); - // Scoped grouped transaction (used by multi-step sequences, e.g. M5). - [[nodiscard]] Transaction transaction(); + // Scoped SPI/APB transaction that selects the panel and de-selects it on exit. + [[nodiscard]] Transaction beginTxn(); // Wait for a refresh/operation to finish using the configured (or given) polarity. void waitBusy(const char* tag = nullptr); @@ -158,11 +219,8 @@ class EpdBus { delay(fallbackDelayMs); } - void beginRawTransaction(); - void endRawTransaction(); - void rawCmd(uint8_t c); - void rawData(uint8_t d); - void rawWriteBytes(const uint8_t* d, uint16_t len); + void createPmLocks(); + [[nodiscard]] Transaction transaction(); EpdPins _pins{-1, -1, -1, -1, -1, -1}; SPISettings _spi; diff --git a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp index 49ab5487..82440e23 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp @@ -93,7 +93,7 @@ void Ed2208M5Driver::initController(EpdBus& bus) { 0x84, 1, 0x01, }; - auto txn = bus.transaction(); + auto txn = bus.beginTxn(); for (size_t i = 0; i < sizeof(initCommands);) { const uint8_t command = initCommands[i++]; const uint8_t length = initCommands[i++]; @@ -109,6 +109,7 @@ void Ed2208M5Driver::initController(EpdBus& bus) { txn.data(static_cast(PANEL_WIDTH & 0xFF)); txn.data(static_cast((PANEL_HEIGHT >> 8) & 0xFF)); txn.data(static_cast(PANEL_HEIGHT & 0xFF)); + txn.end(); } void Ed2208M5Driver::begin(EpdBus& bus) { @@ -138,7 +139,7 @@ void Ed2208M5Driver::writeFrame(EpdBus& bus, const uint8_t* fb) { #endif uint8_t packedRow[PANEL_WIDTH / 2]; - auto txn = bus.transaction(); + auto txn = bus.beginTxn(); txn.cmd(0x10); for (uint16_t panelY = 0; panelY < PANEL_HEIGHT; ++panelY) { for (uint16_t panelX = 0; panelX < PANEL_WIDTH; panelX += 2) { @@ -158,6 +159,7 @@ void Ed2208M5Driver::writeFrame(EpdBus& bus, const uint8_t* fb) { } txn.writeBytes(packedRow, sizeof(packedRow)); } + txn.end(); } void Ed2208M5Driver::setPartialWindow(EpdBus::Transaction& txn, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { @@ -177,18 +179,20 @@ void Ed2208M5Driver::setPartialWindow(EpdBus::Transaction& txn, uint16_t x, uint void Ed2208M5Driver::powerOn(EpdBus& bus) { if (_panelPowerOn) return; - auto txn = bus.transaction(); + auto txn = bus.beginTxn(); txn.cmd(0x04); waitBusy(bus); + txn.end(); _panelPowerOn = true; } void Ed2208M5Driver::powerOff(EpdBus& bus) { if (!_panelPowerOn) return; - auto txn = bus.transaction(); + auto txn = bus.beginTxn(); txn.cmd(0x02); txn.data(0x00); waitBusy(bus); + txn.end(); _panelPowerOn = false; } @@ -271,7 +275,7 @@ void Ed2208M5Driver::refresh(EpdBus& bus, uint16_t dirtyX, uint16_t dirtyY, uint _completeNextRefresh = false; powerOn(bus); - auto txn = bus.transaction(); + auto txn = bus.beginTxn(); txn.cmd(0x06); txn.data(0x6F); txn.data(0x1F); @@ -304,10 +308,11 @@ void Ed2208M5Driver::refresh(EpdBus& bus, uint16_t dirtyX, uint16_t dirtyY, uint delay(10); } delay(BUSY_SETTLE_MS); - auto powerOffTxn = bus.transaction(); + auto powerOffTxn = bus.beginTxn(); powerOffTxn.cmd(0x02); // POWER_OFF powerOffTxn.data(0x00); waitBusy(bus); + powerOffTxn.end(); _panelPowerOn = false; } else { interruptRefresh(bus); diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index a4002900..690bf356 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -72,7 +72,7 @@ void Uc8253MurphyDriver::loadLut(EpdBus& bus, const Uc8253MurphyLutBank& bank) { // (240x416). Controller pixel (cx,cy) maps to framebuffer (srcX=cy, srcY=fbH-1-cx). void Uc8253MurphyDriver::writePlane(EpdBus& bus, uint8_t command, const uint8_t* fb) { bus.cmd(command); - auto txn = bus.transaction(); + auto txn = bus.beginTxn(); uint8_t row[CTRL_WB]; for (uint16_t cy = 0; cy < CTRL_H; cy++) { const uint16_t srcX = cy; // 0..415 -> framebuffer column @@ -84,6 +84,7 @@ void Uc8253MurphyDriver::writePlane(EpdBus& bus, uint8_t command, const uint8_t* } txn.writeBytes(row, CTRL_WB); } + txn.end(); } void Uc8253MurphyDriver::fillPlane(EpdBus& bus, uint8_t command, uint8_t fillByte) { diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp index 8fd658c8..069e7dd8 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253X3Driver.cpp @@ -363,7 +363,7 @@ void Uc8253X3Driver::writeGrayscalePlaneStrip(EpdBus& bus, GrayPlane plane, cons bus.cmd(CMD_PARTIAL_IN); bus.cmdData(CMD_PARTIAL_WINDOW, win, 9); bus.cmd(ramCmd); - auto txn = bus.transaction(); + auto txn = bus.beginTxn(); for (int r = static_cast(numRows) - 1; r >= 0; r--) { txn.writeBytes(rows + static_cast(r) * _wb, _wb); } From fb90544292db909de3e7f2d37b73aa33c0441bee Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Mon, 13 Jul 2026 08:55:24 -0700 Subject: [PATCH 5/9] Centralize EPD transaction selection --- .../display/FreeInkDisplay/src/bus/EpdBus.cpp | 40 ++++--------------- .../src/driver/Ed2208M5Driver.cpp | 5 --- .../src/driver/Uc8253MurphyDriver.cpp | 1 - 3 files changed, 7 insertions(+), 39 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp index 9d2470d5..4e3554bf 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp @@ -128,47 +128,31 @@ void EpdBus::reset(uint16_t extraSettleMs) { EpdBus::Transaction EpdBus::transaction() { return Transaction(*this); } EpdBus::Transaction EpdBus::beginTxn() { - auto txn = transaction(); if (_coCs >= 0) { digitalWrite(_coCs, HIGH); } + auto txn = transaction(); digitalWrite(_pins.cs, LOW); return txn; } void EpdBus::cmd(uint8_t c) { - auto txn = transaction(); - if (_coCs >= 0) { - digitalWrite(_coCs, HIGH); - } - digitalWrite(_pins.cs, LOW); + auto txn = beginTxn(); txn.cmd(c); } void EpdBus::data(uint8_t d) { - auto txn = transaction(); - if (_coCs >= 0) { - digitalWrite(_coCs, HIGH); - } - digitalWrite(_pins.cs, LOW); + auto txn = beginTxn(); txn.data(d); } void EpdBus::data(const uint8_t* d, uint16_t len) { - auto txn = transaction(); - if (_coCs >= 0) { - digitalWrite(_coCs, HIGH); - } - digitalWrite(_pins.cs, LOW); + auto txn = beginTxn(); txn.writeBytes(d, len); } void EpdBus::cmdData(uint8_t c, const uint8_t* d, uint16_t len) { - auto txn = transaction(); - if (_coCs >= 0) { - digitalWrite(_coCs, HIGH); - } - digitalWrite(_pins.cs, LOW); + auto txn = beginTxn(); txn.cmd(c); if (len > 0 && d != nullptr) { txn.writeBytes(d, len); @@ -288,15 +272,10 @@ void EpdBus::writeMirroredPlane(const uint8_t* plane, uint16_t height, uint16_t void EpdBus::sendPlaneFlipped(uint8_t ramCmd, const uint8_t* plane, uint16_t height, uint16_t widthBytes) { cmd(ramCmd); // own CS pulse - auto txn = transaction(); // single CS-low burst for the whole plane - if (_coCs >= 0) { - digitalWrite(_coCs, HIGH); - } - digitalWrite(_pins.cs, LOW); + auto txn = beginTxn(); // single CS-low burst for the whole plane for (int y = static_cast(height) - 1; y >= 0; y--) { txn.writeBytes(plane + static_cast(y) * widthBytes, widthBytes); } - txn.end(); } void EpdBus::fillPlane(uint8_t ramCmd, uint8_t fillByte, uint16_t height, uint16_t widthBytes) { @@ -304,15 +283,10 @@ void EpdBus::fillPlane(uint8_t ramCmd, uint8_t fillByte, uint16_t height, uint16 if (widthBytes > sizeof(row)) widthBytes = sizeof(row); memset(row, fillByte, widthBytes); cmd(ramCmd); - auto txn = transaction(); - if (_coCs >= 0) { - digitalWrite(_coCs, HIGH); - } - digitalWrite(_pins.cs, LOW); + auto txn = beginTxn(); for (uint16_t y = 0; y < height; y++) { txn.writeBytes(row, widthBytes); } - txn.end(); } } // namespace freeink diff --git a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp index 82440e23..e3d65660 100644 --- a/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Ed2208M5Driver.cpp @@ -109,7 +109,6 @@ void Ed2208M5Driver::initController(EpdBus& bus) { txn.data(static_cast(PANEL_WIDTH & 0xFF)); txn.data(static_cast((PANEL_HEIGHT >> 8) & 0xFF)); txn.data(static_cast(PANEL_HEIGHT & 0xFF)); - txn.end(); } void Ed2208M5Driver::begin(EpdBus& bus) { @@ -159,7 +158,6 @@ void Ed2208M5Driver::writeFrame(EpdBus& bus, const uint8_t* fb) { } txn.writeBytes(packedRow, sizeof(packedRow)); } - txn.end(); } void Ed2208M5Driver::setPartialWindow(EpdBus::Transaction& txn, uint16_t x, uint16_t y, uint16_t w, uint16_t h) { @@ -182,7 +180,6 @@ void Ed2208M5Driver::powerOn(EpdBus& bus) { auto txn = bus.beginTxn(); txn.cmd(0x04); waitBusy(bus); - txn.end(); _panelPowerOn = true; } @@ -192,7 +189,6 @@ void Ed2208M5Driver::powerOff(EpdBus& bus) { txn.cmd(0x02); txn.data(0x00); waitBusy(bus); - txn.end(); _panelPowerOn = false; } @@ -312,7 +308,6 @@ void Ed2208M5Driver::refresh(EpdBus& bus, uint16_t dirtyX, uint16_t dirtyY, uint powerOffTxn.cmd(0x02); // POWER_OFF powerOffTxn.data(0x00); waitBusy(bus); - powerOffTxn.end(); _panelPowerOn = false; } else { interruptRefresh(bus); diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp index 690bf356..3d61d241 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8253MurphyDriver.cpp @@ -84,7 +84,6 @@ void Uc8253MurphyDriver::writePlane(EpdBus& bus, uint8_t command, const uint8_t* } txn.writeBytes(row, CTRL_WB); } - txn.end(); } void Uc8253MurphyDriver::fillPlane(EpdBus& bus, uint8_t command, uint8_t fillByte) { From a697af060280e160291e68b6160abef48dc671b5 Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Mon, 13 Jul 2026 09:25:20 -0700 Subject: [PATCH 6/9] Preserve standalone EPD command framing --- .../display/FreeInkDisplay/src/bus/EpdBus.cpp | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp index 4e3554bf..41de6583 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp @@ -137,25 +137,34 @@ EpdBus::Transaction EpdBus::beginTxn() { } void EpdBus::cmd(uint8_t c) { - auto txn = beginTxn(); - txn.cmd(c); + auto txn = transaction(); + digitalWrite(_pins.dc, LOW); + digitalWrite(_pins.cs, LOW); + txn.transfer(c); } void EpdBus::data(uint8_t d) { - auto txn = beginTxn(); - txn.data(d); + auto txn = transaction(); + digitalWrite(_pins.dc, HIGH); + digitalWrite(_pins.cs, LOW); + txn.transfer(d); } void EpdBus::data(const uint8_t* d, uint16_t len) { - auto txn = beginTxn(); - txn.writeBytes(d, len); + auto txn = transaction(); + digitalWrite(_pins.dc, HIGH); + digitalWrite(_pins.cs, LOW); + SPI.writeBytes(d, len); } void EpdBus::cmdData(uint8_t c, const uint8_t* d, uint16_t len) { - auto txn = beginTxn(); - txn.cmd(c); + auto txn = transaction(); + digitalWrite(_pins.cs, LOW); + digitalWrite(_pins.dc, LOW); + txn.transfer(c); if (len > 0 && d != nullptr) { - txn.writeBytes(d, len); + digitalWrite(_pins.dc, HIGH); + SPI.writeBytes(d, len); } } From ba897dd9f76c821579aadfa129f7ede21e332492 Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Wed, 5 Aug 2026 15:21:54 -0700 Subject: [PATCH 7/9] Restore EPD wait-path documentation --- .../display/FreeInkDisplay/src/bus/EpdBus.cpp | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp index 1ab0de1f..05bb2451 100644 --- a/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp +++ b/libs/display/FreeInkDisplay/src/bus/EpdBus.cpp @@ -9,6 +9,14 @@ namespace freeink { +// ── ISR-driven waveform-completion notification ────────────────────────────── +// A single binary semaphore, shared between the BUSY-pin GPIO ISR and +// waitRefreshComplete(). The ISR is attached only for the duration of one +// refresh wait (and only after the waveform is confirmed running), so it fires +// on the real completion edge, not on the idle->busy transition or SPI noise. +// File-static so the plain-C ISR can reach it; only one panel is ever active at +// a time, so a single instance is safe. DRAM_ATTR keeps it out of flash for the +// IRAM_ATTR ISR. Ported from the CrossPoint community-sdk EInkDisplay. static DRAM_ATTR SemaphoreHandle_t s_epdRefreshDone = nullptr; static void IRAM_ATTR epdBusyIsr() { @@ -92,6 +100,7 @@ void EpdBus::begin(const EpdPins& pins, uint32_t spiHz, BusyPolarity busy, int8_ _coCs = coCs; _spi = SPISettings(spiHz, MSBFIRST, SPI_MODE0); + // One-shot semaphore backing waitRefreshComplete()'s ISR wait (created once). if (!s_epdRefreshDone) s_epdRefreshDone = xSemaphoreCreateBinary(); // Power the EPD rail first (boards that gate it, e.g. Sticky's EP_PWR_EN), so the @@ -282,35 +291,61 @@ void EpdBus::waitBusy(BusyPolarity p, const char* tag) { } void EpdBus::waitRefreshComplete(const char* tag) { + // A host that installed a busy-wait slice hook (e.g. CrossPoint light-sleeping + // through the refresh) must keep the polling path: waitBusy() invokes the slice + // hook on each idle step, while this ISR path sleeps the task on a semaphore and + // never calls it. Bypassing the hook costs that host its power policy (~9% more + // per refresh, measured ~29 mC vs ~26.5 mC on X3), and is a latent hazard: edge + // interrupts do not fire during light sleep, so a completion edge taken while the + // host is slept would be missed and the wait would stall to its 30 s timeout. The + // slice hook already delivers GPIO-precise wake, so the ISR path buys these hosts + // nothing — fall back to the hooked poll. if (_busyWaitSliceHook != nullptr) { waitBusy(tag); return; } + // ISR-driven completion wait: sleep the task on a semaphore and wake on the + // exact BUSY completion edge, instead of polling every 1 ms. Falls back to + // polling if the semaphore could not be created. if (!s_epdRefreshDone) { waitBusy(tag); return; } + // Levels/edge by polarity. X4 (ActiveHigh): working HIGH, done on the HIGH->LOW + // (FALLING) edge. X3 (X3TwoPhase) / ActiveLow: working LOW, done on the LOW->HIGH + // (RISING) edge. const bool activeHigh = (_busy == BusyPolarity::ActiveHigh); const int doneEdge = activeHigh ? FALLING : RISING; const int doneLevel = activeHigh ? LOW : HIGH; const int workingLevel = activeHigh ? HIGH : LOW; const unsigned long start = millis(); + // Confirm the waveform is actually running (BUSY at the working level) before + // arming, so the already-done fast path below can't mistake the pre-start idle + // level for completion. Bounded poll: if BUSY never shows the working level the + // refresh was a no-op or already finished, and the fast path handles it. This + // is a no-op for X3 (displayStart already drove BUSY to LOW) and ~instant for + // X4 (SSD1677 asserts BUSY within microseconds of MASTER_ACTIVATION). { const unsigned long c0 = millis(); while (digitalRead(_pins.busy) != workingLevel && millis() - c0 < 20) delay(1); } - xSemaphoreTake(s_epdRefreshDone, 0); + xSemaphoreTake(s_epdRefreshDone, 0); // drain any stale token attachInterrupt(digitalPinToInterrupt(_pins.busy), epdBusyIsr, doneEdge); + // Fast path: the waveform already finished (edge passed before we armed, or a + // no-op refresh) — BUSY sits at the done level. Nothing to wait for. Safe + // against the arm/edge race: the binary semaphore latches a give from the ISR, + // so a take below returns immediately if the edge fired just after arming. if (digitalRead(_pins.busy) == doneLevel) { detachInterrupt(digitalPinToInterrupt(_pins.busy)); xSemaphoreTake(s_epdRefreshDone, 0); return; } + // Long sleep — fire the power hooks (if any) around it, matching the poll path. const bool hook = (_busyWaitBeginHook != nullptr); if (hook) _busyWaitBeginHook(); xSemaphoreTake(s_epdRefreshDone, pdMS_TO_TICKS(30000)); From ab146d7f8b71ae8cf3de68a6533e168a13d32a87 Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Wed, 5 Aug 2026 15:33:20 -0700 Subject: [PATCH 8/9] fix merge --- libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp index 0305a289..e7c20c36 100644 --- a/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp +++ b/libs/display/FreeInkDisplay/src/FreeInkDisplay.cpp @@ -934,15 +934,9 @@ uint16_t FreeInkDisplay::fastRefreshCutoffMs() const { } void FreeInkDisplay::grayscaleRevert() { -<<<<<<< HEAD - if (_inverted) return; - DisplayPmLock pmLock; - if (_driver) _driver->grayscaleRevert(_bus, frameBuffer); -======= if (_inverted || !_driver) return; DisplayPmLock pmLock; _driver->grayscaleRevert(_bus, frameBuffer); ->>>>>>> 80c9f4f808c6e68fd5a4dc3d6ef026f7c4289396 } void FreeInkDisplay::setCustomLUT(bool enabled, const unsigned char* lutData) { From d932b530ff26fe9e7b0a6567cbc1caa1670f796e Mon Sep 17 00:00:00 2001 From: Nick Maliwacki Date: Wed, 5 Aug 2026 15:50:11 -0700 Subject: [PATCH 9/9] migrate uc8279 driver --- libs/display/FreeInkDisplay/src/driver/Uc8279Driver.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/display/FreeInkDisplay/src/driver/Uc8279Driver.cpp b/libs/display/FreeInkDisplay/src/driver/Uc8279Driver.cpp index fbd12e9a..f09212a0 100644 --- a/libs/display/FreeInkDisplay/src/driver/Uc8279Driver.cpp +++ b/libs/display/FreeInkDisplay/src/driver/Uc8279Driver.cpp @@ -270,11 +270,10 @@ void Uc8279Driver::writeGrayscalePlaneStrip(EpdBus& bus, GrayPlane plane, const bus.cmd(CMD_PARTIAL_IN); bus.cmdData(CMD_PARTIAL_WINDOW, win, 9); bus.cmd(ramCmd); - bus.beginTxn(); + auto txn = bus.beginTxn(); for (int r = static_cast(numRows) - 1; r >= 0; r--) { - bus.rawWriteBytes(rows + static_cast(r) * _wb, _wb); + txn.writeBytes(rows + static_cast(r) * _wb, _wb); } - bus.endTxn(); bus.cmd(CMD_PARTIAL_OUT); if (plane == GrayPlane::Lsb) _lsbValid = true; }