ESP32: типы памяти Печать
Добавил(а) microsin   

У чипов ESP32 есть несколько типов памяти, отображаемые на разные области адресного пространства. В этой документации (перевод [1]) описывается, как ESP-IDF использует эти возможности по умолчанию.

Система программирования ESP-IDF [4] учитывает отдельные шины памяти: инструкций (IRAM, IROM, RTC FAST memory) и данных (DRAM, DROM). В памяти инструкций находится исполняемый код, и эта память может быть прочитана и записана только 32-битными словами данных, адрес которых выровнен на границу 4-байта (т. е. адрес слова делится нацело на 4). Память данных не исполняемая, и к ней можно обращаться через операции доступа к отдельным байтам. Дополнительную информацию по разным шинам данных см. в техническом руководстве на используемый чип ESP32, раздел "System and Memory" (PDF).

[DRAM (Data RAM)]

Изменяемые статические данные, не относящиеся к константам (.data) и данные, инициализируемые нулем (.bss) помещаются линкером во внутреннее ОЗУ (Internal SRAM), которое классифицируется ка память данных (data memory). Остальная часть памяти используется как куча, используемая во время работы приложения (runtime heap).

С помощью макроса EXT_RAM_BSS_ATTR инициализируемые нулем данные могут быть также помещены во внешнее ОЗУ (External RAM). Для использования этого макроса должна быть разрешена опция конфигурации CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY, см. "Allow .bss Segment to be Placed in External Memory" [5].

Если используется стек BlueTooth, то доступное пространство внутренней памяти данных DRAM уменьшается на 64 килобайта (сдвигом адреса начала на 0x3FFC0000). Размер этой области данных также дополнительно уменьшается на 16 или 32 килобайта, если используется память трассировки (trace memory). Из-за того, что ROM приводит к некоторым проблемам фрагментации памяти, не получится также использовать все доступное пространство DRAM для статических выделений памяти, однако остальная область DRAM все еще остается доступной для выделения памяти из кучи во время работы программы.

Данные констант также могут быть помещены в DRAM, например если они используются в коде обработчика прерывания, который требует безопасной и очень быстрой, прогнозируемой по времени обработки (non-flash-safe ISR, см. далее "Как поместить код в IRAM").

"noinit" DRAM. Макрос __NOINIT_ATTR может использоваться как атрибут для статического выделения памяти данных, которые не требуют автоматической инициализации. Эти данные размещаются в секции .noinit, они не инициализируются кодом startup и сохранят свое значение после перезапуска программного обеспечения (конечно, если перезапуск не был вызван пропаданием питания).

С помощью макроса EXT_RAM_NOINIT_ATTR не инициализируемые данные могут быть также размещены во внешней памяти (External RAM). Для этого должна быть разрешена опция конфигурации CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY, см. "Allow .noinit Segment to be Placed in External Memory" [5]. Если опция CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY не разрешена, то атрибут EXT_RAM_NOINIT_ATTR будет вести себя просто как атрибут __NOINIT_ATTR, и разместит данные в сегмент .noinit внутреннего ОЗУ (Internal RAM).

Пример:

__NOINIT_ATTR uint32_t noinit_data;

[IRAM (Instruction RAM)]

ESP-IDF выделяет часть внутреннего ОЗУ (область Internal SRAM0) для инструкций. Этот регион описан в техническом руководстве ESP32, раздел "System and Memory -> Embedded Memory" (PDF). Кроме блока первых 64 килобайт, который используется для PRO и кешей APP MMU, остальная часть этого диапазона памяти (например от 0x40080000 до 0x400A0000) используется для сохранения тех частей кода приложения, которые должны работать из RAM.

Необходимость поместить код во внутреннее ОЗУ обусловлена тем, что другие области памяти программ (например память SPI Flash, откуда аппаратура кристалла ESP32 SoC прозрачно подгружает код программы) при обращении к ним могут вводить непрогнозируемые задержки. Т. е. если инструкции уже были загружены в кэш, то эти инструкции будут выполнены без задержки, иначе выполнение будет приостановлено на время загрузки инструкций из Flash через SPI. Для кода, который требует заранее прогнозируемого и максимально быстрого выполнения (например ISR таймера, который оцифровывает звуковой сигнал), такие непрогнозируемые задержки недопустимы, и для такого кода необходимо размещение во внутреннее ОЗУ (IRAM).

