ESP32无PSRAM网络收音机堆损坏重启问题求助
ESP32无PSRAM网络收音机堆损坏重启问题
我使用ESP-Wroom-32(无PSRAM)搭配MAX98357A I2S功放制作了一台网络收音机,但每隔约一分钟就会出现堆损坏错误并重启,错误信息如下:
CORRUPT HEAP: Bad head at 0x3ffe854c. Expected 0xabba1234 got 0xa40c84d1
assert failed: multi_heap_free multi_heap_poisoning.c:279 (head != NULL)
我在Arduino IDE中将分区方案设置为"Huge APP",以下是我的实现代码:
#include <WiFi.h> #include <Audio.h> #include <AiEsp32RotaryEncoder.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #define I2S_DOUT 25 // din #define I2S_BCLK 26 // bclk #define I2S_LRC 27 // lrc #define VOLUME_PIN 34 #define ROTARY_ENCODER_A_PIN 32 // clk #define ROTARY_ENCODER_B_PIN 33 // dt #define ROTARY_ENCODER_BUTTON_PIN 35 // sw #define ROTARY_ENCODER_STEPS 4 #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 32 #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C #define I2C_SDA 21 #define I2C_SCL 22 AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(ROTARY_ENCODER_A_PIN, ROTARY_ENCODER_B_PIN, ROTARY_ENCODER_BUTTON_PIN, -1, ROTARY_ENCODER_STEPS); Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); Audio audio; const char* ssid = "xxx"; const char* password = "xxx"; const char* stations[] = { "https://icast.connectmedia.hu/5001/live.mp3", "https://icast.connectmedia.hu/4738/mr2.mp3", "https://icast.connectmedia.hu/5223/live.mp3", "https://slagerfm.netregator.hu:7813/slagerfm128.mp3", "https://stream.42netmedia.com:8443/sc_gyor1", "https://stream.bauermedia.sk/128.mp3", "https://stream.funradio.sk:8000/fun128.mp3", "https://stream.bauermedia.sk/melody-hi.mp3", "https://stream.bauermedia.sk/europa2.mp3", "https://stream.bauermedia.sk/rock-hi.mp3", "https://stream.radiovlna.sk/vlna-hi.mp3" }; const char* stationNames[] = { "Retro Radio", "Petofi Radio", "Radio 1", "Slager FM", "Gyor Plusz Radio", "Radio Expres", "Fun Radio", "Radio Jemne", "Europa 2", "Radio Rock", "Radio Vlna" }; const int NUM_STATIONS = sizeof(stations) / sizeof(stations[0]); int currentStation = 0; char streamTitle[64] = ""; // Volume control variables const int SAMPLES = 5; int volumeReadings[SAMPLES]; int readIndex = 0; int total = 0; int average = 0; unsigned long lastVolumeCheck = 0; const unsigned long VOLUME_CHECK_INTERVAL = 10; bool isWiFiConnected = false; bool isDisplayInitialized = false; bool isAudioInitialized = false; void IRAM_ATTR readEncoderISR() { rotaryEncoder.readEncoder_ISR(); } void setup() { delay(1000); Serial.begin(115200); while(!Serial) { delay(10); } pinMode(VOLUME_PIN, INPUT); rotaryEncoder.begin(); rotaryEncoder.setup(readEncoderISR); rotaryEncoder.setBoundaries(0, NUM_STATIONS - 1, true); rotaryEncoder.setAcceleration(0); Wire.begin(I2C_SDA, I2C_SCL); for (int i = 0; i < SAMPLES; i++) { volumeReadings[i] = 0; } } void loop() { static unsigned long lastInitAttempt = 0; const unsigned long initInterval = 1000; if (!isDisplayInitialized && millis() - lastInitAttempt > initInterval) { initializeDisplay(); lastInitAttempt = millis(); } if (!isWiFiConnected && millis() - lastInitAttempt > initInterval) { connectToWiFi(); lastInitAttempt = millis(); } if (isWiFiConnected && !isAudioInitialized && millis() - lastInitAttempt > initInterval) { initializeAudio(); lastInitAttempt = millis(); } if (isDisplayInitialized && isWiFiConnected && isAudioInitialized) { audio.loop(); checkEncoder(); checkVolumeControl(); } } void initializeDisplay() { if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { return; } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0,0); display.display(); isDisplayInitialized = true; } void connectToWiFi() { WiFi.begin(ssid, password); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); attempts++; } if (WiFi.status() == WL_CONNECTED) { isWiFiConnected = true; if (isDisplayInitialized) { display.clearDisplay(); display.setCursor(0,0); display.display(); } } } void initializeAudio() { audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); connectToStation(currentStation); isAudioInitialized = true; } void checkEncoder() { if (rotaryEncoder.encoderChanged()) { currentStation = rotaryEncoder.readEncoder(); connectToStation(currentStation); } if (rotaryEncoder.isEncoderButtonClicked()) { // empty } } void connectToStation(int stationIndex) { audio.stopSong(); audio.connecttohost(stations[stationIndex]); updateDisplay(); } void checkVolumeControl() { unsigned long currentMillis = millis(); if (currentMillis - lastVolumeCheck >= VOLUME_CHECK_INTERVAL) { lastVolumeCheck = currentMillis; total = total - volumeReadings[readIndex]; volumeReadings[readIndex] = analogRead(VOLUME_PIN); total = total + volumeReadings[readIndex]; readIndex = (readIndex + 1) % SAMPLES; average = total / SAMPLES; int volume = map(average, 0, 4095, 0, 15); static int lastVolume = -1; if (volume != lastVolume) { audio.setVolume(volume); lastVolume = volume; updateDisplay(); } } } void updateDisplay() { if (!isDisplayInitialized) return; display.clearDisplay(); display.setCursor(0,0); display.println(String(stationNames[currentStation])); display.println(); display.println(String(streamTitle)); display.display(); } void audio_showstreamtitle(const char *info) { Serial.print("streamtitle: "); Serial.println(info); strncpy(streamTitle, info, sizeof(streamTitle) - 1); streamTitle[sizeof(streamTitle) - 1] = '\0'; // Ensure null-termination updateDisplay(); }
解决建议
1. 适配无PSRAM的音频库内存配置
ESP32-audioI2S库默认配置针对带PSRAM的设备,无PSRAM时会占用过多内部RAM引发堆损坏。修改初始化函数添加内存限制:
void initializeAudio() { audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); audio.setBufferSize(1024); // 降低缓冲区大小,适配内部RAM audio.setStackSize(2048); // 调整音频任务栈,避免栈溢出 connectToStation(currentStation); isAudioInitialized = true; }
2. 移除中断回调,改用轮询读取编码器
中断回调中若存在隐式内存操作,容易破坏堆结构。改为在loop中主动轮询:
- 删除
IRAM_ATTR修饰的readEncoderISR函数 - 在
setup中移除rotaryEncoder.setup(readEncoderISR); - 更新
checkEncoder函数:
void checkEncoder() { rotaryEncoder.readEncoder(); // 主动轮询读取状态 if (rotaryEncoder.encoderChanged()) { currentStation = rotaryEncoder.readEncoder(); connectToStation(currentStation); } if (rotaryEncoder.isEncoderButtonClicked()) { // 保留空实现 } }
3. 减少字符串对象的频繁创建
updateDisplay中反复创建String会导致频繁堆分配/释放,产生内存碎片。直接使用原生字符串打印:
void updateDisplay() { if (!isDisplayInitialized) return; display.clearDisplay(); display.setCursor(0,0); display.print(stationNames[currentStation]); display.println(); display.print(streamTitle); display.display(); }
4. 优化WiFi连接的阻塞逻辑
原连接逻辑中delay(500)会长时间阻塞主线程,影响音频任务的内存管理。改为短超时的非阻塞等待:
void connectToWiFi() { if (WiFi.status() != WL_CONNECTED) { WiFi.begin(ssid, password); unsigned long startMillis = millis(); // 最多等待10秒,每次间隔100ms检查状态 while (WiFi.status() != WL_CONNECTED && millis() - startMillis < 10000) { delay(100); } } if (WiFi.status() == WL_CONNECTED) { isWiFiConnected = true; if (isDisplayInitialized) { display.clearDisplay(); display.setCursor(0,0); display.display(); } } }
5. 启用堆调试定位问题(临时排查)
在Arduino IDE的ESP32配置中开启堆调试,可精准定位内存泄漏或越界:
- 工具→ESP32 Arduino→Core Debug Level 设置为
Verbose - 工具→Heap Memory Debugging 设置为
Enabled
内容的提问来源于stack exchange,提问作者Tamás Csiba
相关产品推荐
相关产品推荐

