If you are building data-heavy ESP32 projects—like camera streaming, audio buffering, or large MQTT payloads—you will inevitably hit a memory wall. The direct answer to querying accurate ESP32 internal memory info is to bypass the basic ESP.getFreeHeap() function and instead use the ESP-IDF heap capabilities API: heap_caps_get_free_size(MALLOC_CAP_INTERNAL). This isolates your true internal SRAM from external PSRAM, giving you the exact diagnostic data needed to prevent allocation crashes.

The ESP32 does not have a single, unified block of RAM. It features a fragmented memory architecture split between IRAM, DRAM, RTC memory, and optional SPI-connected PSRAM. Misunderstanding these boundaries is the number one cause of the dreaded Guru Meditation panics. This guide breaks down the hardware reality, provides a complete diagnostic sketch, and gives you the exact debugging steps to fix memory allocation failures.

The ESP32 Memory Architecture: Spec Sheet Breakdown

Before writing diagnostic code, you need to know what you are measuring. The ESP32 allocates its 520KB of internal SRAM into specific capability regions. Here is the hardware reality for the standard ESP32-D0WD-V3 silicon:

Memory Type Typical Size Primary Use Case Allocation Constraint
IRAM (Instruction RAM) ~128 KB Interrupt Service Routines (ISRs), boot code Must be 32-bit aligned; cannot be used for standard malloc.
DRAM (Data RAM) ~176 KB Global variables, standard heap (malloc/new) Standard 8-bit aligned memory. First to fill up.
RTC RAM (Real-Time Clock) 8 KB (Fast) / 8 KB (Slow) Variables that must survive Deep Sleep Requires RTC_DATA_ATTR or RTC_RODATA_ATTR macros.
PSRAM (Pseudo-SRAM) 0 to 8 MB Large buffers (audio, video, JSON parsing) Only available on WROVER modules; accessed via SPI; slower than internal DRAM.
Flash (SPI NOR) 4 MB to 16 MB Firmware, LittleFS/SPIFFS, NVS (Preferences) Read-only during execution; requires partition table mapping.
Bench Tip: When you call ESP.getFreeHeap() on a WROVER module with PSRAM enabled, the Arduino core will often report the combined free space of internal DRAM and PSRAM. This tricks you into thinking you have megabytes of fast internal RAM. Always use heap_caps_get_free_size(MALLOC_CAP_INTERNAL) to see your actual fast SRAM footprint.

Parts List & Diagnostic Wiring

To run the comprehensive memory diagnostic sketch provided below, you need a board that actually features PSRAM so we can test the boundary between internal and external memory. If you use a basic WROOM board, the PSRAM tests will safely abort.

Required Hardware

  • Microcontroller: ESP32-WROVER-E DevKit (e.g., Freenove ESP32-WROVER or Espressif ESP32-WROVER-KIT). Target Variant: 8MB Flash, 8MB QSPI PSRAM.
  • Alternative Board: ESP32-WROOM-32D DevKit (4MB Flash, 520KB SRAM, 0MB PSRAM). The code includes fallback handling for this variant.
  • Power Supply: 5V / 2A USB-C or micro-USB bench supply. PSRAM initialization causes a massive current spike on boot; a weak USB hub will cause a brownout reset loop.
  • Wiring: Jumper wires for UART monitoring (if not using onboard USB-to-UART bridge).

Pin Mapping Table

While this is primarily a software diagnostic tool, we map the onboard status LED and UART pins to provide physical feedback if the serial monitor is disconnected during headless testing.

Function GPIO Pin (WROVER-E) GPIO Pin (WROOM-32D) Notes
Onboard Status LED GPIO 2 GPIO 2 Active HIGH on most clone dev boards.
UART TX (Debug Out) GPIO 1 GPIO 1 Connected to onboard CP2102/CH340.
UART RX (Debug In) GPIO 3 GPIO 3 Used for hardware reset commands.
PSRAM SPI CLK GPIO 17 (Internal) N/A Routed internally on WROVER modules.

Complete Diagnostic Code (Targeting ESP32-WROVER-E)

This sketch queries all memory regions, attempts a controlled PSRAM allocation to verify external memory health, and outputs a formatted table to the Serial Monitor. It includes robust error handling for boards lacking PSRAM.

Board Configuration: In the Arduino IDE, you must select Tools > Board > ESP32 Arduino > ESP32 Wrover Module. Crucially, you must set Tools > PSRAM > Enabled, otherwise the compiler will strip PSRAM support and psramFound() will return false.
#include <Arduino.h>
#include <esp_heap_caps.h>
#include <esp_system.h>

// Pin definitions for physical feedback
#define LED_PIN 2
#define SERIAL_BAUD 115200

// Memory allocation test sizes
#define TEST_BLOCK_SIZE (1024 * 1024) // 1 MB
#define TEST_BLOCKS 4                 // Attempt to allocate 4 MB total

