The ESP32 does not have a single, unified block of RAM. It uses a Harvard architecture with strictly segregated memory regions: fast internal SRAM (IRAM/DRAM), slow external PSRAM, and deep-sleep RTC memory. When your sketch crashes with a Guru Meditation Error or fails to allocate a buffer, it is almost never a lack of total bytes. It is a misalignment between your data's access requirements and the physical memory region you forced it into.

If you are hitting alloc failed or cache panics, the direct answer is that your heap is fragmented, your ISR is executing from Flash instead of IRAM, or you are trying to pass a PSRAM-allocated buffer directly to a DMA peripheral. Below is the exact memory map, a high-speed logging build that pushes these limits, and the debugging sequence to fix the panics.

The ESP32 Memory Map: Where Your Data Actually Lives

Before writing a single line of memory-management code, you must understand the hardware boundaries. The standard ESP32-WROOM-32E has roughly 520KB of usable internal SRAM. The ESP32-WROVER-E adds an external SPI/OPI PSRAM chip (2MB to 8MB), but PSRAM is not a drop-in replacement for internal SRAM. According to the Espressif ESP-IDF Memory Allocation documentation, DMA controllers and interrupt service routines cannot access external PSRAM directly.

Memory Region Address Range Typical Size Primary Purpose Hardware Constraints
IRAM (Internal SRAM 0/1) 0x40080000 - 0x400A0000 ~128 KB Interrupt Service Routines (ISRs), Wi-Fi/BT stacks Must be 32-bit aligned for execution. Fastest access.
DRAM (Internal SRAM 0/1) 0x3FFB0000 - 0x40000000 ~176 KB General heap, global variables, DMA buffers Byte-accessible. Required for SPI/I2S DMA operations.
RTC Fast Memory 0x3FF80000 8 KB Deep sleep variable retention, ULP coprocessor Retained during deep sleep. Lost on hard reset.
RTC Slow Memory 0x50000000 8 KB ULP coprocessor code, RTC I/O state Retained during deep sleep. Very slow access.
External PSRAM 0x3F800000 (MMU mapped) 2 MB - 8 MB Large buffers, audio streaming, TLS certificates Slower (SPI/OPI). Cannot be used for DMA. Requires cache mapping.
Bench Tip: Never use standard malloc() for buffers that will be handed to the SPI or I2S DMA controllers. The DMA hardware will silently read garbage data if the pointer resolves to PSRAM. Always use heap_caps_malloc(size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) for peripheral buffers.

Project Build: High-Speed ADC Logger Pushing SRAM Limits

This build samples an external 16-bit ADC at high speed, buffering 64KB of data in PSRAM before flushing it to a MicroSD card via SPI. We use the ESP32-WROVER-E because a 64KB contiguous block is nearly impossible to secure on a WROOM module once the Wi-Fi stack and Arduino core have fragmented the 176KB DRAM heap.

Parts List

  • MCU: ESP32-WROVER-E DevKit v4.1 (8MB PSRAM, 16MB Flash)
  • ADC: Adafruit ADS1115 16-bit I2C ADC breakout
  • Storage: MicroSD SPI breakout module (with 3.3V LDO and level shifters)
  • Wiring: 22 AWG silicone stranded jumper wires

Pin Mapping Table

Component Pin / Function ESP32-WROVER-E GPIO Notes
ADS1115 SDA / SCL GPIO 21 / GPIO 22 Standard I2C bus, add 4.7k pull-ups
MicroSD MISO / MOSI / SCK / CS GPIO 19 / 23 / 18 / 5 Hardware SPI (VSPI). Do not use GPIO 12 for MISO on WROVER (strapping pin).

Compilable Code with Memory Cap Error Handling

This code targets the ESP32 Dev Module board in the Arduino IDE with PSRAM: Enabled and OPI PSRAM: Disabled (standard QSPI WROVER-E). It demonstrates the critical distinction between PSRAM buffers for data collection and internal DRAM buffers for DMA writes.

