Chapter 238: ESP32 PSRAM Configuration and Usage

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the purpose and benefits of using external PSRAM with an ESP32.
  • Explain how PSRAM is integrated into the ESP32’s memory map.
  • Configure your ESP-IDF project to enable and initialize PSRAM.
  • Allocate and use memory in PSRAM from your application.
  • Identify which ESP32 variants support PSRAM and understand their differences.
  • Diagnose and troubleshoot common PSRAM-related issues.

Introduction

As your embedded applications grow in complexity, they often demand more memory than is available in the microcontroller’s internal SRAM. Applications involving high-resolution displays, audio processing, image capture, or extensive data logging can quickly exhaust the few hundred kilobytes of internal RAM on an ESP32.

This is where PSRAM (Pseudo-Static RAM) becomes an invaluable resource. PSRAM is a type of external dynamic RAM that behaves like static RAM, offering a cost-effective way to dramatically expand the available memory. By adding a PSRAM chip to your hardware design, you can add multiple megabytes of extra RAM, unlocking capabilities that would otherwise be impossible.

In this chapter, we will explore the theory behind how the ESP32 family interfaces with external PSRAM and integrates it seamlessly into the system memory. We will then walk through the practical steps of configuring, initializing, and using PSRAM in your ESP-IDF projects.

Theory

What is PSRAM?

PSRAM stands for Pseudo-Static Random-Access Memory. Internally, it is a Dynamic RAM (DRAM), which requires periodic refreshing to retain its data. However, the PSRAM chip includes a built-in refresh controller. This makes it “pseudo-static” from the perspective of the host microcontroller; the ESP32 can treat it like a simpler Static RAM (SRAM) without needing to manage the refresh cycles itself. This combination provides high-density, low-cost memory with a simple interface.

How ESP32 Integrates PSRAM

The true power of PSRAM on the ESP32 lies in its integration. It is not treated as a simple peripheral that you read and write from manually. Instead, the ESP32’s memory management unit (MMU) and cache controller work together to map the external PSRAM directly into the CPU’s address space.

When you enable PSRAM, a portion of the ESP32’s data address space is reserved for it. When your code attempts to access an address in this range, the hardware automatically handles the communication with the external PSRAM chip.

Key Concepts:

  • SPI/QSPI/OSPI Interface: The PSRAM chip is physically connected to the ESP32 using a Serial Peripheral Interface (SPI). Different ESP32 variants support different modes:
    • SPI (1-bit): Standard SPI, slowest mode.
    • QSPI (4-bit): Quad SPI, uses four data lines for higher throughput.
    • OSPI (8-bit): Octal SPI, uses eight data lines for the highest throughput.
  • Cache: Because external RAM is significantly slower than the ESP32’s internal SRAM, a cache is essential for performance. When data from PSRAM is requested, it is fetched and stored in a small, fast internal cache. Subsequent reads of the same data can be served directly from the cache, avoiding the slow trip to the external chip. This process is transparent to your application code.
  • Memory Mapping: The ESP-IDF maps the PSRAM into the virtual address range 0x3F800000 – 0x3FC00000, allowing up to 4MB of external memory to be addressed. To your C code, an array allocated in PSRAM looks just like any other pointer, and you can access it using standard C operations like array[i] or *ptr.

Heap Integration

The most common way to use PSRAM is by adding it to the heap. The ESP-IDF heap allocator (heap_caps) can manage multiple memory regions with different capabilities. When PSRAM is enabled, it is registered with the heap allocator under the MALLOC_CAP_SPIRAM capability. This allows you to explicitly request memory from the PSRAM pool.

Tip: You can also configure ESP-IDF to use PSRAM for certain system-level allocations, such as the task stacks of newly created FreeRTOS tasks, or to allow malloc() to fall back to PSRAM if internal memory is full. This is configured in menuconfig.

Practical Examples

Let’s build a project that initializes PSRAM, checks its size, and allocates a large block of memory.

Hardware Requirements

  • An ESP32 development board that includes a PSRAM chip (e.g., ESP32-WROVER, ESP32-S3-DevKitC).
  • A USB cable for flashing and monitoring.

Step 1: Project Configuration with menuconfig

  1. Open a terminal in your project directory (e.g., psram_example).
  2. Launch the configuration tool: idf.py menuconfig.
  3. Navigate to Component config —> ESP32-specific (or ESP32S3-specific, etc.).
  4. Enter the SPI RAM config menu.
  5. Check the [*] Support for external SPI RAM option.
  6. You will now see several new options. For most development boards, the defaults are correct.
    • SPI RAM access method: Set to Make RAM allocatable using heap_caps_malloc.
    • Initialize SPI RAM when booting: Ensure this is checked.
    • The SPI RAM Cfg submenu allows you to configure timing and mode. Leave these at their default values unless your specific hardware requires changes.
  7. Save the configuration and exit menuconfig.
