From 15b6907426b7b84883b9e520fe8e49d7af25d5cd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 16 Sep 2025 19:46:16 +0200 Subject: [PATCH 01/25] Improvements to heap-memory and PSRAM handling (#4791) * Improved heap and PSRAM handling - Segment `allocateData()` uses more elaborate DRAM checking to reduce fragmentation and allow for larger setups to run on low heap - Segment data allocation fails if minimum contiguous block size runs low to keep the UI working - Increased `MAX_SEGMENT_DATA` to account for better segment data handling - Memory allocation functions try to keep enough DRAM for segment data - Added constant `PSRAM_THRESHOLD` to improve PSARM usage - Increase MIN_HEAP_SIZE to reduce risk of breaking UI due to low memory for JSON response - ESP32 makes use of IRAM (no 8bit access) for pixeluffers, freeing up to 50kB of RAM - Fix to properly get available heap on all platforms: added function `getFreeHeapSize()` - Bugfix for effects that divide by SEGLEN: don't run FX in service() if segment is not active -Syntax fix in AR: calloc() uses (numelements, size) as arguments * Added new functions for allocation and heap checking - added `allocate_buffer()` function that can be used to allocate large buffers: takes parameters to set preferred ram location, including 32bit accessible RAM on ESP32. Returns null if heap runs low or switches to PSRAM - getFreeHeapSize() and getContiguousFreeHeap() helper functions for all platforms to correctly report free useable heap - updated some constants - updated segment data allocation to free the data if it is large - replaced "psramsafe" variable with it's #ifdef: BOARD_HAS_PSRAM and made accomodating changes - added some compile-time checks to handle invalid env. definitions - updated all allocation functions and some of the logic behind them - added use of fast RTC-Memory where available - increased MIN_HEAP_SIZE for all systems (improved stability in tests) - updated memory calculation in web-UI to account for required segment buffer - added UI alerts if buffer allocation fails - made getUsedSegmentData() non-private (used in buffer alloc function) - changed MAX_SEGMENT_DATA - added more detailed memory log to DEBUG output - added debug output to buffer alloc function --- wled00/bus_manager.cpp | 25 ++++++ wled00/const.h | 16 ++++ wled00/fcn_declare.h | 39 +++++++++ wled00/util.cpp | 193 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 273 insertions(+) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 62da0da86f..753310a6e6 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -57,6 +57,31 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte #define DEBUGOUT Serial #endif +//util.cpp +// memory allocation wrappers +extern "C" { + // prefer DRAM over PSRAM (if available) in d_ alloc functions + void *d_malloc(size_t); + void *d_calloc(size_t, size_t); + void *d_realloc_malloc(void *ptr, size_t size); + #ifndef ESP8266 + inline void d_free(void *ptr) { heap_caps_free(ptr); } + #else + inline void d_free(void *ptr) { free(ptr); } + #endif + #if defined(BOARD_HAS_PSRAM) + // prefer PSRAM over DRAM in p_ alloc functions + void *p_malloc(size_t); + void *p_calloc(size_t, size_t); + void *p_realloc_malloc(void *ptr, size_t size); + inline void p_free(void *ptr) { heap_caps_free(ptr); } + #else + #define p_malloc d_malloc + #define p_calloc d_calloc + #define p_free d_free + #endif +} + #ifdef WLED_DEBUG #ifndef ESP8266 #include diff --git a/wled00/const.h b/wled00/const.h index c81854dad0..38e4d5d563 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -528,6 +528,22 @@ #endif #endif +// minimum heap size required to process web requests: try to keep free heap above this value +#ifdef ESP8266 + #define MIN_HEAP_SIZE (9*1024) +#else + #define MIN_HEAP_SIZE (15*1024) // WLED allocation functions (util.cpp) try to keep this much contiguous heap free for other tasks +#endif +// threshold for PSRAM use: if heap is running low, requests to allocate_buffer(prefer DRAM) above PSRAM_THRESHOLD may be put in PSRAM +// if heap is depleted, PSRAM will be used regardless of threshold +#if defined(CONFIG_IDF_TARGET_ESP32S3) + #define PSRAM_THRESHOLD (12*1024) // S3 has plenty of DRAM +#elif defined(CONFIG_IDF_TARGET_ESP32) + #define PSRAM_THRESHOLD (5*1024) +#else + #define PSRAM_THRESHOLD (2*1024) // S2 does not have a lot of RAM. C3 and ESP8266 do not support PSRAM: the value is not used +#endif + // Web server limits #ifdef ESP8266 // Minimum heap to consider handling a request diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index a9f14d73c9..77b82de6ec 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -491,6 +491,45 @@ inline uint8_t hw_random8() { return HW_RND_REGISTER; }; inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255 inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255 +// memory allocation wrappers (util.cpp) +extern "C" { + // prefer DRAM in d_xalloc functions, PSRAM as fallback + void *d_malloc(size_t); + void *d_calloc(size_t, size_t); + void *d_realloc_malloc(void *ptr, size_t size); + #ifndef ESP8266 + inline void d_free(void *ptr) { heap_caps_free(ptr); } + #else + inline void d_free(void *ptr) { free(ptr); } + #endif + #if defined(BOARD_HAS_PSRAM) + // prefer PSRAM in p_xalloc functions, DRAM as fallback + void *p_malloc(size_t); + void *p_calloc(size_t, size_t); + void *p_realloc_malloc(void *ptr, size_t size); + inline void p_free(void *ptr) { heap_caps_free(ptr); } + #else + #define p_malloc d_malloc + #define p_calloc d_calloc + #define p_realloc_malloc d_realloc_malloc + #define p_free d_free + #endif +} +#ifndef ESP8266 +inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) +inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block +#else +inline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free heap +inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block +#endif +#define BFRALLOC_NOBYTEACCESS (1 << 0) // ESP32 has 32bit accessible DRAM (usually ~50kB free) that must not be byte-accessed +#define BFRALLOC_PREFER_DRAM (1 << 1) // prefer DRAM over PSRAM +#define BFRALLOC_ENFORCE_DRAM (1 << 2) // use DRAM only, no PSRAM +#define BFRALLOC_PREFER_PSRAM (1 << 3) // prefer PSRAM over DRAM +#define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, otherwise uses DRAM +#define BFRALLOC_CLEAR (1 << 5) // clear allocated buffer after allocation +void *allocate_buffer(size_t size, uint32_t type); + // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard class JSONBufferGuard { diff --git a/wled00/util.cpp b/wled00/util.cpp index 06e32dfa3c..dbc9d97259 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -709,6 +709,199 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { return hw_random(diff) + lowerlimit; } +// PSRAM compile time checks to provide info for misconfigured env +#if defined(BOARD_HAS_PSRAM) + #if defined(IDF_TARGET_ESP32C3) || defined(ESP8266) + #error "ESP32-C3 and ESP8266 with PSRAM is not supported, please remove BOARD_HAS_PSRAM definition" + #else + #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) // PSRAM fix only needed for classic esp32 + // BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used for old "rev.1" esp32 + #warning "BOARD_HAS_PSRAM defined, make sure to use -mfix-esp32-psram-cache-issue to prevent issues on rev.1 ESP32 boards \ + see https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html#esp32-rev-v1-0" + #endif + #endif +#else + #if !defined(IDF_TARGET_ESP32C3) && !defined(ESP8266) + #pragma message("BOARD_HAS_PSRAM not defined, not using PSRAM.") + #endif +#endif + +// memory allocation functions with minimum free heap size check +#ifdef ESP8266 +static void *validateFreeHeap(void *buffer) { + // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not + // note: ESP826 needs very little contiguous heap for webserver, checking total free heap works better + if (getFreeHeapSize() < MIN_HEAP_SIZE) { + free(buffer); + return nullptr; + } + return buffer; +} + +void *d_malloc(size_t size) { + // note: using "if (getContiguousFreeHeap() > MIN_HEAP_SIZE + size)" did perform worse in tests with regards to keeping heap healthy and UI working + void *buffer = malloc(size); + return validateFreeHeap(buffer); +} + +void *d_calloc(size_t count, size_t size) { + void *buffer = calloc(count, size); + return validateFreeHeap(buffer); +} + +// realloc with malloc fallback, note: on ESPS8266 there is no safe way to ensure MIN_HEAP_SIZE during realloc()s, free buffer and allocate new one +void *d_realloc_malloc(void *ptr, size_t size) { + //void *buffer = realloc(ptr, size); + //buffer = validateFreeHeap(buffer); + //if (buffer) return buffer; // realloc successful + //d_free(ptr); // free old buffer if realloc failed (or min heap was exceeded) + //return d_malloc(size); // fallback to malloc + free(ptr); + return d_malloc(size); +} +#else +static void *validateFreeHeap(void *buffer) { + // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not + // TODO: between allocate and free, heap can run low (async web access), only IDF V5 allows for a pre-allocation-check of all free blocks + if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH && getContiguousFreeHeap() < MIN_HEAP_SIZE) { + free(buffer); + return nullptr; + } + return buffer; +} + +#ifdef BOARD_HAS_PSRAM +#define RTC_RAM_THRESHOLD 1024 // use RTC RAM for allocations smaller than this size +#else +#define RTC_RAM_THRESHOLD 65535 // without PSRAM, allow any size into RTC RAM (useful especially on S2 without PSRAM) +#endif + +void *d_malloc(size_t size) { + void *buffer = nullptr; + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + // the newer ESP32 variants have byte-accessible fast RTC memory that can be used as heap, access speed is on-par with DRAM + // the system does prefer normal DRAM until full, since free RTC memory is ~7.5k only, its below the minimum heap threshold and needs to be allocated explicitly + // use RTC RAM for small allocations or if DRAM is running low to improve fragmentation + if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) + buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + if (buffer == nullptr) // no RTC RAM allocation: use DRAM + #endif + buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory + buffer = validateFreeHeap(buffer); // make sure there is enough free heap left + #ifdef BOARD_HAS_PSRAM + if (!buffer) + return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // DRAM failed, use PSRAM if available + #endif + return buffer; +} + +void *d_calloc(size_t count, size_t size) { + void *buffer = d_malloc(count * size); + if (buffer) memset(buffer, 0, count * size); // clear allocated buffer + return buffer; +} + +// realloc with malloc fallback, original buffer is freed if realloc fails but not copied! +void *d_realloc_malloc(void *ptr, size_t size) { + void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + buffer = validateFreeHeap(buffer); + if (buffer) return buffer; // realloc successful + d_free(ptr); // free old buffer if realloc failed (or min heap was exceeded) + return d_malloc(size); // fallback to malloc +} + +#ifdef BOARD_HAS_PSRAM +// p_xalloc: prefer PSRAM, use DRAM as fallback +void *p_malloc(size_t size) { + void *buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + return validateFreeHeap(buffer); +} + +void *p_calloc(size_t count, size_t size) { + void *buffer = p_malloc(count * size); + if (buffer) memset(buffer, 0, count * size); // clear allocated buffer + return buffer; +} + +// realloc with malloc fallback, original buffer is freed if realloc fails but not copied! +void *p_realloc_malloc(void *ptr, size_t size) { + void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (buffer) return buffer; // realloc successful + p_free(ptr); // free old buffer if realloc failed + return p_malloc(size); // fallback to malloc +} +#endif +#endif + +// allocation function for buffers like pixel-buffers and segment data +// optimises the use of memory types to balance speed and heap availability, always favours DRAM if possible +// if multiple conflicting types are defined, the lowest bits of "type" take priority (see fcn_declare.h for types) +void *allocate_buffer(size_t size, uint32_t type) { + void *buffer = nullptr; + #ifdef CONFIG_IDF_TARGET_ESP32 + // only classic ESP32 has "32bit accessible only" aka IRAM type. Using it frees up normal DRAM for other purposes + // this memory region is used for IRAM_ATTR functions, whatever is left is unused and can be used for pixel buffers + // prefer this type over PSRAM as it is slightly faster, except for _pixels where it is on-par as PSRAM-caching does a good job for mostly sequential access + if (type & BFRALLOC_NOBYTEACCESS) { + // prefer 32bit region, then PSRAM, fallback to any heap. Note: if adding "INTERNAL"-flag this wont work + buffer = heap_caps_malloc_prefer(size, 3, MALLOC_CAP_32BIT, MALLOC_CAP_SPIRAM, MALLOC_CAP_8BIT); + buffer = validateFreeHeap(buffer); + } + else + #endif + #if !defined(BOARD_HAS_PSRAM) + buffer = d_malloc(size); + #else + if (type & BFRALLOC_PREFER_DRAM) { + if (getContiguousFreeHeap() < 3*(MIN_HEAP_SIZE/2) + size && size > PSRAM_THRESHOLD) + buffer = p_malloc(size); // prefer PSRAM for large allocations & when DRAM is low + else + buffer = d_malloc(size); // allocate in DRAM if enough free heap is available, PSRAM as fallback + } + else if (type & BFRALLOC_ENFORCE_DRAM) + buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // use DRAM only, otherwise return nullptr + else if (type & BFRALLOC_PREFER_PSRAM) { + // if DRAM is plenty, prefer it over PSRAM for speed, reserve enough DRAM for segment data: if MAX_SEGMENT_DATA is exceeded, always uses PSRAM + if (getContiguousFreeHeap() > 4*MIN_HEAP_SIZE + size + ((uint32_t)(MAX_SEGMENT_DATA - Segment::getUsedSegmentData()))) + buffer = d_malloc(size); + else + buffer = p_malloc(size); // prefer PSRAM + } + else if (type & BFRALLOC_ENFORCE_PSRAM) + buffer = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // use PSRAM only, otherwise return nullptr + buffer = validateFreeHeap(buffer); + #endif + if (buffer && (type & BFRALLOC_CLEAR)) + memset(buffer, 0, size); // clear allocated buffer + /* + #if !defined(ESP8266) && defined(WLED_DEBUG) + if (buffer) { + DEBUG_PRINTF_P(PSTR("*Buffer allocated: size:%d, address:%p"), size, (uintptr_t)buffer); + if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH) + DEBUG_PRINTLN(F(" in DRAM")); + #ifndef CONFIG_IDF_TARGET_ESP32C3 + else if ((uintptr_t)buffer > SOC_EXTRAM_DATA_LOW && (uintptr_t)buffer < SOC_EXTRAM_DATA_HIGH) + DEBUG_PRINTLN(F(" in PSRAM")); + #endif + #ifdef CONFIG_IDF_TARGET_ESP32 + else if ((uintptr_t)buffer > SOC_IRAM_LOW && (uintptr_t)buffer < SOC_IRAM_HIGH) + DEBUG_PRINTLN(F(" in IRAM")); // only used on ESP32 (MALLOC_CAP_32BIT) + #else + else if ((uintptr_t)buffer > SOC_RTC_DRAM_LOW && (uintptr_t)buffer < SOC_RTC_DRAM_HIGH) + DEBUG_PRINTLN(F(" in RTCRAM")); // not available on ESP32 + #endif + else + DEBUG_PRINTLN(F(" in ???")); // unknown (check soc.h for other memory regions) + } else + DEBUG_PRINTF_P(PSTR("Buffer allocation failed: size:%d\n"), size); + #endif + */ + return buffer; +} + + + + // Platform-agnostic SHA1 computation from String input String computeSHA1(const String& input) { #ifdef ESP8266 From c6a4e281645d9e7168d8cff69dd15034df9214e6 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:23:43 +0100 Subject: [PATCH 02/25] WLED-MM adaptations * p_malloc() functions always available, to simplify code by avoiding ifdef's * simplify extern definitions * always use SPIRAM as fallback * implement calloc(), instead of relying on malloc() * we don't (yet) use allocate_buffer() * remove some commented-out code --- wled00/const.h | 11 ++-- wled00/fcn_declare.h | 15 +----- wled00/util.cpp | 119 ++++++++++++++++++++++--------------------- wled00/wled.h | 14 +++++ 4 files changed, 83 insertions(+), 76 deletions(-) diff --git a/wled00/const.h b/wled00/const.h index 38e4d5d563..6ed488e992 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -544,7 +544,11 @@ #define PSRAM_THRESHOLD (2*1024) // S2 does not have a lot of RAM. C3 and ESP8266 do not support PSRAM: the value is not used #endif -// Web server limits +// Web server limits (8k for AsyncWebServer) +#if !defined(MIN_HEAP_SIZE) +#define MIN_HEAP_SIZE 8192 +#endif + #ifdef ESP8266 // Minimum heap to consider handling a request #define WLED_REQUEST_MIN_HEAP (8*1024) @@ -561,11 +565,6 @@ // Websockets do not count against this limit. #define WLED_REQUEST_MAX_QUEUE 6 -//#define MIN_HEAP_SIZE (8k for AsyncWebServer) -#if !defined(MIN_HEAP_SIZE) -#define MIN_HEAP_SIZE 8192 -#endif - // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 #define WLED_MAX_NODES 24 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 77b82de6ec..d54c578690 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -497,23 +497,12 @@ extern "C" { void *d_malloc(size_t); void *d_calloc(size_t, size_t); void *d_realloc_malloc(void *ptr, size_t size); - #ifndef ESP8266 - inline void d_free(void *ptr) { heap_caps_free(ptr); } - #else - inline void d_free(void *ptr) { free(ptr); } - #endif - #if defined(BOARD_HAS_PSRAM) + void d_free(void *ptr); // prefer PSRAM in p_xalloc functions, DRAM as fallback void *p_malloc(size_t); void *p_calloc(size_t, size_t); void *p_realloc_malloc(void *ptr, size_t size); - inline void p_free(void *ptr) { heap_caps_free(ptr); } - #else - #define p_malloc d_malloc - #define p_calloc d_calloc - #define p_realloc_malloc d_realloc_malloc - #define p_free d_free - #endif + void p_free(void *ptr); } #ifndef ESP8266 inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) diff --git a/wled00/util.cpp b/wled00/util.cpp index dbc9d97259..b01a4b575d 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -514,7 +514,7 @@ um_data_t* simulateSound(uint8_t simulationId) if (!um_data) { //claim storage for arrays - fftResult = (uint8_t *)malloc(sizeof(uint8_t) * 16); + fftResult = (uint8_t *)d_malloc(sizeof(uint8_t) * 16); // initialize um_data pointer structure // NOTE!!! @@ -709,22 +709,6 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { return hw_random(diff) + lowerlimit; } -// PSRAM compile time checks to provide info for misconfigured env -#if defined(BOARD_HAS_PSRAM) - #if defined(IDF_TARGET_ESP32C3) || defined(ESP8266) - #error "ESP32-C3 and ESP8266 with PSRAM is not supported, please remove BOARD_HAS_PSRAM definition" - #else - #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) // PSRAM fix only needed for classic esp32 - // BOARD_HAS_PSRAM also means that compiler flag "-mfix-esp32-psram-cache-issue" has to be used for old "rev.1" esp32 - #warning "BOARD_HAS_PSRAM defined, make sure to use -mfix-esp32-psram-cache-issue to prevent issues on rev.1 ESP32 boards \ - see https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html#esp32-rev-v1-0" - #endif - #endif -#else - #if !defined(IDF_TARGET_ESP32C3) && !defined(ESP8266) - #pragma message("BOARD_HAS_PSRAM not defined, not using PSRAM.") - #endif -#endif // memory allocation functions with minimum free heap size check #ifdef ESP8266 @@ -759,86 +743,130 @@ void *d_realloc_malloc(void *ptr, size_t size) { free(ptr); return d_malloc(size); } + +void d_free(void *ptr) { free(ptr); } + +void *p_malloc(size_t size) { return d_malloc(size); } +void *p_calloc(size_t count, size_t size) { return d_calloc(count, size); } +void *p_realloc_malloc(void *ptr, size_t size) { return d_realloc_malloc(ptr, size); } +void p_free(void *ptr) { free(ptr); } + #else static void *validateFreeHeap(void *buffer) { // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not // TODO: between allocate and free, heap can run low (async web access), only IDF V5 allows for a pre-allocation-check of all free blocks +#if 0 // WLEDMM disabled -> TODO need to think about this if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH && getContiguousFreeHeap() < MIN_HEAP_SIZE) { free(buffer); return nullptr; } +#endif return buffer; } #ifdef BOARD_HAS_PSRAM #define RTC_RAM_THRESHOLD 1024 // use RTC RAM for allocations smaller than this size #else -#define RTC_RAM_THRESHOLD 65535 // without PSRAM, allow any size into RTC RAM (useful especially on S2 without PSRAM) +#define RTC_RAM_THRESHOLD (psramFound() ? 65535 : 1024) // without PSRAM, allow any size into RTC RAM (useful especially on S2 without PSRAM) #endif void *d_malloc(size_t size) { void *buffer = nullptr; - #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + #if !defined(CONFIG_IDF_TARGET_ESP32) // the newer ESP32 variants have byte-accessible fast RTC memory that can be used as heap, access speed is on-par with DRAM // the system does prefer normal DRAM until full, since free RTC memory is ~7.5k only, its below the minimum heap threshold and needs to be allocated explicitly // use RTC RAM for small allocations or if DRAM is running low to improve fragmentation if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); if (buffer == nullptr) // no RTC RAM allocation: use DRAM - #endif + buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory + #else buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory + #endif + buffer = validateFreeHeap(buffer); // make sure there is enough free heap left - #ifdef BOARD_HAS_PSRAM - if (!buffer) - return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); // DRAM failed, use PSRAM if available + #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) + if (!buffer && psramFound()) + return heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available + else #endif return buffer; } void *d_calloc(size_t count, size_t size) { - void *buffer = d_malloc(count * size); - if (buffer) memset(buffer, 0, count * size); // clear allocated buffer + // similar to d_malloc bus uses heap_caps_calloc + void *buffer = nullptr; + #if !defined(CONFIG_IDF_TARGET_ESP32) + if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) + buffer = heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + if (buffer == nullptr) // no RTC RAM allocation: use DRAM + buffer = heap_caps_calloc(count, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory + #else + buffer = heap_caps_calloc(count, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory + #endif + + buffer = validateFreeHeap(buffer); // make sure there is enough free heap left + #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) + if (!buffer && psramFound()) + return heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available + else + #endif return buffer; } // realloc with malloc fallback, original buffer is freed if realloc fails but not copied! void *d_realloc_malloc(void *ptr, size_t size) { - void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) + void *buffer = heap_caps_realloc_prefer(ptr, size, 3, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); + #else + void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + #endif buffer = validateFreeHeap(buffer); if (buffer) return buffer; // realloc successful d_free(ptr); // free old buffer if realloc failed (or min heap was exceeded) return d_malloc(size); // fallback to malloc } -#ifdef BOARD_HAS_PSRAM +void d_free(void *ptr) { heap_caps_free(ptr); } +void p_free(void *ptr) { heap_caps_free(ptr); } + +#if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 0) // V4 can auto-detect PSRAM // p_xalloc: prefer PSRAM, use DRAM as fallback void *p_malloc(size_t size) { - void *buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + void *buffer = heap_caps_malloc_prefer(size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); return validateFreeHeap(buffer); } void *p_calloc(size_t count, size_t size) { - void *buffer = p_malloc(count * size); - if (buffer) memset(buffer, 0, count * size); // clear allocated buffer + // similar to p_malloc bus uses heap_caps_calloc + void *buffer = heap_caps_calloc_prefer(count, size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); + return validateFreeHeap(buffer); return buffer; } // realloc with malloc fallback, original buffer is freed if realloc fails but not copied! void *p_realloc_malloc(void *ptr, size_t size) { - void *buffer = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + void *buffer = heap_caps_realloc_prefer(ptr, size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); if (buffer) return buffer; // realloc successful p_free(ptr); // free old buffer if realloc failed return p_malloc(size); // fallback to malloc } + +#else // NO PSRAM support -> fall back to DRAM +void *p_malloc(size_t size) { return d_malloc(size); } +void *p_calloc(size_t count, size_t size) { return d_calloc(count, size); } +void *p_realloc_malloc(void *ptr, size_t size) { return d_realloc_malloc(ptr, size); } #endif #endif + +#if 0 // WLEDMM not used yet // allocation function for buffers like pixel-buffers and segment data // optimises the use of memory types to balance speed and heap availability, always favours DRAM if possible // if multiple conflicting types are defined, the lowest bits of "type" take priority (see fcn_declare.h for types) void *allocate_buffer(size_t size, uint32_t type) { void *buffer = nullptr; - #ifdef CONFIG_IDF_TARGET_ESP32 + #if CONFIG_IDF_TARGET_ESP32 // only classic ESP32 has "32bit accessible only" aka IRAM type. Using it frees up normal DRAM for other purposes // this memory region is used for IRAM_ATTR functions, whatever is left is unused and can be used for pixel buffers // prefer this type over PSRAM as it is slightly faster, except for _pixels where it is on-par as PSRAM-caching does a good job for mostly sequential access @@ -873,33 +901,10 @@ void *allocate_buffer(size_t size, uint32_t type) { #endif if (buffer && (type & BFRALLOC_CLEAR)) memset(buffer, 0, size); // clear allocated buffer - /* - #if !defined(ESP8266) && defined(WLED_DEBUG) - if (buffer) { - DEBUG_PRINTF_P(PSTR("*Buffer allocated: size:%d, address:%p"), size, (uintptr_t)buffer); - if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH) - DEBUG_PRINTLN(F(" in DRAM")); - #ifndef CONFIG_IDF_TARGET_ESP32C3 - else if ((uintptr_t)buffer > SOC_EXTRAM_DATA_LOW && (uintptr_t)buffer < SOC_EXTRAM_DATA_HIGH) - DEBUG_PRINTLN(F(" in PSRAM")); - #endif - #ifdef CONFIG_IDF_TARGET_ESP32 - else if ((uintptr_t)buffer > SOC_IRAM_LOW && (uintptr_t)buffer < SOC_IRAM_HIGH) - DEBUG_PRINTLN(F(" in IRAM")); // only used on ESP32 (MALLOC_CAP_32BIT) - #else - else if ((uintptr_t)buffer > SOC_RTC_DRAM_LOW && (uintptr_t)buffer < SOC_RTC_DRAM_HIGH) - DEBUG_PRINTLN(F(" in RTCRAM")); // not available on ESP32 - #endif - else - DEBUG_PRINTLN(F(" in ???")); // unknown (check soc.h for other memory regions) - } else - DEBUG_PRINTF_P(PSTR("Buffer allocation failed: size:%d\n"), size); - #endif - */ + return buffer; } - - +#endif // Platform-agnostic SHA1 computation from String input diff --git a/wled00/wled.h b/wled00/wled.h index b53034e539..044c19b7bc 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -204,6 +204,20 @@ #undef ALL_JSON_TO_PSRAM #define ALL_JSON_TO_PSRAM +// global WLED memory functions (util.cpp) +extern "C" { + // prefer DRAM in d_xalloc functions, PSRAM as fallback + void *d_malloc(size_t); + void *d_calloc(size_t, size_t); + void *d_realloc_malloc(void *ptr, size_t size); + void d_free(void *ptr); + // prefer PSRAM in p_xalloc functions, DRAM as fallback + void *p_malloc(size_t); + void *p_calloc(size_t, size_t); + void *p_realloc_malloc(void *ptr, size_t size); + void p_free(void *ptr); +} + struct PSRAM_Allocator { void* allocate(size_t size) { if (psramFound()) return ps_malloc(size); // use PSRAM if it exists From d27383532a9c7ddb4508a08bb38fb44798746743 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:27:14 +0100 Subject: [PATCH 03/25] fix an ancient compiler warning in DateStrings.cpp --- wled00/src/dependencies/time/DateStrings.cpp | 54 +++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/wled00/src/dependencies/time/DateStrings.cpp b/wled00/src/dependencies/time/DateStrings.cpp index 3eccff3e75..f0e2aeda00 100644 --- a/wled00/src/dependencies/time/DateStrings.cpp +++ b/wled00/src/dependencies/time/DateStrings.cpp @@ -12,6 +12,9 @@ #if defined(__AVR__) #include #else +#if defined(ARDUINO_ARCH_ESP32) +#include +#else // for compatiblity with Arduino Due and Teensy 3.0 and maybe others? #define PROGMEM #define PGM_P const char * @@ -19,6 +22,7 @@ #define pgm_read_word(addr) (*(const unsigned char **)(addr)) #define strcpy_P(dest, src) strcpy((dest), (src)) #endif +#endif #include // for strcpy_P or strcpy #include "TimeLib.h" @@ -27,43 +31,43 @@ static char buffer[dt_MAX_STRING_LEN+1]; // must be big enough for longest string and the terminating null -const char monthStr0[] PROGMEM = ""; -const char monthStr1[] PROGMEM = "January"; -const char monthStr2[] PROGMEM = "February"; -const char monthStr3[] PROGMEM = "March"; -const char monthStr4[] PROGMEM = "April"; -const char monthStr5[] PROGMEM = "May"; -const char monthStr6[] PROGMEM = "June"; -const char monthStr7[] PROGMEM = "July"; -const char monthStr8[] PROGMEM = "August"; -const char monthStr9[] PROGMEM = "September"; -const char monthStr10[] PROGMEM = "October"; -const char monthStr11[] PROGMEM = "November"; -const char monthStr12[] PROGMEM = "December"; +static const char monthStr0[] PROGMEM = ""; +static const char monthStr1[] PROGMEM = "January"; +static const char monthStr2[] PROGMEM = "February"; +static const char monthStr3[] PROGMEM = "March"; +static const char monthStr4[] PROGMEM = "April"; +static const char monthStr5[] PROGMEM = "May"; +static const char monthStr6[] PROGMEM = "June"; +static const char monthStr7[] PROGMEM = "July"; +static const char monthStr8[] PROGMEM = "August"; +static const char monthStr9[] PROGMEM = "September"; +static const char monthStr10[] PROGMEM = "October"; +static const char monthStr11[] PROGMEM = "November"; +static const char monthStr12[] PROGMEM = "December"; -const PROGMEM char * const PROGMEM monthNames_P[] = +static const PROGMEM char * const PROGMEM monthNames_P[] = { monthStr0,monthStr1,monthStr2,monthStr3,monthStr4,monthStr5,monthStr6, monthStr7,monthStr8,monthStr9,monthStr10,monthStr11,monthStr12 }; -const char monthShortNames_P[] PROGMEM = "ErrJanFebMarAprMayJunJulAugSepOctNovDec"; +static const char monthShortNames_P[] PROGMEM = "ErrJanFebMarAprMayJunJulAugSepOctNovDec"; -const char dayStr0[] PROGMEM = "Err"; -const char dayStr1[] PROGMEM = "Sunday"; -const char dayStr2[] PROGMEM = "Monday"; -const char dayStr3[] PROGMEM = "Tuesday"; -const char dayStr4[] PROGMEM = "Wednesday"; -const char dayStr5[] PROGMEM = "Thursday"; -const char dayStr6[] PROGMEM = "Friday"; -const char dayStr7[] PROGMEM = "Saturday"; +static const char dayStr0[] PROGMEM = "Err"; +static const char dayStr1[] PROGMEM = "Sunday"; +static const char dayStr2[] PROGMEM = "Monday"; +static const char dayStr3[] PROGMEM = "Tuesday"; +static const char dayStr4[] PROGMEM = "Wednesday"; +static const char dayStr5[] PROGMEM = "Thursday"; +static const char dayStr6[] PROGMEM = "Friday"; +static const char dayStr7[] PROGMEM = "Saturday"; -const PROGMEM char * const PROGMEM dayNames_P[] = +static const PROGMEM char * const PROGMEM dayNames_P[] = { dayStr0,dayStr1,dayStr2,dayStr3,dayStr4,dayStr5,dayStr6,dayStr7 }; -const char dayShortNames_P[] PROGMEM = "ErrSunMonTueWedThuFriSat"; +static const char dayShortNames_P[] PROGMEM = "ErrSunMonTueWedThuFriSat"; /* functions to return date strings */ From a861a315cd679a5c37390ce2d3dec103b7c77b3e Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:28:26 +0100 Subject: [PATCH 04/25] use new p_malloc functions with ArduinoJSON --- wled00/src/dependencies/json/AsyncJson-v6.h | 16 +++++++++++++++- wled00/wled.h | 8 +++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/wled00/src/dependencies/json/AsyncJson-v6.h b/wled00/src/dependencies/json/AsyncJson-v6.h index 32ac546077..20fede3ae1 100644 --- a/wled00/src/dependencies/json/AsyncJson-v6.h +++ b/wled00/src/dependencies/json/AsyncJson-v6.h @@ -15,6 +15,20 @@ #include "ArduinoJson-v6.h" #include +// global WLED memory functions (util.cpp) +extern "C" { + // prefer DRAM in d_xalloc functions, PSRAM as fallback + void *d_malloc(size_t); + void *d_calloc(size_t, size_t); + void *d_realloc_malloc(void *ptr, size_t size); + void d_free(void *ptr); + // prefer PSRAM in p_xalloc functions, DRAM as fallback + void *p_malloc(size_t); + void *p_calloc(size_t, size_t); + void *p_realloc_malloc(void *ptr, size_t size); + void p_free(void *ptr); +} + #ifdef ESP8266 #define DYNAMIC_JSON_DOCUMENT_SIZE 8192 #else @@ -154,7 +168,7 @@ class AsyncCallbackJsonWebHandler: public AsyncWebHandler { if (_onRequest) { _contentLength = total; if (total > 0 && request->_tempObject == NULL && (int)total < _maxContentLength) { - request->_tempObject = malloc(total); + request->_tempObject = p_malloc(total); } if (request->_tempObject != NULL) { memcpy((uint8_t*)(request->_tempObject) + index, data, len); diff --git a/wled00/wled.h b/wled00/wled.h index 044c19b7bc..e4475e06d4 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -220,15 +220,13 @@ extern "C" { struct PSRAM_Allocator { void* allocate(size_t size) { - if (psramFound()) return ps_malloc(size); // use PSRAM if it exists - else return malloc(size); // fallback + return p_malloc(size); // use PSRAM if it exists } void* reallocate(void* ptr, size_t new_size) { - if (psramFound()) return ps_realloc(ptr, new_size); // use PSRAM if it exists - else return realloc(ptr, new_size); // fallback + return p_realloc_malloc(ptr, new_size); // use PSRAM if it exists } void deallocate(void* pointer) { - free(pointer); + p_free(pointer); } }; using PSRAMDynamicJsonDocument = BasicJsonDocument; From b78d5e62ee5a5288365d3d5123f47efcaab17321 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:30:55 +0100 Subject: [PATCH 05/25] use new memory allocator utilities instead of malloc() callloc() and free() --- usermods/artifx/arti.h | 6 ++--- usermods/audioreactive/audio_reactive.h | 12 ++++----- .../usermod_v2_rotary_encoder_ui_ALT.h | 6 ++--- wled00/FX.h | 6 ++--- wled00/FX_2Dfcn.cpp | 11 ++++---- wled00/FX_fcn.cpp | 25 ++++++++++--------- wled00/bus_manager.cpp | 19 ++++---------- wled00/file.cpp | 20 ++++++++------- wled00/presets.cpp | 9 ++----- 9 files changed, 52 insertions(+), 62 deletions(-) diff --git a/usermods/artifx/arti.h b/usermods/artifx/arti.h index 21eb649fbb..5065ec77b0 100644 --- a/usermods/artifx/arti.h +++ b/usermods/artifx/arti.h @@ -2565,7 +2565,7 @@ class ARTI { uint16_t programFileSize; #if ARTI_PLATFORM == ARTI_ARDUINO programFileSize = programFile.size(); - programText = (char *)malloc(programFileSize+1); + programText = (char *)d_malloc(programFileSize+1); programFile.read((byte *)programText, programFileSize); programText[programFileSize] = '\0'; #else @@ -2607,7 +2607,7 @@ class ARTI { #endif if (stages < 1) { - if (nullptr != programText) free(programText); // softhack007 prevent memory leak + if (nullptr != programText) d_free(programText); // softhack007 prevent memory leak close(); return true; } @@ -2666,7 +2666,7 @@ class ARTI { #endif } #if ARTI_PLATFORM == ARTI_ARDUINO //not on windows as cause crash??? - free(programText); + d_free(programText); #endif if (stages >= 3) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 90e1651fcf..f06626c202 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -454,13 +454,13 @@ static bool alocateFFTBuffers(void) { USER_PRINT(F("\nFree heap ")); USER_PRINTLN(ESP.getFreeHeap()); #endif - if (vReal) free(vReal); // should not happen - if (vImag) free(vImag); // should not happen - if ((vReal = (float*) calloc(samplesFFT, sizeof(float))) == nullptr) return false; // calloc or die - if ((vImag = (float*) calloc(samplesFFT, sizeof(float))) == nullptr) return false; + if (vReal) d_free(vReal); // should not happen + if (vImag) d_free(vImag); // should not happen + if ((vReal = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; // calloc or die + if ((vImag = (float*) d_calloc(samplesFFT, sizeof(float))) == nullptr) return false; #ifdef FFT_MAJORPEAK_HUMAN_EAR - if (pinkFactors) free(pinkFactors); - if ((pinkFactors = (float*) calloc(samplesFFT, sizeof(float))) == nullptr) return false; + if (pinkFactors) p_free(pinkFactors); + if ((pinkFactors = (float*) p_calloc(samplesFFT, sizeof(float))) == nullptr) return false; #endif #ifdef SR_DEBUG diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 66a1401a1d..218a668f38 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -352,7 +352,7 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() { } byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) { - byte *indexes = (byte *)malloc(sizeof(byte) * numModes); + byte *indexes = (byte *)d_calloc(numModes, sizeof(byte)); for (byte i = 0; i < numModes; i++) { indexes[i] = i; } @@ -364,7 +364,7 @@ byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) { * They don't end in '\0', they end in '"'. */ const char **RotaryEncoderUIUsermod::re_findModeStrings(const char json[], int numModes) { - const char **modeStrings = (const char **)malloc(sizeof(const char *) * numModes); + const char **modeStrings = (const char **)d_calloc(numModes, sizeof(const char *)); uint8_t modeIndex = 0; bool insideQuotes = false; // advance past the mark for markLineNum that may exist. @@ -380,7 +380,7 @@ const char **RotaryEncoderUIUsermod::re_findModeStrings(const char json[], int n insideQuotes = !insideQuotes; if (insideQuotes) { // We have a new mode or palette - modeStrings[modeIndex] = (char *)(json + i + 1); + if (modeIndex < numModes) modeStrings[modeIndex] = (char *)(json + i + 1); //WLEDMM prevent array bounds violation } break; case '[': diff --git a/wled00/FX.h b/wled00/FX.h index ddfa3c8777..3a03e6ccfa 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -609,7 +609,7 @@ typedef struct Segment { endImagePlayback(this); #endif - if ((Segment::_globalLeds == nullptr) && !strip_uses_global_leds() && (ledsrgb != nullptr)) {free(ledsrgb); ledsrgb = nullptr;} // WLEDMM we need "!strip_uses_global_leds()" to avoid crashes (#104) + if ((Segment::_globalLeds == nullptr) && !strip_uses_global_leds() && (ledsrgb != nullptr)) {d_free(ledsrgb); ledsrgb = nullptr;} // WLEDMM we need "!strip_uses_global_leds()" to avoid crashes (#104) if (name) { delete[] name; name = nullptr; } if (_t) { transitional = false; delete _t; _t = nullptr; } deallocateData(); @@ -1009,7 +1009,7 @@ class WS2812FX { // 96 bytes #ifdef WLED_DEBUG if (Serial) Serial.println(F("~WS2812FX destroying strip.")); // WLEDMM can't use DEBUG_PRINTLN here #endif - if (customMappingTable) delete[] customMappingTable; + if (customMappingTable) d_free(customMappingTable); customMappingTable = nullptr; _mode.clear(); _modeData.clear(); _segments.clear(); @@ -1017,7 +1017,7 @@ class WS2812FX { // 96 bytes panel.clear(); #endif customPalettes.clear(); - if (useLedsArray && Segment::_globalLeds) free(Segment::_globalLeds); + if (useLedsArray && Segment::_globalLeds) d_free(Segment::_globalLeds); } static WS2812FX* getInstance(void) { return instance; } diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index d12153d5ff..a350a651da 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -76,11 +76,12 @@ void WS2812FX::setUpMatrix() { // don't use new / delete if ((size > 0) && (customMappingTable != nullptr)) { // resize - customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize + //customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize + customMappingTable = (uint16_t*) d_realloc_malloc(customMappingTable, sizeof(uint16_t) * size); // will free memory if it cannot resize } if ((size > 0) && (customMappingTable == nullptr)) { // second try DEBUG_PRINTLN("setUpMatrix: trying to get fresh memory block."); - customMappingTable = (uint16_t*) calloc(size, sizeof(uint16_t)); + customMappingTable = (uint16_t*) d_calloc(size, sizeof(uint16_t)); if (customMappingTable == nullptr) { USER_PRINTLN("setUpMatrix: alloc failed"); errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag @@ -122,7 +123,7 @@ void WS2812FX::setUpMatrix() { JsonArray map = doc.as(); gapSize = map.size(); if (!map.isNull() && (gapSize > 0) && gapSize >= customMappingSize) { // not an empty map //softhack also check gapSize>0 - gapTable = new(std::nothrow) int8_t[gapSize]; + gapTable = static_cast(p_malloc(gapSize)); if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } @@ -152,7 +153,7 @@ void WS2812FX::setUpMatrix() { } // delete gap array as we no longer need it - if (gapTable) {delete[] gapTable; gapTable=nullptr;} // softhack prevent dangling pointer + if (gapTable) {p_free(gapTable); gapTable=nullptr;} // softhack prevent dangling pointer #ifdef WLED_DEBUG_MAPS DEBUG_PRINTF("Matrix ledmap: \n"); @@ -185,7 +186,7 @@ void WS2812FX::setUpMatrix() { if (customMappingTable[i] != (uint16_t)i ) isIdentity = false; } if (isIdentity) { - free(customMappingTable); customMappingTable = nullptr; + d_free(customMappingTable); customMappingTable = nullptr; USER_PRINTF("!setupmatrix: customMappingTable is not needed. Dropping %d bytes.\n", customMappingTableSize * sizeof(uint16_t)); customMappingTableSize = 0; customMappingSize = 0; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 996bb57b60..e92a679a52 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -111,7 +111,7 @@ void Segment::allocLeds() { DEBUG_PRINTF("allocLeds warning: size == %u !!\n", size); if (ledsrgb && (ledsrgbSize == 0)) { USER_PRINTLN("allocLeds warning: ledsrgbSize == 0 but ledsrgb!=NULL"); - free(ledsrgb); ledsrgb=nullptr; + d_free(ledsrgb); ledsrgb=nullptr; } // softhack007 clean up buffer } if ((size > 0) && (!ledsrgb || size > ledsrgbSize)) { //softhack dont allocate zero bytes @@ -124,8 +124,8 @@ void Segment::allocLeds() { ledsrgb = nullptr; portEXIT_CRITICAL(&ledsrgb_mux); - if (oldLedsRgb) free(oldLedsRgb); // we need a bigger buffer, so free the old one first - CRGB* newLedsRgb = (CRGB*)calloc(size, 1); // WLEDMM This is an OS call, so we should not wrap it in portEnterCRITICAL + if (oldLedsRgb) d_free(oldLedsRgb); // we need a bigger buffer, so free the old one first + CRGB* newLedsRgb = (CRGB*)d_calloc(size, 1); // WLEDMM This is an OS call, so we should not wrap it in portEnterCRITICAL portENTER_CRITICAL(&ledsrgb_mux); ledsrgb = newLedsRgb; @@ -175,7 +175,7 @@ Segment& Segment::operator= (const Segment &orig) { if (_t) delete _t; CRGB* oldLeds = ledsrgb; size_t oldLedsSize = ledsrgbSize; - if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); + if (ledsrgb && !Segment::_globalLeds) d_free(ledsrgb); deallocateData(); // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); @@ -212,7 +212,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept { if (name) { delete[] name; name = nullptr; } // free old name deallocateData(); // free old runtime data if (_t) { delete _t; _t = nullptr; } - if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); //WLEDMM: not needed anymore as we will use leds from copy. no need to nullify ledsrgb as it gets new value in memcpy + if (ledsrgb && !Segment::_globalLeds) d_free(ledsrgb); //WLEDMM: not needed anymore as we will use leds from copy. no need to nullify ledsrgb as it gets new value in memcpy // WLEDMM temporarily prevent any fast draw calls to old and new segment orig._isSimpleSegment = false; @@ -265,7 +265,7 @@ bool Segment::allocateData(size_t len, bool allowOverdraft) { // WLEDMM allowOv // data = (byte*) ps_malloc(len); //else //#endif - data = (byte*) malloc(len); + data = (byte*) d_malloc(len); if (!data) { _dataLen = 0; // WLEDMM reset dataLen if ((errorFlag != ERR_LOW_MEM) && (errorFlag != ERR_LOW_SEG_MEM)) { // spam filter @@ -292,7 +292,7 @@ void Segment::deallocateData() { _dataLen = 0; return; } // WLEDMM reset dataLen - free(data); + d_free(data); data = nullptr; DEBUG_PRINTF("Segment::deallocateData: free'd %d bytes.\n", _dataLen); Segment::addUsedSegmentData(-_dataLen); @@ -308,7 +308,7 @@ void Segment::deallocateData() { */ void Segment::resetIfRequired() { if (reset) { - if (ledsrgb && !Segment::_globalLeds) { free(ledsrgb); ledsrgb = nullptr; ledsrgbSize=0;} // WLEDMM segment has changed, so we need a fresh buffer. + if (ledsrgb && !Segment::_globalLeds) { d_free(ledsrgb); ledsrgb = nullptr; ledsrgbSize=0;} // WLEDMM segment has changed, so we need a fresh buffer. if (transitional && _t) { transitional = false; delete _t; _t = nullptr; } deallocateData(); next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; @@ -1866,7 +1866,7 @@ void WS2812FX::finalizeInit(void) portENTER_CRITICAL(&ledsrgb_mux); Segment::_globalLeds = nullptr; portEXIT_CRITICAL(&ledsrgb_mux); - free(oldGLeds); + d_free(oldGLeds); purgeSegments(true); // WLEDMM moved here, because it seems to improve stability. } if (useLedsArray && getLengthTotal()>0) { // WLEDMM avoid malloc(0) @@ -1877,7 +1877,7 @@ void WS2812FX::finalizeInit(void) // Segment::_globalLeds = (CRGB*) ps_malloc(arrSize); //else //#endif - if (arrSize > 0) Segment::_globalLeds = (CRGB*) malloc(arrSize); // WLEDMM avoid malloc(0) + if (arrSize > 0) Segment::_globalLeds = (CRGB*) d_malloc(arrSize); // WLEDMM avoid malloc(0) if ((Segment::_globalLeds != nullptr) && (arrSize > 0)) memset(Segment::_globalLeds, 0, arrSize); // WLEDMM avoid dereferencing nullptr if ((Segment::_globalLeds == nullptr) && (arrSize > 0)) errorFlag = ERR_NORAM_PX; // WLEDMM raise errorflag } @@ -2703,11 +2703,12 @@ bool WS2812FX::deserializeMap(uint8_t n) { // don't use new / delete if ((size > 0) && (customMappingTable != nullptr)) { - customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize + //customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize + customMappingTable = (uint16_t*) d_realloc_malloc(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize } if ((size > 0) && (customMappingTable == nullptr)) { // second try DEBUG_PRINTLN("deserializeMap: trying to get fresh memory block."); - customMappingTable = (uint16_t*) calloc(size, sizeof(uint16_t)); + customMappingTable = (uint16_t*) d_calloc(size, sizeof(uint16_t)); if (customMappingTable == nullptr) { DEBUG_PRINTLN("deserializeMap: alloc failed!"); errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 753310a6e6..3b7420b5ad 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -59,27 +59,18 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte //util.cpp // memory allocation wrappers +// forward declaration: memory functions (util.cpp) extern "C" { - // prefer DRAM over PSRAM (if available) in d_ alloc functions + // prefer DRAM in d_xalloc functions, PSRAM as fallback void *d_malloc(size_t); void *d_calloc(size_t, size_t); void *d_realloc_malloc(void *ptr, size_t size); - #ifndef ESP8266 - inline void d_free(void *ptr) { heap_caps_free(ptr); } - #else - inline void d_free(void *ptr) { free(ptr); } - #endif - #if defined(BOARD_HAS_PSRAM) - // prefer PSRAM over DRAM in p_ alloc functions + void d_free(void *ptr); + // prefer PSRAM in p_xalloc functions, DRAM as fallback void *p_malloc(size_t); void *p_calloc(size_t, size_t); void *p_realloc_malloc(void *ptr, size_t size); - inline void p_free(void *ptr) { heap_caps_free(ptr); } - #else - #define p_malloc d_malloc - #define p_calloc d_calloc - #define p_free d_free - #endif + void p_free(void *ptr); } #ifdef WLED_DEBUG diff --git a/wled00/file.cpp b/wled00/file.cpp index 88e2d9389a..dfac394026 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -460,7 +460,7 @@ static const uint8_t *getPresetCache(size_t &size) { if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { - free(presetsCached); + p_free(presetsCached); presetsCached = nullptr; } } @@ -475,7 +475,7 @@ static const uint8_t *getPresetCache(size_t &size) { presetsCachedTime = presetsModifiedTime; presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; - presetsCached = (uint8_t*)ps_malloc(file.size() + 1); + presetsCached = (uint8_t*)p_malloc(file.size() + 1); if (presetsCached) { presetsCachedSize = file.size(); file.read(presetsCached, presetsCachedSize); @@ -506,14 +506,16 @@ void invalidateFileNameCache() { // reset "file not found" cache haveICOFile = true; haveCpalFile = true; - #if defined(BOARD_HAS_PSRAM) && (defined(WLED_USE_PSRAM) || defined(WLED_USE_PSRAM_JSON)) + #if (defined(BOARD_HAS_PSRAM) || ESP_IDF_VERSION_MAJOR > 3) && (defined(WLED_USE_PSRAM) || defined(WLED_USE_PSRAM_JSON)) // WLEDMM hack to clear presets.json cache - size_t dummy; - unsigned long realpresetsTime = presetsModifiedTime; - presetsModifiedTime = toki.second(); // pretend we have changes - (void) getPresetCache(dummy); // clear presets.json cache - presetsModifiedTime = realpresetsTime; // restore correct value -#endif + if (psramFound()) { + size_t dummy; + unsigned long realpresetsTime = presetsModifiedTime; + presetsModifiedTime = toki.second(); // pretend we have changes + (void) getPresetCache(dummy); // clear presets.json cache + presetsModifiedTime = realpresetsTime; // restore correct value + } + #endif //USER_PRINTLN("WS FileRead cache cleared"); } diff --git a/wled00/presets.cpp b/wled00/presets.cpp index e675b63b5f..1adc960342 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -77,16 +77,11 @@ static void doSaveState() { */ #if defined(ARDUINO_ARCH_ESP32) if (!persist) { - if (tmpRAMbuffer!=nullptr) free(tmpRAMbuffer); + if (tmpRAMbuffer!=nullptr) p_free(tmpRAMbuffer); size_t len = measureJson(*fileDoc) + 1; DEBUG_PRINTLN(len); // if possible use SPI RAM on ESP32 - #if defined(BOARD_HAS_PSRAM) && (defined(WLED_USE_PSRAM) || defined(WLED_USE_PSRAM_JSON)) // WLEDMM - if (psramFound()) - tmpRAMbuffer = (char*) ps_malloc(len); - else - #endif - tmpRAMbuffer = (char*) malloc(len); + tmpRAMbuffer = (char*) p_malloc(len); if (tmpRAMbuffer!=nullptr) { serializeJson(*fileDoc, tmpRAMbuffer, len); } else { From e714ede76e4f7cc847328285f837768e84f6a1c8 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 15 Feb 2026 23:42:56 +0100 Subject: [PATCH 06/25] p -> d in AsyncJSON --- wled00/src/dependencies/json/AsyncJson-v6.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/src/dependencies/json/AsyncJson-v6.h b/wled00/src/dependencies/json/AsyncJson-v6.h index 20fede3ae1..ca5086f7f8 100644 --- a/wled00/src/dependencies/json/AsyncJson-v6.h +++ b/wled00/src/dependencies/json/AsyncJson-v6.h @@ -168,7 +168,7 @@ class AsyncCallbackJsonWebHandler: public AsyncWebHandler { if (_onRequest) { _contentLength = total; if (total > 0 && request->_tempObject == NULL && (int)total < _maxContentLength) { - request->_tempObject = p_malloc(total); + request->_tempObject = d_malloc(total); } if (request->_tempObject != NULL) { memcpy((uint8_t*)(request->_tempObject) + index, data, len); From 98c97885a670a9d69ba1667db06a7ae51651b017 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Sun, 15 Feb 2026 23:55:54 +0100 Subject: [PATCH 07/25] fallback if !psramFound() add fallback (d_malloc()) if psram not found (can happen before setup(), or when a BOARD_HAS_PSRAM build runs on boards without PSRAM) --- wled00/util.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index b01a4b575d..97ae3a8096 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -833,19 +833,21 @@ void p_free(void *ptr) { heap_caps_free(ptr); } #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 0) // V4 can auto-detect PSRAM // p_xalloc: prefer PSRAM, use DRAM as fallback void *p_malloc(size_t size) { + if (!psramFound()) return d_malloc(size); void *buffer = heap_caps_malloc_prefer(size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); return validateFreeHeap(buffer); } void *p_calloc(size_t count, size_t size) { // similar to p_malloc bus uses heap_caps_calloc + if (!psramFound()) return d_calloc(count, size); void *buffer = heap_caps_calloc_prefer(count, size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); return validateFreeHeap(buffer); - return buffer; } // realloc with malloc fallback, original buffer is freed if realloc fails but not copied! void *p_realloc_malloc(void *ptr, size_t size) { + if (!psramFound()) return d_realloc_malloc(ptr, size); void *buffer = heap_caps_realloc_prefer(ptr, size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); if (buffer) return buffer; // realloc successful p_free(ptr); // free old buffer if realloc failed From 31e56ae14d655df9d5b54ac224ceb609cceb546e Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:03:02 +0100 Subject: [PATCH 08/25] two bugfixes * wrong logic for RTC_RAM_THRESHOLD * match p_malloc with p_free --- wled00/presets.cpp | 2 +- wled00/util.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 1adc960342..c3fe1e5ccd 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -293,7 +293,7 @@ void handlePresets() #if defined(ARDUINO_ARCH_ESP32) //Aircoookie recommended not to delete buffer if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { - free(tmpRAMbuffer); + p_free(tmpRAMbuffer); tmpRAMbuffer = nullptr; } #endif diff --git a/wled00/util.cpp b/wled00/util.cpp index 97ae3a8096..1d74e9422f 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -767,7 +767,7 @@ static void *validateFreeHeap(void *buffer) { #ifdef BOARD_HAS_PSRAM #define RTC_RAM_THRESHOLD 1024 // use RTC RAM for allocations smaller than this size #else -#define RTC_RAM_THRESHOLD (psramFound() ? 65535 : 1024) // without PSRAM, allow any size into RTC RAM (useful especially on S2 without PSRAM) +#define RTC_RAM_THRESHOLD (psramFound() ? 1024 : 65535) // without PSRAM, allow any size into RTC RAM (useful especially on S2 without PSRAM) #endif void *d_malloc(size_t size) { From 4c9314638f5cd15fd75a7d7e34c2515234f84e91 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:08:30 +0100 Subject: [PATCH 09/25] correct alloc size in d_calloc --- wled00/util.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index 1d74e9422f..cacb2873e0 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -794,10 +794,10 @@ void *d_malloc(size_t size) { } void *d_calloc(size_t count, size_t size) { - // similar to d_malloc bus uses heap_caps_calloc + // similar to d_malloc but uses heap_caps_calloc void *buffer = nullptr; #if !defined(CONFIG_IDF_TARGET_ESP32) - if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) + if ((size * count) <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + (size * count)) buffer = heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); if (buffer == nullptr) // no RTC RAM allocation: use DRAM buffer = heap_caps_calloc(count, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory @@ -839,7 +839,7 @@ void *p_malloc(size_t size) { } void *p_calloc(size_t count, size_t size) { - // similar to p_malloc bus uses heap_caps_calloc + // similar to p_malloc but uses heap_caps_calloc if (!psramFound()) return d_calloc(count, size); void *buffer = heap_caps_calloc_prefer(count, size, 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); return validateFreeHeap(buffer); From 735047fd5fde70e4017a711aa466e3ffd63fcbcb Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:45:48 +0100 Subject: [PATCH 10/25] bugfix: allow user override for MIN_HEAP_SIZE --- wled00/FX_fcn.cpp | 2 +- wled00/const.h | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index e92a679a52..ba80d0f374 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -2704,7 +2704,7 @@ bool WS2812FX::deserializeMap(uint8_t n) { // don't use new / delete if ((size > 0) && (customMappingTable != nullptr)) { //customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize - customMappingTable = (uint16_t*) d_realloc_malloc(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize + customMappingTable = (uint16_t*) d_realloc_malloc(customMappingTable, sizeof(uint16_t) * size); // will free memory if it cannot resize } if ((size > 0) && (customMappingTable == nullptr)) { // second try DEBUG_PRINTLN("deserializeMap: trying to get fresh memory block."); diff --git a/wled00/const.h b/wled00/const.h index 6ed488e992..1953d5ee36 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -529,11 +529,13 @@ #endif // minimum heap size required to process web requests: try to keep free heap above this value +#if !defined(MIN_HEAP_SIZE) #ifdef ESP8266 #define MIN_HEAP_SIZE (9*1024) #else #define MIN_HEAP_SIZE (15*1024) // WLED allocation functions (util.cpp) try to keep this much contiguous heap free for other tasks #endif +#endif // threshold for PSRAM use: if heap is running low, requests to allocate_buffer(prefer DRAM) above PSRAM_THRESHOLD may be put in PSRAM // if heap is depleted, PSRAM will be used regardless of threshold #if defined(CONFIG_IDF_TARGET_ESP32S3) @@ -545,9 +547,9 @@ #endif // Web server limits (8k for AsyncWebServer) -#if !defined(MIN_HEAP_SIZE) -#define MIN_HEAP_SIZE 8192 -#endif +//#if !defined(MIN_HEAP_SIZE) +//#define MIN_HEAP_SIZE 8192 +//#endif #ifdef ESP8266 // Minimum heap to consider handling a request From 78a964c38e73bfa6d84800cdfbd41c5ab22f3330 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:46:26 +0100 Subject: [PATCH 11/25] add RTCRAM statistics to WLED_DEBUG output --- wled00/wled.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 83eaee95c1..d46adbb5eb 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -366,6 +366,17 @@ void WLED::loop() DEBUG_PRINTF("%s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM #endif #if defined(ARDUINO_ARCH_ESP32) + #if defined(CONFIG_IDF_TARGET_ESP32) + // 32-bit DRAM (not byte accessible, only available on ESP32) + size_t dram32_free = heap_caps_get_free_size(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL) - dram_free; // returns all 32bit DRAM, subtract 8bit DRAM + //size_t dram32_largest = heap_caps_get_largest_free_block(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL); // returns largest DRAM block -> not useful + DEBUG_PRINTF_P(PSTR("DRAM 32-bit: Free: %7u bytes | Largest block: N/A\n"), dram32_free); + #else + // Fast RTC Memory (not available on ESP32) + size_t rtcram_free = heap_caps_get_free_size(MALLOC_CAP_RTCRAM); + size_t rtcram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_RTCRAM); + DEBUG_PRINTF_P(PSTR("RTC RAM: Free: %7u bytes | Largest block: %7u bytes\n"), rtcram_free, rtcram_largest); + #endif #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // V4 can auto-detect PSRAM if (psramFound()) { //DEBUG_PRINT(F("Total PSRAM: ")); DEBUG_PRINT(ESP.getPsramSize()/1024); DEBUG_PRINTLN("kB"); From 1eaad1bcd3e9d32f33cd550a096ccc7030779d32 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 02:35:04 +0100 Subject: [PATCH 12/25] work in progress * bugfix for p_malloc compilation guard * enable validateFreeHeap * comments --- wled00/fcn_declare.h | 6 ++++-- wled00/util.cpp | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index d54c578690..c2b858f805 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -505,12 +505,13 @@ extern "C" { void p_free(void *ptr); } #ifndef ESP8266 -inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) -inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block +inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) // WLEDMM can cause LED glitches +inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block // WLEDMM may glitch, too #else inline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free heap inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block #endif +#if 0 // WLEDMM not used yet #define BFRALLOC_NOBYTEACCESS (1 << 0) // ESP32 has 32bit accessible DRAM (usually ~50kB free) that must not be byte-accessed #define BFRALLOC_PREFER_DRAM (1 << 1) // prefer DRAM over PSRAM #define BFRALLOC_ENFORCE_DRAM (1 << 2) // use DRAM only, no PSRAM @@ -518,6 +519,7 @@ inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // r #define BFRALLOC_ENFORCE_PSRAM (1 << 4) // use PSRAM if available, otherwise uses DRAM #define BFRALLOC_CLEAR (1 << 5) // clear allocated buffer after allocation void *allocate_buffer(size_t size, uint32_t type); +#endif // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard diff --git a/wled00/util.cpp b/wled00/util.cpp index cacb2873e0..e9eb686785 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -755,7 +755,7 @@ void p_free(void *ptr) { free(ptr); } static void *validateFreeHeap(void *buffer) { // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not // TODO: between allocate and free, heap can run low (async web access), only IDF V5 allows for a pre-allocation-check of all free blocks -#if 0 // WLEDMM disabled -> TODO need to think about this +#if 1 // WLEDMM TODO need to think about this if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH && getContiguousFreeHeap() < MIN_HEAP_SIZE) { free(buffer); return nullptr; @@ -830,7 +830,7 @@ void *d_realloc_malloc(void *ptr, size_t size) { void d_free(void *ptr) { heap_caps_free(ptr); } void p_free(void *ptr) { heap_caps_free(ptr); } -#if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 0) // V4 can auto-detect PSRAM +#if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // V4 can auto-detect PSRAM // p_xalloc: prefer PSRAM, use DRAM as fallback void *p_malloc(size_t size) { if (!psramFound()) return d_malloc(size); From 60f8648046c6d5c3aa92e4ea2eaf432823318095 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:59:25 +0100 Subject: [PATCH 13/25] (experimental) glitch-free heap size measurement only measures heap size when !strip.isUpdating(); otherwise returns last measured value. -> A bit inexact, but still better than regular flickering. --- wled00/bus_manager.cpp | 16 ++++++++-------- wled00/fcn_declare.h | 10 ++++++++-- wled00/json.cpp | 8 ++++++-- wled00/util.cpp | 24 ++++++++++++++++++++++++ wled00/wled.cpp | 34 +++++++++++++++++++++------------- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 3b7420b5ad..19d432b629 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -684,7 +684,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh MatrixPanel_I2S_DMA* display = nullptr; VirtualMatrixPanel* fourScanPanel = nullptr; HUB75_I2S_CFG mxconfig; - size_t lastHeap = ESP.getFreeHeap(); + size_t lastHeap = getFreeHeapSize(); _valid = false; _len = 0; @@ -951,7 +951,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh USER_PRINTF("\tLAT = %2d, OE = %2d, CLK = %2d\n\n", mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk); USER_FLUSH(); - DEBUG_PRINT(F("Free heap: ")); DEBUG_PRINTLN(ESP.getFreeHeap()); lastHeap = ESP.getFreeHeap(); + DEBUG_PRINT(F("Free heap: ")); DEBUG_PRINTLN(getFreeHeapSize()); lastHeap = getFreeHeapSize(); // check if we can re-use the existing display driver if (activeDisplay) { @@ -996,7 +996,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh USER_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********"); activeDisplay = nullptr; activeFourScanPanel = nullptr; - USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - getFreeHeapSize())); return; } @@ -1027,11 +1027,11 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh _bri = (last_bri > 0) ? last_bri : 25; // try to restore persistent brightness value delay(24); // experimental - DEBUG_PRINT(F("heap usage: ")); DEBUG_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + DEBUG_PRINT(F("heap usage: ")); DEBUG_PRINTLN(int(lastHeap - getFreeHeapSize())); // Allocate memory and start DMA display if (newDisplay && (display->begin() == false)) { USER_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********"); - USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - getFreeHeapSize())); _valid = false; return; } @@ -1039,7 +1039,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh if (newDisplay) { USER_PRINTLN("MatrixPanel_I2S_DMA begin, started ok"); } else { USER_PRINTLN("MatrixPanel_I2S_DMA begin, using existing display."); } - USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - getFreeHeapSize())); delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle _valid = true; display->setBrightness8(_bri); // range is 0-255, 0 - 0%, 255 - 100% // [setBrightness()] Tried to set output brightness before begin() @@ -1069,7 +1069,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh USER_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for leds buffer!")); cleanup(); // free buffers, and deallocate pins _valid = false; - USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - getFreeHeapSize())); return; // fail } @@ -1130,7 +1130,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh #endif instanceCount++; - USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - ESP.getFreeHeap())); + USER_PRINT(F("heap usage: ")); USER_PRINTLN(int(lastHeap - getFreeHeapSize())); } void __attribute__((hot)) IRAM_ATTR BusHub75Matrix::setPixelColor(uint16_t pix, uint32_t c) { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index c2b858f805..4908d10a97 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -505,8 +505,14 @@ extern "C" { void p_free(void *ptr); } #ifndef ESP8266 -inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) // WLEDMM can cause LED glitches -inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block // WLEDMM may glitch, too +//inline size_t getFreeHeapSize() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns free heap (ESP.getFreeHeap() can include other memory types) // WLEDMM can cause LED glitches +//inline size_t getContiguousFreeHeap() { return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); } // returns largest contiguous free block // WLEDMM may glitch, too + +extern size_t d_measureFreeHeap(void); +extern size_t d_measureContiguousFreeHeap(void); +inline size_t getFreeHeapSize() { return d_measureFreeHeap();} // total free heap - with flicker protection +inline size_t getContiguousFreeHeap() { return d_measureContiguousFreeHeap();} // largest free block - with flicker protection + #else inline size_t getFreeHeapSize() { return ESP.getFreeHeap(); } // returns free heap inline size_t getContiguousFreeHeap() { return ESP.getMaxFreeBlockSize(); } // returns largest contiguous free block diff --git a/wled00/json.cpp b/wled00/json.cpp index 1b8b0fea3f..087cc51d31 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1128,11 +1128,15 @@ void serializeInfo(JsonObject root) #endif root[F("getflash")] = ESP.getFlashChipSize(); //WLEDMM and Athom, works for both ESP32 and ESP8266 - root[F("freeheap")] = ESP.getFreeHeap(); + //root[F("freeheap")] = ESP.getFreeHeap(); + root[F("freeheap")] = getFreeHeapSize(); //WLEDMM: conditional on esp32 #if defined(ARDUINO_ARCH_ESP32) root[F("freestack")] = uxTaskGetStackHighWaterMark(NULL); //WLEDMM - root[F("minfreeheap")] = ESP.getMinFreeHeap(); + //root[F("minfreeheap")] = ESP.getMinFreeHeap(); + auto maxFreeBlock = getContiguousFreeHeap(); + root[F("minfreeheap")] = maxFreeBlock; + root[F("maxalloc")] = maxFreeBlock; // for upstream WLED compatibility #endif #if defined(ARDUINO_ARCH_ESP32) #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // V4 can auto-detect PSRAM diff --git a/wled00/util.cpp b/wled00/util.cpp index e9eb686785..aa2ce8019d 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -752,6 +752,30 @@ void *p_realloc_malloc(void *ptr, size_t size) { return d_realloc_malloc(ptr, si void p_free(void *ptr) { free(ptr); } #else + +static size_t lastHeap = 65535; +static size_t lastMinHeap = 65535; +inline static void d_measureHeap(void) { +#ifdef WLEDMM_FILEWAIT // only when we don't use the RMTHI driver + if (!strip.isUpdating()) // skip measurement while sending out LEDs - prevents flickering +#endif + { + lastHeap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + lastMinHeap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + } +} + +size_t d_measureContiguousFreeHeap(void) { + d_measureHeap(); + return lastMinHeap; +} // returns largest contiguous free block // WLEDMM may glitch, too + +size_t d_measureFreeHeap(void) { + d_measureHeap(); + return lastHeap; +} // returns free heap (ESP.getFreeHeap() can include other memory types) // WLEDMM can cause LED glitches + + static void *validateFreeHeap(void *buffer) { // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not // TODO: between allocate and free, heap can run low (async web access), only IDF V5 allows for a pre-allocation-check of all free blocks diff --git a/wled00/wled.cpp b/wled00/wled.cpp index d46adbb5eb..2eb404d3c1 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -358,8 +358,7 @@ void WLED::loop() DEBUG_PRINT(F("Name: ")); DEBUG_PRINTLN(serverDescription); DEBUG_PRINT(F("Runtime: ")); DEBUG_PRINTLN(millis()); DEBUG_PRINT(F("Unix time: ")); toki.printTime(toki.getTime()); - DEBUG_PRINT(F("Free heap : ")); DEBUG_PRINTLN(ESP.getFreeHeap()); - DEBUG_PRINT(F("Free heap: ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINT(F("Free heap: ")); DEBUG_PRINTLN(getFreeHeapSize()); //WLEDMM #ifdef ARDUINO_ARCH_ESP32 DEBUG_PRINT(F("Avail heap: ")); DEBUG_PRINTLN(ESP.getMaxAllocHeap()); @@ -660,7 +659,7 @@ void WLED::setup() DEBUG_PRINT(F("esp8266 ")); DEBUG_PRINTLN(ESP.getCoreVersion()); #endif - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(getFreeHeapSize()); #ifdef ARDUINO_ARCH_ESP32 #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) // unfortunately not available in older framework versions DEBUG_PRINT(F("\nArduino max stack ")); DEBUG_PRINTLN(getArduinoLoopTaskStackSize()); @@ -763,7 +762,7 @@ void WLED::setup() DEBUG_PRINTLN(F("Registering usermods ...")); registerUsermods(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(getFreeHeapSize()); #ifdef ARDUINO_ARCH_ESP32 DEBUG_PRINTF("%s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM #endif @@ -813,12 +812,12 @@ void WLED::setup() DEBUG_PRINTLN(F("Initializing strip")); beginStrip(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(getFreeHeapSize()); USER_PRINTLN(F("\nUsermods setup ...")); userSetup(); usermods.setup(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(getFreeHeapSize()); if (strcmp(clientSSID, DEFAULT_CLIENT_SSID) == 0) showWelcomePage = true; @@ -882,7 +881,7 @@ void WLED::setup() // HTTP server page init DEBUG_PRINTLN(F("initServer")); initServer(); - DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(getFreeHeapSize()); #ifdef ARDUINO_ARCH_ESP32 DEBUG_PRINT(pcTaskGetTaskName(NULL)); DEBUG_PRINT(F(" free stack ")); DEBUG_PRINTLN(uxTaskGetStackHighWaterMark(NULL)); #endif @@ -967,7 +966,7 @@ void WLED::setup() USER_PRINTLN(F("\n")); #endif - USER_PRINT(F("Free heap ")); USER_PRINTLN(ESP.getFreeHeap());USER_PRINTLN(); + USER_PRINT(F("Free heap ")); USER_PRINTLN(getFreeHeapSize());USER_PRINTLN(); // WLEDMM force initial calculation of gamma correction LUT if ((gammaCorrectVal < 0.999f) || (gammaCorrectVal > 3.0f)) calcGammaTable(1.0f); // no gamma => create linear LUT @@ -1346,13 +1345,22 @@ void WLED::handleConnection() } static unsigned retryCount = 0; // WLEDMM - #ifdef ARDUINO_ARCH_ESP32 + #ifdef ARDUINO_ARCH_ESP32 // reconnect WiFi to clear stale allocations if heap gets too low - if ((!strip.isUpdating()) && (now - heapTime > 5000)) { // WLEDMM: updated with better logic for small heap available by block, not total. // WLEDMM trying to use a moment when the strip is idle -#if defined(ARDUINO_ARCH_ESP32S2) || defined(WLED_ENABLE_HUB75MATRIX) - uint32_t heap = ESP.getFreeHeap(); // WLEDMM works better on -S2 + if ((now - heapTime > 5000) && !strip.isUpdating()) { // WLEDMM: updated with better logic for small heap available by block, not total. // WLEDMM trying to use a moment when the strip is idle + #ifdef WLEDMM_FILEWAIT // only when we don't use the RMTHI driver + // calling getContiguousFreeHeap() during led update causes glitches on C3 + // this can (probably) be removed once RMT driver for C3 is fixed + unsigned t0 = millis(); + while (strip.isUpdating() && (millis() - t0 < 15)) delay(1); // be nice, but not too nice. Waits up to 15ms + #endif + +#if defined(ARDUINO_ARCH_ESP32S2) /*|| defined(WLED_ENABLE_HUB75MATRIX)*/ + //uint32_t heap = ESP.getFreeHeap(); // WLEDMM works better on -S2 + uint32_t heap = getFreeHeapSize(); // WLEDMM works better on -S2 #else - uint32_t heap = heap_caps_get_largest_free_block(0x1800); // WLEDMM: This is a better metric for free heap. + //uint32_t heap = heap_caps_get_largest_free_block(0x1800); // WLEDMM: This is a better metric for free heap. + uint32_t heap = getContiguousFreeHeap(); // WLEDMM: This is a better metric for free heap. #endif if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) { if (retryCount < 5) { // WLEDMM avoid repeated disconnects From 67c70a2310af491da363413925ca1edf543e8071 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:07:26 +0100 Subject: [PATCH 14/25] add missing DRAM debug code for classic esp32 --- wled00/wled.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 2eb404d3c1..47d827dcb8 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -365,6 +365,10 @@ void WLED::loop() DEBUG_PRINTF("%s min free stack %d\n", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL)); //WLEDMM #endif #if defined(ARDUINO_ARCH_ESP32) + // Internal DRAM (standard 8-bit accessible heap) + size_t dram_free = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL); + size_t dram_largest = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL); + DEBUG_PRINTF_P(PSTR("DRAM 8-bit: Free: %7u bytes | Largest block: %7u bytes\n"), dram_free, dram_largest); #if defined(CONFIG_IDF_TARGET_ESP32) // 32-bit DRAM (not byte accessible, only available on ESP32) size_t dram32_free = heap_caps_get_free_size(MALLOC_CAP_32BIT | MALLOC_CAP_INTERNAL) - dram_free; // returns all 32bit DRAM, subtract 8bit DRAM From 68dbd5eb79564895f58a124a6bce76ea7a345f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20M=C3=B6hle?= <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:17:09 +0100 Subject: [PATCH 15/25] re-enable more relaxed heap checks in HUB75 builds --- wled00/wled.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 47d827dcb8..c3ee3e5c81 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -1359,8 +1359,8 @@ void WLED::handleConnection() while (strip.isUpdating() && (millis() - t0 < 15)) delay(1); // be nice, but not too nice. Waits up to 15ms #endif -#if defined(ARDUINO_ARCH_ESP32S2) /*|| defined(WLED_ENABLE_HUB75MATRIX)*/ - //uint32_t heap = ESP.getFreeHeap(); // WLEDMM works better on -S2 +#if defined(ARDUINO_ARCH_ESP32S2) || defined(WLED_ENABLE_HUB75MATRIX) + //uint32_t heap = ESP.getFreeHeap(); // WLEDMM works better on -S2; also avoid too-early panic on HUB75 builds uint32_t heap = getFreeHeapSize(); // WLEDMM works better on -S2 #else //uint32_t heap = heap_caps_get_largest_free_block(0x1800); // WLEDMM: This is a better metric for free heap. From 69946e463530a9ee3df1b20beea0cd33ffc7aad5 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:59:16 +0100 Subject: [PATCH 16/25] add early heap capacity checking before allocating isOkForDRAMHeap() rejects allocations when the remaining heap might go below MIN_FREE_HEAP. -> prevents fragmentation when a block is first allocated, but then undone in validateFreeHeap. --- wled00/util.cpp | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index aa2ce8019d..4a86f6ae68 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -775,16 +775,30 @@ size_t d_measureFreeHeap(void) { return lastHeap; } // returns free heap (ESP.getFreeHeap() can include other memory types) // WLEDMM can cause LED glitches +// early check: reject DRAM request if remaining heap possibly gets too low (avoids heap fragmentation) +static inline bool isOkForDRAMHeap(size_t amount) { +#if !defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) + if (!psramFound()) return true; // No PSRAM -> no opther options, so let's try + size_t avail = getContiguousFreeHeap(); + if ((amount < avail) && (avail - amount > MIN_HEAP_SIZE)) return true; + else { + DEBUG_PRINTF("!isOkForDRAMHeap() rejected allocation (%lu bytes, %lu available).\n", amount, avail); + return(false); + } + #else + return true; // No PSRAM -> no opther options + #endif +} static void *validateFreeHeap(void *buffer) { // make sure there is enough free heap left if buffer was allocated in DRAM region, free it if not // TODO: between allocate and free, heap can run low (async web access), only IDF V5 allows for a pre-allocation-check of all free blocks -#if 1 // WLEDMM TODO need to think about this + if (buffer == nullptr) return buffer; // early exit, nothing to check if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH && getContiguousFreeHeap() < MIN_HEAP_SIZE) { free(buffer); + DEBUG_PRINTLN("!validateFreeHeap() rejected allocation."); return nullptr; } -#endif return buffer; } @@ -800,9 +814,12 @@ void *d_malloc(size_t size) { // the newer ESP32 variants have byte-accessible fast RTC memory that can be used as heap, access speed is on-par with DRAM // the system does prefer normal DRAM until full, since free RTC memory is ~7.5k only, its below the minimum heap threshold and needs to be allocated explicitly // use RTC RAM for small allocations or if DRAM is running low to improve fragmentation - if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) - buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - if (buffer == nullptr) // no RTC RAM allocation: use DRAM + if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) { + //buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + buffer = heap_caps_malloc(size, MALLOC_CAP_RTCRAM | MALLOC_CAP_8BIT); + DEBUG_PRINTF("d_malloc() trying RTCRAM (%lu bytes) - %s.\n", size, buffer?"success":"fail"); + } + if ((buffer == nullptr) && isOkForDRAMHeap(size)) // no RTC RAM allocation: use DRAM buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory #else buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory @@ -810,9 +827,10 @@ void *d_malloc(size_t size) { buffer = validateFreeHeap(buffer); // make sure there is enough free heap left #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) - if (!buffer && psramFound()) + if (!buffer && psramFound()) { + DEBUG_PRINTF("d_malloc() using PSRAM(%lu bytes).\n", size); return heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available - else + } else #endif return buffer; } @@ -821,9 +839,12 @@ void *d_calloc(size_t count, size_t size) { // similar to d_malloc but uses heap_caps_calloc void *buffer = nullptr; #if !defined(CONFIG_IDF_TARGET_ESP32) - if ((size * count) <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + (size * count)) - buffer = heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - if (buffer == nullptr) // no RTC RAM allocation: use DRAM + if ((size * count) <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + (size * count)) { + //buffer = heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + buffer = heap_caps_calloc(count, size, MALLOC_CAP_RTCRAM | MALLOC_CAP_8BIT); + DEBUG_PRINTF("d_calloc() trying RTCRAM (%lu bytes) - %s.\n", size*count, buffer?"success":"fail"); + } + if ((buffer == nullptr) && isOkForDRAMHeap(size*count)) // no RTC RAM allocation: use DRAM buffer = heap_caps_calloc(count, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory #else buffer = heap_caps_calloc(count, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory @@ -831,9 +852,10 @@ void *d_calloc(size_t count, size_t size) { buffer = validateFreeHeap(buffer); // make sure there is enough free heap left #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) - if (!buffer && psramFound()) + if (!buffer && psramFound()) { + DEBUG_PRINTF("d_calloc() using PSRAM (%lu bytes).\n", size*count); return heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available - else + } else #endif return buffer; } From 381ad569bedc282c2529f9bd2ba9ee8006a3dbc1 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:21:56 +0100 Subject: [PATCH 17/25] revert unintended semantic change for "minfreeheap" --- wled00/json.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 087cc51d31..79a590ca50 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1133,9 +1133,8 @@ void serializeInfo(JsonObject root) //WLEDMM: conditional on esp32 #if defined(ARDUINO_ARCH_ESP32) root[F("freestack")] = uxTaskGetStackHighWaterMark(NULL); //WLEDMM - //root[F("minfreeheap")] = ESP.getMinFreeHeap(); + root[F("minfreeheap")] = ESP.getMinFreeHeap(); auto maxFreeBlock = getContiguousFreeHeap(); - root[F("minfreeheap")] = maxFreeBlock; root[F("maxalloc")] = maxFreeBlock; // for upstream WLED compatibility #endif #if defined(ARDUINO_ARCH_ESP32) From 2d71ab31ae32bba3ffd9da11ea8500eb6ea17a86 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:36:17 +0100 Subject: [PATCH 18/25] USER_PRINT memory alloc failure messages Alloc errors are only reported for d_malloc() and d_calloc(). PSRAM alloc is assumed to always succeed as PSRAM is much bigger than DRAM. --- wled00/util.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index 4a86f6ae68..2ed3d5830b 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -782,7 +782,7 @@ static inline bool isOkForDRAMHeap(size_t amount) { size_t avail = getContiguousFreeHeap(); if ((amount < avail) && (avail - amount > MIN_HEAP_SIZE)) return true; else { - DEBUG_PRINTF("!isOkForDRAMHeap() rejected allocation (%lu bytes, %lu available).\n", amount, avail); + DEBUG_PRINTF("* isOkForDRAMHeap() rejected allocation (%lu bytes, %lu available) !\n", amount, avail); return(false); } #else @@ -796,7 +796,7 @@ static void *validateFreeHeap(void *buffer) { if (buffer == nullptr) return buffer; // early exit, nothing to check if ((uintptr_t)buffer > SOC_DRAM_LOW && (uintptr_t)buffer < SOC_DRAM_HIGH && getContiguousFreeHeap() < MIN_HEAP_SIZE) { free(buffer); - DEBUG_PRINTLN("!validateFreeHeap() rejected allocation."); + USER_PRINTLN("* validateFreeHeap() rejected allocation !"); return nullptr; } return buffer; @@ -817,7 +817,7 @@ void *d_malloc(size_t size) { if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) { //buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); buffer = heap_caps_malloc(size, MALLOC_CAP_RTCRAM | MALLOC_CAP_8BIT); - DEBUG_PRINTF("d_malloc() trying RTCRAM (%lu bytes) - %s.\n", size, buffer?"success":"fail"); + DEBUG_PRINTF("* d_malloc() trying RTCRAM (%lu bytes) - %s.\n", size, buffer?"success":"fail"); } if ((buffer == nullptr) && isOkForDRAMHeap(size)) // no RTC RAM allocation: use DRAM buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory @@ -828,10 +828,11 @@ void *d_malloc(size_t size) { buffer = validateFreeHeap(buffer); // make sure there is enough free heap left #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) if (!buffer && psramFound()) { - DEBUG_PRINTF("d_malloc() using PSRAM(%lu bytes).\n", size); + DEBUG_PRINTF("* d_malloc() using PSRAM(%lu bytes).\n", size); return heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available } else #endif + if (!buffer) { USER_PRINTF("* d_malloc() failed (%lu bytes) !\n", size); } return buffer; } @@ -842,7 +843,7 @@ void *d_calloc(size_t count, size_t size) { if ((size * count) <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + (size * count)) { //buffer = heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); buffer = heap_caps_calloc(count, size, MALLOC_CAP_RTCRAM | MALLOC_CAP_8BIT); - DEBUG_PRINTF("d_calloc() trying RTCRAM (%lu bytes) - %s.\n", size*count, buffer?"success":"fail"); + DEBUG_PRINTF("* d_calloc() trying RTCRAM (%lu bytes) - %s.\n", size*count, buffer?"success":"fail"); } if ((buffer == nullptr) && isOkForDRAMHeap(size*count)) // no RTC RAM allocation: use DRAM buffer = heap_caps_calloc(count, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory @@ -853,10 +854,11 @@ void *d_calloc(size_t count, size_t size) { buffer = validateFreeHeap(buffer); // make sure there is enough free heap left #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) if (!buffer && psramFound()) { - DEBUG_PRINTF("d_calloc() using PSRAM (%lu bytes).\n", size*count); + DEBUG_PRINTF("* d_calloc() using PSRAM (%lu bytes).\n", size*count); return heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available } else #endif + if (!buffer) { USER_PRINTF("* d_calloc() failed (%lu bytes) !\n", size*count); } return buffer; } From b62b40b782134b19cc01f96a0493a5afbc8ab4a4 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:37:48 +0100 Subject: [PATCH 19/25] AR: better handling of alloc failure --- usermods/audioreactive/audio_reactive.h | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index f06626c202..c410c2da3a 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -511,13 +511,20 @@ void FFTcode(void * parameter) static float* oldSamples = nullptr; // previous 50% of samples static bool haveOldSamples = false; // for sliding window FFT bool usingOldSamples = false; - if (!oldSamples) oldSamples = (float*) calloc(samplesFFT_2, sizeof(float)); // allocate on first run - if (!oldSamples) { disableSoundProcessing = true; return; } // no memory -> die + if (!oldSamples) oldSamples = (float*) d_calloc(samplesFFT_2, sizeof(float)); // allocate on first run + if (!oldSamples) { disableSoundProcessing = true; haveOldSamples = false; return; } // no memory -> die #endif bool success = true; if ((vReal == nullptr) || (vImag == nullptr)) success = alocateFFTBuffers(); // allocate sample buffers on first run - if (success == false) { disableSoundProcessing = true; return; } // no memory -> die + if (success == false) { + // no memory -> clean up heap, then suspend + disableSoundProcessing = true; + if (pinkFactors) d_free(pinkFactors); pinkFactors = nullptr; + if (vImag) d_free(vImag); vImag = nullptr; + if (vReal) d_free(vReal); vReal = nullptr; + return; + } // create FFT object - we have to do if after allocating buffers #if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19 @@ -527,7 +534,8 @@ void FFTcode(void * parameter) // recommended version optimized by @softhack007 (API version 1.9) #if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) static float* windowWeighingFactors = nullptr; - if (!windowWeighingFactors) windowWeighingFactors = (float*) calloc(samplesFFT, sizeof(float)); // cache for FFT windowing factors - use heap + if (!windowWeighingFactors) windowWeighingFactors = (float*) d_calloc(samplesFFT, sizeof(float)); // cache for FFT windowing factors - use heap + if (!windowWeighingFactors) { disableSoundProcessing = true; haveOldSamples = false; return; } // alloc failed #else static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors - use global RAM #endif From ffe718b00ed12895c9fc1a371879f3535bee6675 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:03:13 +0100 Subject: [PATCH 20/25] ARTIFX and rotary UM robustness improvements --- usermods/artifx/arti.h | 25 ++++++++++++++++--- .../usermod_v2_rotary_encoder_ui_ALT.h | 7 ++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/usermods/artifx/arti.h b/usermods/artifx/arti.h index 5065ec77b0..1e3cbd0391 100644 --- a/usermods/artifx/arti.h +++ b/usermods/artifx/arti.h @@ -2562,14 +2562,24 @@ class ARTI { //open programFile char * programText = nullptr; - uint16_t programFileSize; + size_t programFileSize; #if ARTI_PLATFORM == ARTI_ARDUINO programFileSize = programFile.size(); programText = (char *)d_malloc(programFileSize+1); + if (programText == nullptr) { + ERROR_ARTI("ARTI-FX: Failed to allocate memory for program file (%u bytes)\n", programFileSize+1); + programFile.close(); + return false; + } programFile.read((byte *)programText, programFileSize); programText[programFileSize] = '\0'; #else programText = (char *)malloc(programTextSize); + if (programText == nullptr) { + ERROR_ARTI("ARTI-FX: Failed to allocate memory for program file (%u bytes)\n", programTextSize); + programFile.close(); + return false; + } programFile.read(programText, programTextSize); DEBUG_ARTI("programFile size %lu bytes\n", programFile.gcount()); programText[programFile.gcount()] = '\0'; @@ -2607,7 +2617,13 @@ class ARTI { #endif if (stages < 1) { - if (nullptr != programText) d_free(programText); // softhack007 prevent memory leak + // softhack007 prevent memory leak + #if ARTI_PLATFORM == ARTI_ARDUINO + if (nullptr != programText) d_free(programText); + #else + if (nullptr != programText) free(programText); + #endif + programText = nullptr; close(); return true; } @@ -2666,8 +2682,11 @@ class ARTI { #endif } #if ARTI_PLATFORM == ARTI_ARDUINO //not on windows as cause crash??? - d_free(programText); + d_free(programText); + #else + free(programText); #endif + programText = nullptr; if (stages >= 3) { diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 218a668f38..7990215bba 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -352,7 +352,8 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() { } byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) { - byte *indexes = (byte *)d_calloc(numModes, sizeof(byte)); + byte* indexes = (byte *)p_calloc(numModes, sizeof(byte)); + if (!indexes) return nullptr; // avoid OOM crash for (byte i = 0; i < numModes; i++) { indexes[i] = i; } @@ -364,7 +365,9 @@ byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) { * They don't end in '\0', they end in '"'. */ const char **RotaryEncoderUIUsermod::re_findModeStrings(const char json[], int numModes) { - const char **modeStrings = (const char **)d_calloc(numModes, sizeof(const char *)); + const char** modeStrings = (const char **)p_calloc(numModes, sizeof(const char *)); + if (!modeStrings) return nullptr; // avoid OOM crash + uint8_t modeIndex = 0; bool insideQuotes = false; // advance past the mark for markLineNum that may exist. From 66847a3a8181ca8e9f694b44038000f82ff5eee7 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:10:22 +0100 Subject: [PATCH 21/25] printf format string correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit size_t is `unsigned` not ùnsigned long` => %u --- wled00/util.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index 2ed3d5830b..db2bf99b1e 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -782,7 +782,7 @@ static inline bool isOkForDRAMHeap(size_t amount) { size_t avail = getContiguousFreeHeap(); if ((amount < avail) && (avail - amount > MIN_HEAP_SIZE)) return true; else { - DEBUG_PRINTF("* isOkForDRAMHeap() rejected allocation (%lu bytes, %lu available) !\n", amount, avail); + DEBUG_PRINTF("* isOkForDRAMHeap() rejected allocation (%u bytes, %u available) !\n", amount, avail); return(false); } #else @@ -817,7 +817,7 @@ void *d_malloc(size_t size) { if (size <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + size) { //buffer = heap_caps_malloc_prefer(size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); buffer = heap_caps_malloc(size, MALLOC_CAP_RTCRAM | MALLOC_CAP_8BIT); - DEBUG_PRINTF("* d_malloc() trying RTCRAM (%lu bytes) - %s.\n", size, buffer?"success":"fail"); + DEBUG_PRINTF("* d_malloc() trying RTCRAM (%u bytes) - %s.\n", size, buffer?"success":"fail"); } if ((buffer == nullptr) && isOkForDRAMHeap(size)) // no RTC RAM allocation: use DRAM buffer = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory @@ -828,11 +828,11 @@ void *d_malloc(size_t size) { buffer = validateFreeHeap(buffer); // make sure there is enough free heap left #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) if (!buffer && psramFound()) { - DEBUG_PRINTF("* d_malloc() using PSRAM(%lu bytes).\n", size); + DEBUG_PRINTF("* d_malloc() using PSRAM(%u bytes).\n", size); return heap_caps_malloc_prefer(size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available } else #endif - if (!buffer) { USER_PRINTF("* d_malloc() failed (%lu bytes) !\n", size); } + if (!buffer) { USER_PRINTF("* d_malloc() failed (%u bytes) !\n", size); } return buffer; } @@ -843,7 +843,7 @@ void *d_calloc(size_t count, size_t size) { if ((size * count) <= RTC_RAM_THRESHOLD || getContiguousFreeHeap() < 2*MIN_HEAP_SIZE + (size * count)) { //buffer = heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_RTCRAM, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); buffer = heap_caps_calloc(count, size, MALLOC_CAP_RTCRAM | MALLOC_CAP_8BIT); - DEBUG_PRINTF("* d_calloc() trying RTCRAM (%lu bytes) - %s.\n", size*count, buffer?"success":"fail"); + DEBUG_PRINTF("* d_calloc() trying RTCRAM (%u bytes) - %s.\n", size*count, buffer?"success":"fail"); } if ((buffer == nullptr) && isOkForDRAMHeap(size*count)) // no RTC RAM allocation: use DRAM buffer = heap_caps_calloc(count, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // allocate in any available heap memory @@ -854,11 +854,11 @@ void *d_calloc(size_t count, size_t size) { buffer = validateFreeHeap(buffer); // make sure there is enough free heap left #if defined(BOARD_HAS_PSRAM) || (ESP_IDF_VERSION_MAJOR > 3) // WLEDMM always try PSRAM (auto-detected) if (!buffer && psramFound()) { - DEBUG_PRINTF("* d_calloc() using PSRAM (%lu bytes).\n", size*count); + DEBUG_PRINTF("* d_calloc() using PSRAM (%u bytes).\n", size*count); return heap_caps_calloc_prefer(count, size, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_DEFAULT); // DRAM failed,try PSRAM if available } else #endif - if (!buffer) { USER_PRINTF("* d_calloc() failed (%lu bytes) !\n", size*count); } + if (!buffer) { USER_PRINTF("* d_calloc() failed (%u bytes) !\n", size*count); } return buffer; } From ccc96af9f9d55eac7c5fbf2d905a7d9b54d0f3bb Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:14:40 +0100 Subject: [PATCH 22/25] bugfix for C3 and S2 --- usermods/audioreactive/audio_reactive.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index c410c2da3a..270e9fa087 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -520,7 +520,9 @@ void FFTcode(void * parameter) if (success == false) { // no memory -> clean up heap, then suspend disableSoundProcessing = true; +#ifdef FFT_MAJORPEAK_HUMAN_EAR if (pinkFactors) d_free(pinkFactors); pinkFactors = nullptr; +#endif if (vImag) d_free(vImag); vImag = nullptr; if (vReal) d_free(vReal); vReal = nullptr; return; From 26a4abe493db0dbfcf9d5cee0c70c34d8896a564 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:47:30 +0100 Subject: [PATCH 23/25] AR: centralize memory cleanup --- usermods/audioreactive/audio_reactive.h | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 270e9fa087..7101008d03 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -472,6 +472,21 @@ static bool alocateFFTBuffers(void) { return(true); // success } +// de-allocate FFT sample buffers from heap +static void destroyFFTBuffers(void) { + #ifdef FFT_MAJORPEAK_HUMAN_EAR + if (pinkFactors) p_free(pinkFactors); pinkFactors = nullptr; +#endif + if (vImag) d_free(vImag); vImag = nullptr; + if (vReal) d_free(vReal); vReal = nullptr; + #ifdef SR_DEBUG + USER_PRINTLN("\ndesroyFFTBuffers() completed successfully."); + USER_PRINT(F("Free heap: ")); USER_PRINTLN(ESP.getFreeHeap()); + USER_FLUSH(); + #endif +} + + // High-Pass "DC blocker" filter // see https://www.dsprelated.com/freebooks/filters/DC_Blocker.html static void runDCBlocker(uint_fast16_t numSamples, float *sampleBuffer) { @@ -512,7 +527,7 @@ void FFTcode(void * parameter) static bool haveOldSamples = false; // for sliding window FFT bool usingOldSamples = false; if (!oldSamples) oldSamples = (float*) d_calloc(samplesFFT_2, sizeof(float)); // allocate on first run - if (!oldSamples) { disableSoundProcessing = true; haveOldSamples = false; return; } // no memory -> die + if (!oldSamples) { disableSoundProcessing = true; haveOldSamples = false; destroyFFTBuffers(); return; } // no memory -> die #endif bool success = true; @@ -520,11 +535,7 @@ void FFTcode(void * parameter) if (success == false) { // no memory -> clean up heap, then suspend disableSoundProcessing = true; -#ifdef FFT_MAJORPEAK_HUMAN_EAR - if (pinkFactors) d_free(pinkFactors); pinkFactors = nullptr; -#endif - if (vImag) d_free(vImag); vImag = nullptr; - if (vReal) d_free(vReal); vReal = nullptr; + destroyFFTBuffers(); return; } @@ -537,7 +548,7 @@ void FFTcode(void * parameter) #if defined(WLED_ENABLE_HUB75MATRIX) && defined(CONFIG_IDF_TARGET_ESP32) static float* windowWeighingFactors = nullptr; if (!windowWeighingFactors) windowWeighingFactors = (float*) d_calloc(samplesFFT, sizeof(float)); // cache for FFT windowing factors - use heap - if (!windowWeighingFactors) { disableSoundProcessing = true; haveOldSamples = false; return; } // alloc failed + if (!windowWeighingFactors) { disableSoundProcessing = true; haveOldSamples = false; destroyFFTBuffers(); return; } // alloc failed #else static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors - use global RAM #endif From 6f99aa03d37921877e0524fb86d0aca4dd6b96e2 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:50:44 +0100 Subject: [PATCH 24/25] typo --- usermods/audioreactive/audio_reactive.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 7101008d03..2b3a13482f 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -480,7 +480,7 @@ static void destroyFFTBuffers(void) { if (vImag) d_free(vImag); vImag = nullptr; if (vReal) d_free(vReal); vReal = nullptr; #ifdef SR_DEBUG - USER_PRINTLN("\ndesroyFFTBuffers() completed successfully."); + USER_PRINTLN("\ndestroyFFTBuffers() completed successfully."); USER_PRINT(F("Free heap: ")); USER_PRINTLN(ESP.getFreeHeap()); USER_FLUSH(); #endif From 75cbe6ced4b4c24f4b6a8ddcdf25f9c8521ca858 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:05:34 +0100 Subject: [PATCH 25/25] UI "free heap" was sometimes behind -> back to ESP.getFreeHeap() for UI consistency --- wled00/json.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 79a590ca50..f41c4e5c29 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1128,8 +1128,7 @@ void serializeInfo(JsonObject root) #endif root[F("getflash")] = ESP.getFlashChipSize(); //WLEDMM and Athom, works for both ESP32 and ESP8266 - //root[F("freeheap")] = ESP.getFreeHeap(); - root[F("freeheap")] = getFreeHeapSize(); + root[F("freeheap")] = ESP.getFreeHeap(); //WLEDMM: conditional on esp32 #if defined(ARDUINO_ARCH_ESP32) root[F("freestack")] = uxTaskGetStackHighWaterMark(NULL); //WLEDMM