#include <Wire.h>
#include <SD.h>
#include <SPI.h>
#include <Adafruit_ADS1X15.h>
#include <esp_heap_caps.h>

// --- Pin Definitions ---
#define SD_CS_PIN    5
#define I2C_SDA_PIN  21
#define I2C_SCL_PIN  22

// --- Buffer Sizes ---
#define MAIN_BUFFER_SIZE  65536  // 64KB in PSRAM
#define DMA_CHUNK_SIZE    4096   // 4KB in Internal DRAM for SPI DMA

Adafruit_ADS1115 ads;
uint8_t* psram_buffer = nullptr;
uint8_t* dma_buffer = nullptr;

void setup() {
  Serial.begin(115200);
  delay(1000);
  
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  if (!ads.begin(0x48, &Wire)) {
    Serial.println("FATAL: ADS1115 not found on I2C.");
    while(1) { delay(100); }
  }
  ads.setDataRate(RATE_ADS1115_860SPS);

  // 1. Allocate large collection buffer in PSRAM
  // MALLOC_CAP_SPIRAM forces allocation to external chip
  psram_buffer = (uint8_t*)heap_caps_malloc(MAIN_BUFFER_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  if (psram_buffer == nullptr) {
    Serial.println("FATAL: PSRAM allocation failed. Check Tools > PSRAM setting.");
    while(1) { delay(100); }
  }

  // 2. Allocate smaller DMA buffer in Internal SRAM
  // SD card SPI library uses DMA; DMA CANNOT access PSRAM
  dma_buffer = (uint8_t*)heap_caps_malloc(DMA_CHUNK_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
  if (dma_buffer == nullptr) {
    Serial.println("FATAL: Internal DMA allocation failed. Heap too fragmented.");
    while(1) { delay(100); }
  }

  // 3. Initialize SD Card
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("FATAL: SD Card mount failed.");
    while(1) { delay(100); }
  }
  
  Serial.printf("PSRAM Free: %d bytes\n", ESP.getFreePsram());
  Serial.printf("Internal Max Alloc: %d bytes\n", ESP.getMaxAllocHeap());
}

void loop() {
  size_t buffer_index = 0;
  
  // Fill PSRAM buffer with ADC readings
  while (buffer_index < MAIN_BUFFER_SIZE - 1) {
    int16_t adc_val = ads.readADC_SingleEnded(0);
    psram_buffer[buffer_index++] = (uint8_t)(adc_val >> 8);
    psram_buffer[buffer_index++] = (uint8_t)(adc_val & 0xFF);
  }

  // Flush to SD Card in DMA-safe chunks
  File dataFile = SD.open("/data_log.bin", FILE_WRITE);
  if (!dataFile) {
    Serial.println("ERROR: Could not open file for writing.");
    return;
  }

  for (size_t i = 0; i < MAIN_BUFFER_SIZE; i += DMA_CHUNK_SIZE) {
    // Copy from PSRAM to Internal DRAM for the SPI DMA controller
    memcpy(dma_buffer, psram_buffer + i, DMA_CHUNK_SIZE);
    dataFile.write(dma_buffer, DMA_CHUNK_SIZE);
  }
  
  dataFile.close();
  Serial.println("Flush complete. Sleeping for 5s.");
  delay(5000);
}

Debugging "alloc failed" and Guru Meditation Panics

When the ESP32 runs out of memory or violates memory access rules, the FreeRTOS kernel triggers a panic. Here are the exact error strings you will see in the serial monitor, ranked by frequency, and how to resolve them.

1. The Cache Disabled Panic

Exact Error String: Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed)

The Cause: When the ESP32 writes to SPI Flash (e.g., saving to LittleFS, writing to an SD card via certain libraries, or performing an OTA update), it must disable the Flash cache. If an Interrupt Service Routine (ISR) fires during this window, and that ISR's code resides in Flash rather than IRAM, the CPU tries to fetch instructions from disabled cache and panics.