%%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#4B5563', 'fontFamily': 'Open Sans, sans-serif'}}}%%
graph TD
    subgraph ESP-IDF Project Configuration for PSRAM

    A[Start: Project Directory] --> B{Run idf.py menuconfig};
    style A fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6

    B --> C["Navigate to <br><b>Component config</b>"];
    style C fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF

    C --> D["Select chip-specific menu <br> e.g., <b>ESP32-specific</b>"];
    style D fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF

    D --> E["Enter <b>SPI RAM config</b>"];
    style E fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF

    E --> F{"Enable <br><b>[*] Support for external SPI RAM</b>"};
    style F fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E

    F --> G["Set <b>SPI RAM access method</b> to <br> <i>'Make RAM allocatable...'</i>"];
    style G fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    
    G --> H{"Ensure <br><b>[*] Initialize SPI RAM when booting</b> <br> is checked"};
    style H fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B

    H --> I["(Optional) Adjust timing/mode <br> in <b>SPI RAM Cfg</b> for custom hardware"];
    style I fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF

    I --> J[Save Configuration & Exit];
    style J fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF

    J --> K[Configuration Complete];
    style K fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46
    
    end

Warning: Incorrectly configuring the PSRAM clock speed or mode can lead to instability or prevent the device from booting. Always start with the manufacturer’s recommended settings for your board.

Step 2: Application Code

Create a file main/psram_example_main.c and add the following code.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_heap_caps.h"

static const char *TAG = "PSRAM_EXAMPLE";

void app_main(void)
{
    ESP_LOGI(TAG, "--- PSRAM Example Start ---");

    // Check if PSRAM is available
    if (!esp_spiram_is_initialized()) {
        ESP_LOGE(TAG, "PSRAM not initialized!");
        // Depending on the application, you might want to halt or handle this error
        return;
    }

    // Get total and free size of PSRAM
    size_t psram_size = esp_spiram_get_size();
    ESP_LOGI(TAG, "PSRAM detected, total size: %d bytes", psram_size);

    // --- Heap Information ---
    // Log heap information BEFORE allocation
    ESP_LOGI(TAG, "--- Heap status before allocation ---");
    heap_caps_print_heap_info(MALLOC_CAP_DEFAULT);
    heap_caps_print_heap_info(MALLOC_CAP_SPIRAM);

    // Allocate a large buffer in PSRAM
    // Let's try to allocate 1MB (1024 * 1024 bytes)
    size_t buffer_size = 1 * 1024 * 1024;
    ESP_LOGI(TAG, "Attempting to allocate %d bytes from PSRAM...", buffer_size);
    
    // Use heap_caps_malloc with the MALLOC_CAP_SPIRAM flag
    char *psram_buffer = (char *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);

    if (psram_buffer == NULL) {
        ESP_LOGE(TAG, "Failed to allocate memory from PSRAM!");
        // Log heap info again to see the state after failed allocation
        heap_caps_print_heap_info(MALLOC_CAP_SPIRAM);
    } else {
        ESP_LOGI(TAG, "Successfully allocated %d bytes at address %p", buffer_size, psram_buffer);

        // --- Heap Information After Allocation ---
        ESP_LOGI(TAG, "--- Heap status after allocation ---");
        heap_caps_print_heap_info(MALLOC_CAP_SPIRAM);
        
        // Let's test the allocated memory
        ESP_LOGI(TAG, "Testing PSRAM buffer by writing and reading...");
        // Fill the buffer with a pattern
        for (size_t i = 0; i < buffer_size; i++) {
            psram_buffer[i] = (char)(i % 256);
        }

        // Verify the pattern
        bool success = true;
        for (size_t i = 0; i < buffer_size; i++) {
            if (psram_buffer[i] != (char)(i % 256)) {
                ESP_LOGE(TAG, "Verification failed at index %d!", i);
                success = false;
                break;
            }
        }
        
        if (success) {
            ESP_LOGI(TAG, "PSRAM buffer test passed!");
        } else {
            ESP_LOGE(TAG, "PSRAM buffer test failed!");
        }

        // Free the allocated memory when done
        heap_caps_free(psram_buffer);
        ESP_LOGI(TAG, "Freed PSRAM buffer.");

        // --- Heap Information After Freeing ---
        ESP_LOGI(TAG, "--- Heap status after freeing memory ---");
        heap_caps_print_heap_info(MALLOC_CAP_SPIRAM);
    }

    ESP_LOGI(TAG, "--- PSRAM Example End ---");
}