void printMemoryInfo() {
  Serial.println("\n--- ESP32 INTERNAL MEMORY INFO ---");
  
  // 1. Internal Heap (DRAM)
  uint32_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
  uint32_t total_internal = heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
  Serial.printf("Internal SRAM Free: %u bytes / %u bytes\n", free_internal, total_internal);
  
  // 2. IRAM (Instruction RAM)
  uint32_t free_iram = heap_caps_get_free_size(MALLOC_CAP_EXEC);
  uint32_t total_iram = heap_caps_get_total_size(MALLOC_CAP_EXEC);
  Serial.printf("IRAM (Exec) Free:   %u bytes / %u bytes\n", free_iram, total_iram);
  
  // 3. Total Heap (Includes PSRAM if enabled)
  Serial.printf("Total Heap Free:    %u bytes (ESP.getFreeHeap)\n", ESP.getFreeHeap());
  
  // 4. PSRAM Diagnostics
  if (psramFound()) {
    Serial.println("PSRAM Detected:     YES");
    Serial.printf("PSRAM Total Size:   %u bytes\n", ESP.getPsramSize());
    Serial.printf("PSRAM Free Size:    %u bytes\n", ESP.getFreePsram());
  } else {
    Serial.println("PSRAM Detected:     NO (WROOM module or PSRAM disabled in IDE)");
  }
  
  // 5. Sketch and Flash Info
  Serial.printf("Sketch Size:        %u bytes\n", ESP.getSketchSize());
  Serial.printf("Free Sketch Space:   %u bytes\n", ESP.getFreeSketchSpace());
  Serial.printf("Flash Chip Size:    %u bytes\n", ESP.getFlashChipSize());
  Serial.println("----------------------------------\n");
}

bool testPsramAllocation() {
  if (!psramFound()) {
    Serial.println("Skipping PSRAM stress test: No PSRAM detected.");
    return true; 
  }

  Serial.printf("Attempting to allocate %d blocks of %d KB in PSRAM...\n", TEST_BLOCKS, TEST_BLOCK_SIZE / 1024);
  uint8_t* blocks[TEST_BLOCKS];
  bool success = true;

  for (int i = 0; i < TEST_BLOCKS; i++) {
    // Force allocation into PSRAM using MALLOC_CAP_SPIRAM
    blocks[i] = (uint8_t*)heap_caps_malloc(TEST_BLOCK_SIZE, MALLOC_CAP_SPIRAM);
    if (blocks[i] == NULL) {
      Serial.printf("ERROR: Failed to allocate block %d in PSRAM!\n", i + 1);
      success = false;
      break;
    }
    // Write dummy data to ensure physical memory pages are mapped
    memset(blocks[i], 0xAA, TEST_BLOCK_SIZE);
  }

  // Free allocated memory
  for (int i = 0; i < TEST_BLOCKS; i++) {
    if (blocks[i] != NULL) {
      free(blocks[i]);
    }
  }

  if (success) Serial.println("PSRAM stress test PASSED.");
  return success;
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH); // LED ON during init
  
  Serial.begin(SERIAL_BAUD);
  while (!Serial) { delay(10); }
  
  Serial.println("ESP32 Memory Diagnostic Tool Starting...");
  printMemoryInfo();
  
  if (!testPsramAllocation()) {
    Serial.println("CRITICAL: PSRAM allocation failed. Check wiring, module variant, or IDE PSRAM setting.");
    // Blink LED rapidly to indicate hardware/config failure
    while(true) {
      digitalWrite(LED_PIN, !digitalRead(LED_PIN));
      delay(100);
    }
  }
  
  digitalWrite(LED_PIN, LOW); // LED OFF indicates successful boot
}

void loop() {
  // Print memory stats every 5 seconds to monitor for heap leaks
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 5000) {
    lastPrint = millis();
    Serial.printf("[Loop] Internal Free: %u | Total Free: %u\n", 
                  heap_caps_get_free_size(MALLOC_CAP_INTERNAL), 
                  ESP.getFreeHeap());
  }
}

Debugging Memory Allocation Failures

When your ESP32 runs out of memory or accesses the wrong memory region, it doesn't just fail silently; it triggers a hardware exception. Here are the exact error strings you will see in the Serial Monitor, ranked by frequency, along with their root causes.

1. The "StoreProhibited" Guru Meditation Error

Exact Error String: Guru Meditation Error: Core 1 panic'ed (StoreProhibited). Exception was unhandled.

Root Cause: You are trying to write to a null pointer or an invalid memory address. This usually happens when malloc() or new fails (returns NULL) because the heap is exhausted, and your code attempts to write to that null pointer without checking it first.

Fix: Always wrap allocations in a check. if (buffer == NULL) { log_e("Alloc failed"); return; }