Случаи, когда части приложения должны быть помещены в IRAM:

● Обработчики прерывания должны быть помещены в IRAM, если при регистрации обработчика использовался атрибут ESP_INTR_FLAG_IRAM. Для дополнительной информации см. раздел "IRAM-Safe обработчики прерывания" статьи [6].
● Критический по времени выполнения код, чтобы устранить затраты времени на загрузку кода из Flash. ESP32 читает код и данные через кэш MMU. В некоторых случаях размещение кода в IRAM может уменьшить задержки, вызванные промахами кэша, что значительно улучшит быстродействие кода.

Как поместить код в IRAM. Некоторые части кода автоматически помещаются в регион IRAM с помощью скрипта настроек линкера.

Если какой-либо код приложения нужно поместить в IRAM, то это можно сделать с помощью функции Linker Script Generation [7], с добавлением фрагмента файла настроек линкера в ваш компонент, который нацелен на целый файл исходного кода или отдельные функции, которые не должны попасть в Flash.

Альтернативно можно указать размещение кода в IRAM с помощью макроса IRAM_ATTR:

#include "esp_attr.h"
 
void IRAM_ATTR gpio_isr_handler(void* arg)
{
   // ...
}

При размещении кода в IRAM могут возникнуть некоторые проблемы в обработчиках прерывания (IRAM-safe interrupt handlers):

● Строки или константы внутри функции с атрибутом IRAM_ATTR не могут быть автоматически помещены в RAM. Для решения этой проблемы такие данные могут быть помечены атрибутом DRAM_ATTR, или можно использовать метод скрипта линкера, чтобы данные строк и констант были автоматически и корректно размещены в RAM.

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("Это строка, которая находится в ОЗУ");
}

Следует помнить, что компилятору GCC трудно понять, какие данные должны быть помещены в ОЗУ, и он может выполнить "оптимизацию", разместив данные во Flash, если ему явно не указать, куда следует поместить данные с помощью атрибута DRAM_ATTR.

● Оптимизации GCC, которые автоматически генерируют таблицы перехода (jump tables) или таблицы перехода switch/case (lookup tables), поместят эти таблицы в память Flash. Чтобы обойти эту проблему, ESP-IDF по умолчанию собирает все модули кода с флагами компиляции -fno-jump-tables-fno-tree-switch-conversion.

Оптимизации для таблиц переходов можно вернуть обратно для некоторых модулей исходного кода, который не нужно размещать в IRAM. Руководство, как добавить опции -fno-jump-tables -fno-tree-switch-conversion для компиляции отдельных файлов, см. в разделе "Управление компиляцией компонента" статьи [8].

[IROM (код, который выполняется из Flash)]

Если функция не должна быть явно размещена в IRAM (Instruction RAM) или памяти RTC [9], то она будет помещена в Flash. Поскольку пространство IRAM ограничено, то обычно почти весь код приложения помещается в IROM.

Механизм, которым контроллер Flash MMU дает возможность коду выполняться из памяти SPI Flash, описывается в техническом руководстве ESP32 (PDF), раздел "Memory Management and Protection Units (MMU, MPU)".

По время процесса запуска приложения [10] загрузчик (bootloader, который работает из IRAM) конфигурирует кэш MMU Flash, чтобы отобразить регион кода инструкций приложения на адресное пространство, выделенное для инструкций. Данные кода Flash, которые доступны через блок MMU, кэшируются в некоторой внутренней памяти SRAM, и к данным в кэше доступ получается такой же быстрый, как и доступ к другим типам внутренней памяти.

[DROM (данные, сохраненные в Flash)]

По умолчанию данные констант помещаются линкером в регион памяти, отображенный кэшем MMU Flash. Обращение к такой памяти происходит так же, как и к секции IROM (код, исполняемый из Flash), однако данные только для чтения не являются исполняемым кодом.

Единственными постоянными данными, не помещенными в этот тип памяти по умолчанию, являются литеральные константы, которые встраиваются компилятором в код приложения. Они размещаются в виде исполняемых команд окружающей функции.

Может использоваться атрибут DRAM_ATTR (см. описание выше), чтобы принудительно переместить константы из секции DROM в секцию DRAM (Data RAM).

[Память RTC]