%%{init: {'theme': 'base', 'themeVariables': {'lineColor': '#4B5563', 'fontFamily': 'Open Sans, sans-serif'}}}%%
sequenceDiagram
    actor App as Application Code
    participant ESPIDF as ESP-IDF API
    participant Heap as Heap Allocator
    participant PSRAM as PSRAM Heap
    participant SRAM as Internal Heap

    App->>ESPIDF: esp_spiram_is_initialized()
    activate ESPIDF
    ESPIDF-->>App: returns true
    deactivate ESPIDF
    
    App->>ESPIDF: heap_caps_malloc(size, MALLOC_CAP_SPIRAM)
    activate ESPIDF
    ESPIDF->>Heap: Request memory with SPIRAM capability
    activate Heap
    
    Heap->>PSRAM: Allocate block of 'size' bytes
    activate PSRAM
    PSRAM-->>Heap: Return pointer to allocated block
    deactivate PSRAM
    
    Heap-->>ESPIDF: Forward the pointer
    deactivate Heap
    ESPIDF-->>App: returns psram_buffer*
    deactivate ESPIDF
    
    Note right of App: Application now has a valid pointer<br>to a memory block in PSRAM.<br>Address is in the 0x3F8xxxxx range.

    App->>App: Use psram_buffer (read/write)
    
    App->>ESPIDF: heap_caps_free(psram_buffer)
    activate ESPIDF
    ESPIDF->>Heap: Request to free the pointer
    activate Heap
    Heap->>PSRAM: Mark block as free
    activate PSRAM
    deactivate PSRAM
    deactivate Heap
    deactivate ESPIDF
    
    Note right of App: The memory is now returned<br>to the PSRAM heap.

Step 3: Build, Flash, and Monitor

  1. Build the project: idf.py build
  2. Flash to your device: idf.py -p /dev/ttyUSB0 flash (replace /dev/ttyUSB0 with your device’s port).
  3. Monitor the output: idf.py -p /dev/ttyUSB0 monitor

Expected Output

You should see output similar to the following. The exact memory sizes will vary based on your ESP-IDF version and configuration.

I (314) PSRAM_EXAMPLE: --- PSRAM Example Start ---
I (314) spiram: Found 4MB SPI RAM device
I (324) spiram: Speed: 80MHz, mode: QIO
I (324) spiram: PSRAM initialized, cache is available
I (324) PSRAM_EXAMPLE: PSRAM detected, total size: 4194252 bytes
I (334) PSRAM_EXAMPLE: --- Heap status before allocation ---
I (344) HEAP_CAPS: At 0x3FFB0000 len 262144 (256 KiB): DRAM
... (other internal heap info) ...
I (364) HEAP_CAPS: At 0x3F800000 len 4194252 (4095 KiB): SPIRAM
I (364) PSRAM_EXAMPLE: Attempting to allocate 1048576 bytes from PSRAM...
I (374) PSRAM_EXAMPLE: Successfully allocated 1048576 bytes at address 0x3f800020
I (384) PSRAM_EXAMPLE: --- Heap status after allocation ---
I (394) HEAP_CAPS: At 0x3F900020 len 3145676 (3071 KiB): SPIRAM
I (394) PSRAM_EXAMPLE: Testing PSRAM buffer by writing and reading...
I (1434) PSRAM_EXAMPLE: PSRAM buffer test passed!
I (1434) PSRAM_EXAMPLE: Freed PSRAM buffer.
I (1434) PSRAM_EXAMPLE: --- Heap status after freeing memory ---
I (1444) HEAP_CAPS: At 0x3F800000 len 4194252 (4095 KiB): SPIRAM
I (1454) PSRAM_EXAMPLE: --- PSRAM Example End ---

Variant Notes

PSRAM support is highly dependent on the ESP32 variant you are using.

