You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 18:07:01