The Fix: Ensure every function called from an ISR, and the ISR itself, is tagged with the IRAM_ATTR macro. Alternatively, disable interrupts (noInterrupts()) immediately before the flash write operation and re-enable them after.

2. The PSRAM Detection Failure

Exact Error String: [E][esp32-hal-psram.c:75] psramInit(): PSRAM enabled in settings but not detected or E (123) spiram: SPI RAM chip not detected

The Cause: The Arduino IDE board menu is configured for a WROVER module, but you have physically wired a WROOM module. Alternatively, you are using an 8MB OPI (Octal SPI) PSRAM module (like the ESP32-S3-WROOM-2) but the board menu is set to standard QSPI, or the PSRAM clock speed is set to 80MHz when the PCB trace length only supports 40MHz.

The Fix: Verify the physical silicon. If it is a WROVER-E, set PSRAM: Enabled and PSRAM Clock: 40MHz in the Arduino Tools menu. If using ESP32-S3, ensure OPI PSRAM is explicitly selected.

3. The Heap Fragmentation Failure

Exact Error String: std::bad_alloc (C++) or malloc returning NULL despite ESP.getFreeHeap() showing 40,000+ bytes available.

The Cause: Heap fragmentation. You have 40KB free, but it is broken into 200 chunks of 200 bytes. A request for a contiguous 10KB buffer fails.

The Fix: Stop using dynamic allocation (new, malloc, String class) inside the loop(). Allocate large buffers exactly once in setup() using heap_caps_malloc, or use statically allocated global arrays.

First 3 Things to Check When Memory Fails:
  1. Board Menu Settings: Is PSRAM actually enabled in the Arduino IDE Tools menu? The compiler needs the -DBOARD_HAS_PSRAM flag to map the memory correctly.
  2. Fragmentation Delta: Print both ESP.getFreeHeap() and ESP.getMaxAllocHeap(). If Free is 50KB but MaxAlloc is only 2KB, your heap is shattered. Restructure your allocations.
  3. ISR Attributes: Search your code for attachInterrupt. Verify the callback function is prefixed with IRAM_ATTR and does not call Serial.print() or delay().

Extending and Simplifying the Build

Depending on your final deployment environment, you will either need to scale this architecture up for production or strip it down for a low-cost BOM.

How to Extend: Dual-Core Pipeline and MQTT

The ESP32 has two main cores. Core 0 handles the Wi-Fi and Bluetooth stacks by default. To maximize throughput, pin your ADC sampling task to Core 1. Use a FreeRTOS queue to pass 4KB chunks from the Core 1 sampling task to a Core 0 task that handles the SD card writes or MQTT publishing. This prevents the Wi-Fi stack from starving your ADC sampling timer. For remote telemetry, replace the SD card write block with PubSubClient, but remember to keep your MQTT payload buffers in internal DRAM, as the Wi-Fi hardware requires DMA access to transmit packets.

How to Simplify: Drop the External ADC and Dynamic Allocation

If you are building a cost-sensitive sensor node and the 12-bit internal ADC is sufficient, drop the ADS1115 and the I2C library overhead. More importantly, if your total data payload is under 16KB, abandon heap_caps_malloc entirely. Use static allocation:

// Static allocation bypasses the heap manager entirely, preventing fragmentation.
// Placed in global scope, the linker assigns this directly to DRAM.
static uint8_t adc_buffer[16384]; 

By relying on static arrays and the internal 12-bit ADC, you can run this entire logging architecture on a bare $3.00 ESP32-C3 or ESP32-WROOM-32E module without ever touching the complexities of PSRAM mapping or DMA buffer copying. For further reading on hardware constraints, refer to the ESP32-WROVER-E Datasheet for exact strapping pin and PSRAM timing requirements.