RTC Slow memory. Глобальные и статические переменные, используемые кодом, который работает из памяти RTC, должны быть размещены в память RTC Slow [9]. Например, переменные deep sleep могут быть размещены в память RTC Slow вместо памяти RTC FAST, или в RTC Slow может быть размещен код или переменные, к которым обращается сопроцессор режима ультранизкого энергопотребления ULP [11].

Для размещения данных в этот тип памяти может использоваться макрос RTC_NOINIT_ATTR. Значения, которые помещены в эту секцию, сохранят свое значение при выходе процессора из режима глубокого сна (waking from deep sleep).

Пример:

RTC_NOINIT_ATTR uint32_t rtc_noinit_data;

RTC FAST memory. Тот же самый регион RTC FAST memory может быть доступен как память для инструкций, так и как память для данных. Код, который должен выполняться при выходе из deep sleep, должен быть размещен в память RTC. Подробную документацию по режиму глубокого сна и соответствующим подпрограммам см. в документации [12].

К памяти RTC FAST можно обращаться только через PRO CPU (что такое PRO CPU и APP CPU см. [10]).

В режиме одного ядра (single core mode) остальная часть памяти RTC FAST добавляется к куче, если не запрещена опция CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP. Эта память RTC FAST может использоваться взаимозаменяемо с DRAM (Data RAM), однако RTC FAST несколько медленнее для доступа, потому что не поддерживается DMA.

[Требования к памяти для DMA]

Большинство контроллеров периферийных устройств, поддерживающих DMA (например SPI, SDMMC, и т. п.) требуют наличие буферов, которые размещены в DRAM, и у которых начальный адрес и размер выровнены на размер слова 32 бита (байтовый адрес и байтовый размер нацело делятся на 4). Рекомендуется размещать буферы DMA в статических переменных вместо того, чтобы помещать их в стеке. Для декларирования таких глобальных или статических локальных переменных с помощью макроса атрибута DMA_ATTR, например:

DMA_ATTR uint8_t buffer[]="Я хочу что-то там передать";
 
void app_main()
{
   // Код инициализации
   ...
   spi_transaction_t temp = {
        .tx_buffer = buffer,
        .length = 8 * sizeof(buffer),
   };
   spi_device_transmit(spi, &temp);
   // Другой код
   ...
}

Или можно использовать этот атрибут для определения переменной в теле функции:

void app_main()
{
   DMA_ATTR static uint8_t buffer[] = "Я хочу что-то передать";
   // Код инициализации
   ...
   spi_transaction_t temp = {
       .tx_buffer = buffer,
       .length = 8 * sizeof(buffer),
   };
   spi_device_transmit(spi, &temp);
   // Другой код
   ...
}

Можно также динамически выделять память, поддерживающую DMA, с помощью флага MALLOC_CAP_DMA.

Буфер DMA в стеке. Размещение буферов DMA в стеке возможно, но не рекомендуется. Если вы поступаете таким образом, то имейте в виду следующее:

● Размещение буферов DRAM в стеке не рекомендуется, если стек может находиться в PSRAM. Если стек потока (task) размещен в PSRAM, то необходимо предпринять несколько шагов, описанных в документации по поддержке внешней оперативной памяти [5].

● Используйте перед переменными макрос WORD_ALIGNED_ATTR в функциях, чтобы поместить переменные в правильных позициях, например:

void app_main()
{
   uint8_t stuff;
   WORD_ALIGNED_ATTR uint8_t buffer[] = "Я хочу что-то передать";
   // Код инициализации
   ...
   spi_transaction_t temp = {
        .tx_buffer = buffer,
        .length = 8 * sizeof(buffer),
   };
   spi_device_transmit(spi, &temp);
   // Другой код
   ...
}

[Ссылки]

1. ESP32 Memory Types site:docs.espressif.com.
2. ESP32 Programmers’ Memory Model site:blog.espressif.com.
3. ESP32-C3: выделение памяти из кучи.
4. Установка среды разработки ESP-IDF для ESP32.
5. ESP32 Support for External RAM site:docs.espressif.com.
6. ESP32-C3: выделение прерывания.
7. ESP-IDF Linker Script Generation site:docs.espressif.com.
8. ESP-IDF Build System.
9. ESP32: устройство системы и памяти.
10. ESP32: процесс запуска приложения.
11. ESP32: программирование сопроцессора ULP.
12. ESP32 Deep Sleep Wake Stubs site:docs.espressif.com.