2. The SPI SRAM Memory Test Fail

Exact Error String: E (123) spiram: SPI SRAM memory test fail! Some of the memory is inaccessible.

Root Cause: The ESP32 bootloader initializes PSRAM on startup. If this fails, it's almost always a power delivery issue (brownout during the SPI clock spike) or you have selected a WROVER board definition in the IDE but are physically using a WROOM board.

Fix: Verify your physical module. If it is a WROVER, upgrade your USB cable and power supply to handle the 500mA+ boot spike.

3. Cache Disabled Panic

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

Root Cause: You are performing SPI Flash operations (like writing to LittleFS or OTA updating) while an Interrupt Service Routine (ISR) is running. If the ISR code is stored in Flash instead of IRAM, the CPU cannot fetch the instruction when the Flash cache is disabled.

Fix: Add the IRAM_ATTR macro to your ISR function definition: void IRAM_ATTR myInterrupt() { ... }.

The First 3 Things to Check When Memory Fails:
  1. Partition Scheme: Go to Tools > Partition Scheme. If you are using heavy libraries, switch from "Default 4MB with spiffs" to "Huge APP (3MB No OTA/1MB SPIFFS)" to give your compiled code room to breathe.
  2. PSRAM IDE Toggle: Ensure Tools > PSRAM is explicitly set to "Enabled" if using a WROVER, or "Disabled" if using a WROOM. Mismatching this causes instant boot loops.
  3. Power Supply Ripple: Measure the 3.3V rail with an oscilloscope. PSRAM read/write operations cause high-frequency current draws that cheap linear regulators on clone boards cannot handle, resulting in silent memory corruption.

Extending or Simplifying the Build

To Simplify (WROOM-32D Target): If you are deploying to a basic ESP32-WROOM-32D module without PSRAM, you can strip the testPsramAllocation() function entirely. Change your board definition to "ESP32 Dev Module" and ensure PSRAM is disabled in the IDE. The heap_caps_get_free_size(MALLOC_CAP_INTERNAL) calls will remain perfectly valid and give you the exact DRAM footprint of your application.

To Extend (Adding Filesystem Tracking): If your project uses LittleFS for data logging, internal memory info is only half the picture. Add the #include <LittleFS.h> library, mount the filesystem in setup(), and append LittleFS.totalBytes() and LittleFS.usedBytes() to the diagnostic printout. This allows you to monitor Flash wear and partition exhaustion alongside RAM fragmentation.

Frequently Asked Questions (FAQ)

How do I check ESP32 internal memory info for PSRAM specifically?

To isolate PSRAM from internal SRAM, use ESP.getFreePsram() and ESP.getPsramSize(). If you need to force a specific variable or buffer into PSRAM to save internal DRAM, do not use standard malloc. Instead, use heap_caps_malloc(size, MALLOC_CAP_SPIRAM) or the C++ equivalent new (std::nothrow) uint8_t[size] combined with compiler hints, though the ESP-IDF heap capabilities API is the most reliable method for guaranteed placement.

Why does my ESP32 internal memory info show less RAM than the datasheet?

The datasheet advertises 520KB of internal SRAM, but your diagnostic sketch might only show ~300KB of free heap on a fresh boot. This is not a defect. The ESP32 Wi-Fi and Bluetooth stacks, the FreeRTOS operating system, and the Arduino core framework consume roughly 150KB to 200KB of DRAM just to initialize. If you disable Wi-Fi and Bluetooth entirely using esp_bt_controller_deinit() and WiFi.mode(WIFI_OFF), you will reclaim a significant portion of that "missing" memory.

Can I store variables in ESP32 RTC memory to survive deep sleep?

Yes, but standard variables are wiped when the ESP32 enters Deep Sleep because the main CPU and standard SRAM are powered down. To preserve data, you must declare your variables with the RTC_DATA_ATTR macro (e.g., RTC_DATA_ATTR int bootCount = 0;). This places the variable in the 8KB RTC Slow Memory region, which remains powered via the RTC domain. Note that RTC memory is not initialized to zero on a hard reset, only on a power-on reset, so always initialize your RTC variables explicitly in your code logic. For more on sleep states, refer to the Espressif Deep Sleep API Guide.

What is the difference between MALLOC_CAP_INTERNAL and MALLOC_CAP_DEFAULT?

MALLOC_CAP_DEFAULT allows the allocator to use any available memory, including PSRAM (if initialized). MALLOC_CAP_INTERNAL strictly restricts the allocation to the ESP32's internal SRAM (DRAM/IRAM). For time-critical tasks, audio I2S buffers, or DMA operations, you must use MALLOC_CAP_INTERNAL or MALLOC_CAP_DMA, as the SPI bus latency of PSRAM will cause buffer underruns and DMA access faults. See the ESP-IDF Memory Allocation Documentation for the full list of capability flags.