ESP32 Variant PSRAM Support Interface Max Speed (Typical) Best For
ESP32 (Original) Yes SPI / QSPI (4-bit) 80 MHz General-purpose applications needing memory expansion (e.g., WROVER modules).
ESP32-S2 Yes SPI / QSPI (4-bit) 80 MHz Applications needing a bit more performance and security features than the original ESP32.
ESP32-S3 Yes QSPI / OSPI (8-bit) 120 MHz (Octal) High-throughput applications like graphical displays (LVGL), audio streaming, and image processing.
ESP32-C3 / C6 No N/A N/A Cost-sensitive, low-power IoT nodes where external RAM is not required.
ESP32-H2 No N/A N/A Thread/Zigbee-focused applications that do not have high RAM requirements.
  • ESP32 (Original): Supports SPI and QSPI PSRAM. Most common modules (like the WROVER series) use a 4MB PSRAM chip. Performance is good but limited by the SPI bus speed (typically 40MHz or 80MHz).
  • ESP32-S2: Supports SPI and QSPI PSRAM. The memory controller is improved, offering slightly better performance than the original ESP32.
  • ESP32-S3: Offers the best PSRAM performance. It supports high-speed Octal SPI (OSPI) PSRAM, which uses an 8-bit data bus. This allows for much higher data throughput, making it suitable for applications like driving graphical displays or processing video streams directly from PSRAM.
  • ESP32-C3, ESP32-C6, ESP32-H2: These variants do not support external PSRAM. They are designed for lower-cost applications and lack the required GPIOs and memory controller to interface with an external RAM chip. If your application requires more RAM, you must choose an ESP32, ESP32-S2, or ESP32-S3.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Configuration Forgotten Code compiles, but esp_spiram_is_initialized() returns false. Allocation with MALLOC_CAP_SPIRAM fails, returning NULL. Run idf.py menuconfig. Navigate to:
Component configESP32-specificSPI RAM config and ensure [*] Support for external SPI RAM is checked.
Hardware / Boot Failure Device fails to boot. Serial monitor shows a boot loop or errors like E (152) spiram: PSRAM ID read error or E (159) spiram: SPIRAM init failed. 1. Verify your board has a PSRAM chip (e.g., ESP32-WROVER).
2. For custom PCBs, double-check schematics, soldering, and GPIO connections.
3. Try a lower clock speed in menuconfig as a test.
Incorrect DMA Usage DMA transfers (e.g., for SPI, I2S, LCD) are corrupted, slow, or fail entirely. The device might crash with a Guru Meditation Error. DMA buffers must be in internal RAM. Allocate them with the MALLOC_CAP_DMA flag. Example:
dma_buffer = heap_caps_malloc(size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
Unrealistic Performance Expectation The application is functionally correct but runs much slower than expected when processing data stored in PSRAM. Remember PSRAM is slower than SRAM. Optimize for cache performance by accessing memory sequentially. For critical tasks (interrupts, fast loops), use internal RAM. Profile your code to identify bottlenecks.
Stack Overflow in PSRAM When using task stacks in PSRAM, the device crashes unexpectedly. The backtrace may be corrupted or misleading. Even though the stack is large, it can still overflow. Increase the task stack size in xTaskCreate. Use uxTaskGetStackHighWaterMark() to check actual stack usage during runtime to find the right size.

Exercises

  1. Heap Fallback: Modify the project configuration (menuconfig -> Component config -> ESP32-specific -> SPI RAM config) to enable “Allow malloc() to automatically fall back to SPI RAM”. Write a program that first allocates all available internal DRAM and then shows that a subsequent malloc() call successfully allocates memory from PSRAM.
  2. Task Stack in PSRAM: Configure your project to place the stacks of newly created FreeRTOS tasks into PSRAM (CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY). Create a task that allocates a large array on its stack (e.g., int my_large_array[1024];) and verify that the application still runs correctly. Observe the change in available internal vs. external memory.
  3. Performance Comparison: Write a function that performs a computationally intensive operation on a large array (e.g., summing all elements). Time how long this function takes to execute when the array is in internal RAM versus when it’s in PSRAM. Print the results to see the performance difference firsthand.

Summary

  • PSRAM provides a cost-effective way to add megabytes of extra memory to your ESP32 project.
  • It is integrated into the ESP32’s memory map and made accessible through the heap allocator, making it easy to use from application code.
  • The ESP32’s cache is crucial for making PSRAM access reasonably fast, but it’s still significantly slower than internal RAM.
  • PSRAM must be explicitly enabled and configured in menuconfig.
  • The heap_caps_malloc function with the MALLOC_CAP_SPIRAM flag is the primary way to allocate memory in PSRAM.
  • PSRAM support varies by chip: ESP32, ESP32-S2, and ESP32-S3 support it, while the C-series and H-series do not.
  • For performance-critical data and DMA buffers, internal RAM is almost always the better choice.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top