psram-aware malloc functions (backport of upstream #4895) (#342)

Introduces new memory allocation functions, based on wled#4895 by @DedeHai

* keep a minimum of 15KB RAM available to UI - improves stability
* S3/S2/C3 automatically use "fast RTC Ram" for small data (reduces fragmentation)
* auto-detects PSRAM (or no PSRAM) when firmware was built with -D BOARD_HAS_PSRAM
* d_malloc() and d_calloc() prefer DRAM if possible (faster), but fall back to PSRAM when free RAM gets low.
This commit is contained in:
Frank Möhle
2026-02-18 17:57:47 +01:00
committed by GitHub
parent 50bb4d2910
commit cce3c0b5d3
19 changed files with 696 additions and 191 deletions

View File

@@ -2562,14 +2562,24 @@ public:
//open programFile
char * programText = nullptr;
uint16_t programFileSize;
size_t programFileSize;
#if ARTI_PLATFORM == ARTI_ARDUINO
programFileSize = programFile.size();
programText = (char *)malloc(programFileSize+1);
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);
programText = (char *)malloc(programTextSize+1);
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 @@ public:
#endif
if (stages < 1) {
if (nullptr != programText) 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 @@ public:
#endif
}
#if ARTI_PLATFORM == ARTI_ARDUINO //not on windows as cause crash???
d_free(programText);
#else
free(programText);
#endif
programText = nullptr;
if (stages >= 3)
{

View File

@@ -131,9 +131,10 @@ static volatile bool disableSoundProcessing = false; // if true, sound proc
static uint8_t audioSyncEnabled = AUDIOSYNC_NONE; // bit field: bit 0 - send, bit 1 - receive, bit 2 - use local if not receiving
static bool audioSyncSequence = true; // if true, the receiver will drop out-of-sequence packets
static uint8_t audioSyncPurge = 1; // 0: process each packet (don't purge); 1: auto-purge old packets; 2: only process latest received packet (always purge)
static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group
static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group
static volatile bool isOOM = false; // FFTask: not enough memory for buffers (audio processing failed to start)
#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !!
#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !!
// audioreactive variables
#ifdef ARDUINO_ARCH_ESP32
@@ -454,13 +455,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); vReal = nullptr; // should not happen
if (vImag) d_free(vImag); vImag = nullptr; // 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
@@ -472,6 +473,27 @@ static bool alocateFFTBuffers(void) {
return(true); // success
}
// de-allocate FFT sample buffers from heap
static void destroyFFTBuffers(bool panicOOM) {
#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;
if (panicOOM && !isOOM) { // notify user
isOOM = true;
errorFlag = ERR_LOW_MEM;
USER_PRINTLN("AR startup failed - out of memory!");
}
#ifdef SR_DEBUG
USER_PRINTLN("\ndestroyFFTBuffers() 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) {
@@ -488,6 +510,22 @@ static void runDCBlocker(uint_fast16_t numSamples, float *sampleBuffer) {
}
}
//
// FFT runner - "return" from a task function causes crash. This wrapper keeps the task alive
//
void runFFTcode(void * parameter) __attribute__((noreturn,used));
void runFFTcode(void * parameter) {
bool firstFail = true; // prevents flood of warnings
do {
if (!disableSoundProcessing) {
FFTcode(parameter);
if (firstFail) {USER_PRINTLN(F("warning: unexpected exit of FFT main task."));}
firstFail = false;
} else firstFail = true; // re-enable warning message
vTaskDelay(1000); // if we arrive here, FFcode has returned due to OOM. Wait a bit, then try again.
} while (true);
}
//
// FFT main task
//
@@ -511,13 +549,18 @@ 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; destroyFFTBuffers(true); 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;
destroyFFTBuffers(true);
return;
}
// create FFT object - we have to do if after allocating buffers
#if defined(FFT_LIB_REV) && FFT_LIB_REV > 0x19
@@ -527,7 +570,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; destroyFFTBuffers(true); return; } // alloc failed
#else
static float windowWeighingFactors[samplesFFT] = {0.0f}; // cache for FFT windowing factors - use global RAM
#endif
@@ -1921,6 +1965,7 @@ class AudioReactive : public Usermod {
void setup() override
{
disableSoundProcessing = true; // just to be sure
isOOM = false;
if (!initDone) {
// usermod exchangeable data
// we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers
@@ -2497,11 +2542,12 @@ class AudioReactive : public Usermod {
vTaskResume(FFT_Task);
connected(); // resume UDP
} else {
isOOM = false;
if (audioSource) // WLEDMM only create FFT task if we have a valid audio source
// xTaskCreatePinnedToCore(
// xTaskCreate( // no need to "pin" this task to core #0
xTaskCreateUniversal(
FFTcode, // Function to implement the task
runFFTcode, // Function to implement the task
"FFT", // Name of the task
3592, // Stack size in words // 3592 leaves 800-1024 bytes of task stack free
NULL, // Task input parameter
@@ -2646,7 +2692,7 @@ class AudioReactive : public Usermod {
#else // ESP32 only
} else {
// Analog or I2S digital input
if (audioSource && (audioSource->isInitialized())) {
if (audioSource && (audioSource->isInitialized()) && !isOOM) {
// audio source successfully configured
if (audioSource->getType() == AudioSource::Type_I2SAdc) {
infoArr.add(F("ADC analog"));
@@ -2667,7 +2713,8 @@ class AudioReactive : public Usermod {
} else {
// error during audio source setup
infoArr.add(F("not initialized"));
if (dmType < 254) infoArr.add(F(" - check pin settings"));
if (isOOM) infoArr.add(F(" - out of memory"));
else if (dmType < 254) infoArr.add(F(" - check pin settings"));
}
}

View File

@@ -151,9 +151,9 @@ private:
unsigned char Enc_A_prev = 0;
bool currentEffectAndPaletteInitialized = false;
uint8_t effectCurrentIndex = 0;
uint16_t effectCurrentIndex = 0;
uint8_t effectPaletteIndex = 0;
uint8_t knownMode = 0;
uint16_t knownMode = 0;
uint8_t knownPalette = 0;
uint8_t currentCCT = 128;
@@ -339,9 +339,11 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() {
//modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount());
modes_qstrings = strip.getModeDataSrc();
modes_alpha_indexes = re_initIndexArray(strip.getModeCount());
if (!modes_alpha_indexes) return; // avoid nullptr crash when allocation has failed (OOM)
re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT);
palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount());
if (!palettes_qstrings) return; // avoid nullptr crash when allocation has failed (OOM)
palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); // only use internal palettes
// How many palette names start with '*' and should not be sorted?
@@ -352,9 +354,10 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() {
}
byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) {
byte *indexes = (byte *)malloc(sizeof(byte) * numModes);
for (byte i = 0; i < numModes; i++) {
indexes[i] = i;
byte* indexes = (byte *)p_calloc(numModes, sizeof(byte));
if (!indexes) return nullptr; // avoid OOM crash
for (uint16_t i = 0; i < numModes; i++) {
indexes[i] = min(i, uint16_t(255));
}
return indexes;
}
@@ -364,8 +367,10 @@ 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);
uint8_t modeIndex = 0;
const char** modeStrings = (const char **)p_calloc(numModes, sizeof(const char *));
if (!modeStrings) return nullptr; // avoid OOM crash
uint16_t modeIndex = 0;
bool insideQuotes = false;
// advance past the mark for markLineNum that may exist.
char singleJsonSymbol;
@@ -380,7 +385,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 '[':
@@ -630,7 +635,7 @@ void RotaryEncoderUIUsermod::displayNetworkInfo() {
void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() {
if (modes_alpha_indexes == nullptr) return; // WLEDMM bugfix
currentEffectAndPaletteInitialized = true;
for (uint8_t i = 0; i < strip.getModeCount(); i++) {
for (uint16_t i = 0; i < strip.getModeCount(); i++) {
if (modes_alpha_indexes[i] == effectCurrent) {
effectCurrentIndex = i;
break;