diff --git a/src/graphics/EInkParallelDisplay.cpp b/src/graphics/EInkParallelDisplay.cpp index 293f57e81..552b11747 100644 --- a/src/graphics/EInkParallelDisplay.cpp +++ b/src/graphics/EInkParallelDisplay.cpp @@ -77,12 +77,13 @@ EInkParallelDisplay::~EInkParallelDisplay() bool EInkParallelDisplay::connect() { LOG_INFO("Do EPD init"); + int initRc = BBEP_SUCCESS; if (!epaper) { epaper = new FASTEPD; #if defined(T5_S3_EPAPER_PRO_V1) - epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000); + initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000); #elif defined(T5_S3_EPAPER_PRO_V2) - epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); + initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000); // initialize all port 0 pins (0-7) as outputs / HIGH for (int i = 0; i < 8; i++) { epaper->ioPinMode(i, OUTPUT); @@ -93,6 +94,16 @@ bool EInkParallelDisplay::connect() #endif } + // FastEPD allocates its framebuffer only from PSRAM; if PSRAM init failed the alloc returns + // NULL and initPanel() returns an error, so clearWhite() below would memset(NULL). Skip EInk + // bring-up but return true so OLEDDisplay still allocates its base buffer (base draw ops stay + // safe); displayReady stays false so the FastEPD push paths no-op -> node runs headless. + if (initRc != BBEP_SUCCESS || epaper->currentBuffer() == nullptr) { + LOG_ERROR("EPD framebuffer unavailable (initPanel rc=%d, PSRAM=%u); running headless", initRc, + (unsigned)ESP.getPsramSize()); + return true; + } + // epaper->setRotation(rotation); // does not work, messes up width/height epaper->setMode(BB_MODE_1BPP); epaper->clearWhite(); @@ -103,6 +114,7 @@ bool EInkParallelDisplay::connect() resetGhostPixelTracking(); #endif + displayReady = true; return true; } @@ -187,6 +199,9 @@ void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters) */ void EInkParallelDisplay::display(void) { + if (!displayReady) // no framebuffer (PSRAM absent / init failed) -> nothing to push + return; + const uint16_t w = this->displayWidth; const uint16_t h = this->displayHeight; @@ -400,6 +415,9 @@ void EInkParallelDisplay::resetGhostPixelTracking() */ bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit) { + if (!displayReady) + return false; + uint32_t now = millis(); if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) { display(); @@ -410,6 +428,8 @@ bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit) void EInkParallelDisplay::endUpdate() { + if (!displayReady) + return; { // ensure any async full update is started/completed if (asyncFullRunning.load()) { diff --git a/src/graphics/EInkParallelDisplay.h b/src/graphics/EInkParallelDisplay.h index 81189e400..60ef7f20c 100644 --- a/src/graphics/EInkParallelDisplay.h +++ b/src/graphics/EInkParallelDisplay.h @@ -40,6 +40,9 @@ class EInkParallelDisplay : public OLEDDisplay uint32_t lastDrawMsec = 0; FASTEPD *epaper; + // Set only when connect() fully succeeds; framebuffer-touching methods no-op while false. + bool displayReady = false; + private: // Async full-refresh support std::atomic asyncFullRunning{false}; diff --git a/src/platform/esp32/xtensa_swatomic.c b/src/platform/esp32/xtensa_swatomic.c new file mode 100644 index 000000000..8e905a275 --- /dev/null +++ b/src/platform/esp32/xtensa_swatomic.c @@ -0,0 +1,111 @@ +// Weak software __atomic_*_{1,2,4} for ESP32-S2/S3. -mdisable-hardware-atomics makes +// GCC emit these libcalls, but the precompiled libnewlib ships only the _8 variants and +// some toolchains' libgcc ships none, so S3 links fail on macOS. Weak = a real libgcc +// definition wins with no clash. Spinlock/critical-section like IDF's stdatomic.c. + +#include "sdkconfig.h" + +#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2) + +#include +#include + +#include "freertos/FreeRTOS.h" + +// These names are GCC builtins; declaring them trips -Wbuiltin-declaration-mismatch +// (uint32_t vs GCC's unsigned int, same ABI). Suppressed as IDF's stdatomic.c does. +#pragma GCC diagnostic ignored "-Wbuiltin-declaration-mismatch" + +static portMUX_TYPE s_swatomic_mux = portMUX_INITIALIZER_UNLOCKED; + +#define SWATOMIC_ENTER() portENTER_CRITICAL_SAFE(&s_swatomic_mux) +#define SWATOMIC_EXIT() portEXIT_CRITICAL_SAFE(&s_swatomic_mux) + +// load / store / exchange / compare_exchange for one width. +#define GEN_SWATOMIC_CORE(N, TYPE) \ + __attribute__((weak, used)) TYPE __atomic_load_##N(const volatile void *ptr, int memorder) \ + { \ + (void)memorder; \ + SWATOMIC_ENTER(); \ + TYPE ret = *(const volatile TYPE *)ptr; \ + SWATOMIC_EXIT(); \ + return ret; \ + } \ + __attribute__((weak, used)) void __atomic_store_##N(volatile void *ptr, TYPE val, int memorder) \ + { \ + (void)memorder; \ + SWATOMIC_ENTER(); \ + *(volatile TYPE *)ptr = val; \ + SWATOMIC_EXIT(); \ + } \ + __attribute__((weak, used)) TYPE __atomic_exchange_##N(volatile void *ptr, TYPE val, int memorder) \ + { \ + (void)memorder; \ + SWATOMIC_ENTER(); \ + TYPE old = *(volatile TYPE *)ptr; \ + *(volatile TYPE *)ptr = val; \ + SWATOMIC_EXIT(); \ + return old; \ + } \ + __attribute__((weak, used)) bool __atomic_compare_exchange_##N(volatile void *ptr, void *expected, TYPE desired, \ + bool is_weak, int success, int failure) \ + { \ + (void)is_weak; \ + (void)success; \ + (void)failure; \ + bool ok; \ + SWATOMIC_ENTER(); \ + TYPE cur = *(volatile TYPE *)ptr; \ + if (cur == *(TYPE *)expected) { \ + *(volatile TYPE *)ptr = desired; \ + ok = true; \ + } else { \ + *(TYPE *)expected = cur; \ + ok = false; \ + } \ + SWATOMIC_EXIT(); \ + return ok; \ + } + +// A read-modify-write pair: fetch_ returns the old value, _fetch the +// new. EXPR is evaluated over `old` and `val`. +#define GEN_SWATOMIC_RMW(N, TYPE, NAME, EXPR) \ + __attribute__((weak, used)) TYPE __atomic_fetch_##NAME##_##N(volatile void *ptr, TYPE val, int memorder) \ + { \ + (void)memorder; \ + SWATOMIC_ENTER(); \ + TYPE old = *(volatile TYPE *)ptr; \ + *(volatile TYPE *)ptr = (TYPE)(EXPR); \ + SWATOMIC_EXIT(); \ + return old; \ + } \ + __attribute__((weak, used)) TYPE __atomic_##NAME##_fetch_##N(volatile void *ptr, TYPE val, int memorder) \ + { \ + (void)memorder; \ + SWATOMIC_ENTER(); \ + TYPE old = *(volatile TYPE *)ptr; \ + TYPE nv = (TYPE)(EXPR); \ + *(volatile TYPE *)ptr = nv; \ + SWATOMIC_EXIT(); \ + return nv; \ + } + +#define GEN_SWATOMIC_ALL(N, TYPE) \ + GEN_SWATOMIC_CORE(N, TYPE) \ + GEN_SWATOMIC_RMW(N, TYPE, add, old + val) \ + GEN_SWATOMIC_RMW(N, TYPE, sub, old - val) \ + GEN_SWATOMIC_RMW(N, TYPE, and, old &val) \ + GEN_SWATOMIC_RMW(N, TYPE, or, old | val) \ + GEN_SWATOMIC_RMW(N, TYPE, xor, old ^ val) \ + GEN_SWATOMIC_RMW(N, TYPE, nand, ~(old & val)) + +GEN_SWATOMIC_ALL(1, uint8_t) +GEN_SWATOMIC_ALL(2, uint16_t) +GEN_SWATOMIC_ALL(4, uint32_t) + +#else + +// Keep this a non-empty translation unit on targets that inline hardware atomics. +typedef int swatomic_translation_unit_not_empty; + +#endif // CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32S2 diff --git a/variants/esp32s3/t5s3_epaper/platformio.ini b/variants/esp32s3/t5s3_epaper/platformio.ini index 781b382b5..16997945f 100644 --- a/variants/esp32s3/t5s3_epaper/platformio.ini +++ b/variants/esp32s3/t5s3_epaper/platformio.ini @@ -28,6 +28,14 @@ lib_deps = https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip +; This board's 3.3V octal (AP_3v3) 8MB PSRAM is unreliable at the qio_opi default 80MHz +; (Total PSRAM reads 0); 40MHz makes it enumerate. Forces a from-source build as no +; precompiled 40MHz-octal libs exist; bandwidth is immaterial for an e-paper node. +custom_sdkconfig = + ${esp32s3_base.custom_sdkconfig} + CONFIG_SPIRAM_SPEED_40M=y + CONFIG_SPIRAM_SPEED=40 + [env:t5s3_epaper_inkhud] extends = t5s3_epaper_base, inkhud board_level = extra