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 likearray[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 inmenuconfig
.
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
- Open a terminal in your project directory (e.g.,
psram_example
). - Launch the configuration tool:
idf.py menuconfig
. - Navigate to
Component config
—>ESP32-specific
(orESP32S3-specific
, etc.). - Enter the
SPI RAM config
menu. - Check the
[*] Support for external SPI RAM
option. - You will now see several new options. For most development boards, the defaults are correct.
SPI RAM access method
: Set toMake 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.
- 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
- Build the project:
idf.py build
- Flash to your device:
idf.py -p /dev/ttyUSB0 flash
(replace/dev/ttyUSB0
with your device’s port). - 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 config → ESP32-specific → SPI 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
- Heap Fallback: Modify the project configuration (
menuconfig
->Component config
->ESP32-specific
->SPI RAM config
) to enable “Allowmalloc()
to automatically fall back to SPI RAM”. Write a program that first allocates all available internal DRAM and then shows that a subsequentmalloc()
call successfully allocates memory from PSRAM. - 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. - 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 theMALLOC_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
- ESP-IDF SPI RAM Documentation: https://docs.espressif.com/projects/esp-idf/en/v5.2.1/esp32/api-reference/system/external_ram.html
- ESP-IDF Heap Memory Allocation: https://docs.espressif.com/projects/esp-idf/en/v5.2.1/esp32/api-reference/system/mem_alloc.html
- ESP32-S3 Technical Reference Manual (for deep details on Octal SPI): https://www.espressif.com/sites/default/files/documentation/esp32-s3_technical_reference_manual_en.pdf