Memory Types

ESP32-C3 chip has multiple memory types and flexible memory mapping features. This section describes how ESP-IDF uses these features by default.

ESP-IDF distinguishes between instruction memory bus (IRAM, IROM, RTC FAST memory) and data memory bus (DRAM, DROM). Instruction memory is executable, and can only be read or written via 4-byte aligned words. Data memory is not executable and can be accessed via individual byte operations. For more information about the different memory buses consult the ESP32-C3 Technical Reference Manual* > System and Memory [PDF].

DRAM (Data RAM)

Non-constant static data (.data) and zero-initialized data (.bss) is placed by the linker into Internal SRAM as data memory. Remaining space in this region is used for the runtime heap.

Note

The maximum statically allocated DRAM size is reduced by the IRAM (Instruction RAM) size of the compiled application. The available heap memory at runtime is reduced by the total static IRAM and DRAM usage of the application.

Constant data may also be placed into DRAM, for example if it is used in an non-flash-safe ISR (see explanation under How to place code in IRAM).

“noinit” DRAM

The macro __NOINIT_ATTR can be used as attribute to place data into .noinit section. The values placed into this section will not be initialized at startup and should keep its value after software restart.

Example:

__NOINIT_ATTR uint32_t noinit_data;

IRAM (Instruction RAM)

Note

Any internal SRAM which is not used for Instruction RAM will be made available as DRAM (Data RAM) for static data and dynamic allocation (heap).

Why place code in IRAM

Cases when parts of application should be placed into IRAM:

  • Interrupt handlers must be placed into IRAM if ESP_INTR_FLAG_IRAM is used when registering the interrupt handler. For more information, see IRAM-Safe Interrupt Handlers.

  • Some timing critical code may be placed into IRAM to reduce the penalty associated with loading the code from flash. ESP32-C3 reads code and data from flash via the MMU cache. In some cases, placing a function into IRAM may reduce delays caused by a cache miss and significantly improve that function’s performance.

How to place code in IRAM

Some code is automatically placed into the IRAM region using the linker script.

If some specific application code needs to be placed into IRAM, it can be done by using the Linker Script Generation feature and adding a linker script fragment file to your component that targets entire source files or functions with the noflash placement. See the Linker Script Generation docs for more information.

Alternatively, it’s possible to specify IRAM placement in the source code using the IRAM_ATTR macro:

#include "esp_attr.h"

void IRAM_ATTR gpio_isr_handler(void* arg)
{
        // ...
}

There are some possible issues with placement in IRAM, that may cause problems with IRAM-safe interrupt handlers:

  • Strings or constants inside an IRAM_ATTR function may not be placed in RAM automatically. It’s possible to use DRAM_ATTR attributes to mark these, or using the linker script method will cause these to be automatically placed correctly.

    void IRAM_ATTR gpio_isr_handler(void* arg)
    {
       const static DRAM_ATTR uint8_t INDEX_DATA[] = { 45, 33, 12, 0 };
       const static char *MSG = DRAM_STR("I am a string stored in RAM");
    }
    

    Note that knowing which data should be marked with DRAM_ATTR can be hard, the compiler will sometimes recognize that a variable or expression is constant (even if it is not marked const) and optimize it into flash, unless it is marked with DRAM_ATTR.

  • GCC optimizations that automatically generate jump tables or switch/case lookup tables place these tables in flash. There are two possible ways to resolve this issue:

    • Use a linker script fragment to mark the entire source file as noflash

    • Pass specific flags to the compiler to disable these optimizations in the relevant source files. For CMake, place the following in the component CMakeLists.txt file:

      set_source_files_properties("${CMAKE_CURRENT_LIST_DIR}/relative/path/to/file" PROPERTIES
                                  COMPILE_FLAGS "-fno-jump-tables -fno-tree-switch-conversion")
      

IROM (code executed from Flash)

If a function is not explicitly placed into IRAM (Instruction RAM) or RTC memory, it is placed into flash. The mechanism by which Flash MMU is used to allow code execution from flash is described in ESP32-C3 Technical Reference Manual > Memory Management and Protection Units (MMU, MPU) [PDF]. As IRAM is limited, most of an application’s binary code must be placed into IROM instead.

During Application Startup Flow, the bootloader (which runs from IRAM) configures the MMU flash cache to map the app’s instruction code region to the instruction space. Flash accessed via the MMU is cached using some internal SRAM and accessing cached flash data is as fast as accessing other types of internal memory.

RTC fast memory

The same region of RTC fast memory can be accessed as both instruction and data memory. Code which has to run after wake-up from deep sleep mode has to be placed into RTC memory. Please check detailed description in deep sleep documentation.

Remaining RTC fast memory is added to the heap unless the option CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP is disabled. This memory can be used interchangeably with DRAM (Data RAM), but is slightly slower to access.

DROM (data stored in Flash)

By default, constant data is placed by the linker into a region mapped to the MMU flash cache. This is the same as the IROM (code executed from Flash) section, but is for read-only data not executable code.

The only constant data not placed into into this memory type by default are literal constants which are embedded by the compiler into application code. These are placed as the surrounding function’s executable instructions.

The DRAM_ATTR attribute can be used to force constants from DROM into the DRAM (Data RAM) section (see above).

DMA Capable Requirement

Most peripheral DMA controllers (e.g. SPI, sdmmc, etc.) have requirements that sending/receiving buffers should be placed in DRAM and word-aligned. We suggest to place DMA buffers in static variables rather than in the stack. Use macro DMA_ATTR to declare global/local static variables like:

DMA_ATTR uint8_t buffer[]="I want to send something";

void app_main()
{
    // initialization code...
    spi_transaction_t temp = {
        .tx_buffer = buffer,
        .length = 8 * sizeof(buffer),
    };
    spi_device_transmit(spi, &temp);
    // other stuff
}

Or:

void app_main()
{
    DMA_ATTR static uint8_t buffer[] = "I want to send something";
    // initialization code...
    spi_transaction_t temp = {
        .tx_buffer = buffer,
        .length = 8 * sizeof(buffer),
    };
    spi_device_transmit(spi, &temp);
    // other stuff
}

It is also possible to allocate DMA-capable memory buffers dynamically by using the MALLOC_CAP_DMA capabilities flag.

DMA Buffer in the stack

Placing DMA buffers in the stack is possible but discouraged. If doing so, pay attention to the following:

  • Use macro WORD_ALIGNED_ATTR in functions before variables to place them in proper positions like:

    void app_main()
    {
        uint8_t stuff;
        WORD_ALIGNED_ATTR uint8_t buffer[] = "I want to send something";   //or the buffer will be placed right after stuff.
        // initialization code...
        spi_transaction_t temp = {
            .tx_buffer = buffer,
            .length = 8 * sizeof(buffer),
        };
        spi_device_transmit(spi, &temp);
        // other stuff
    }