Перейти к содержимому

Добрый день, уважаемый читатель!

В этой статье поговорим о использовании оперативной памяти ( RAM ) на микроконтроллерах ESP32 для платформы ESP-IDF. Впрочем все то же самое справедливо и для фреймворка Arduino. Если вас больше интересует информация об использовании и разметке FLASH-памяти ESP32, то вам стоит прочесть другую статью.

Картинка из свободного доступа, в качестве иллюстрации

Приложения ESP32 используют общие шаблоны компьютерной архитектуры:

  • стекдинамическая память, автоматически выделяемая потоком управления программой,
  • heap или кучадинамическая память, выделяемая вызовами функций malloc и calloc,
  • статическая памятьпамять, выделяемая под переменные во время компиляции.

При этом сам микроконтроллер ESP32 имеет на борту несколько типов оперативной памяти ( RAM ):

  • DRAM (ОЗУ данных) — это память, которая подключена к шине данных ЦП и используется для хранения данных. Это наиболее распространенный вид памяти, доступ к которому осуществляется в виде кучи и стека. При запуске куча DRAM содержит всю память данных, которая не выделена приложением статически. Уменьшение статически выделяемых буферов увеличивает объем доступной свободной кучи и наоборот.
  • IRAM (ОЗУ инструкций) — это память, которая подключена к шине инструкций ЦП и обычно содержит только исполняемые данные (т. е. инструкции, или скомпилированная программа). Важно понимать: в этой части оперативной памяти находится не вся ваша программа, а только отдельные функции, помещенные атрибутом IRAM_ATTR, а весь остальной код находится во flash-памяти и вызывается прямо оттуда -подробнее см. ниже. Однако её в некоторых случаях можно использовать и в качестве обычной памяти. Если доступ к IRAM осуществляется как доступ к общей памяти, все обращения должны быть в  32-битных единицах
  • D/IRAM — это ОЗУ, которое подключено к шине данных ЦП и шине инструкций, поэтому может использоваться как ОЗУ инструкций, так и ОЗУ данных. 
  • Также к ESP32 можно подключить внешнюю SPI RAM. Внешняя оперативная память интегрирована в карту памяти ESP32 через кеш в DRAM, и доступ к ней осуществляется аналогично DRAM. Такая память установлена в модулях серии ESP32-WROVER и на плате ESP32-CAM.

Поскольку ESP32 использует несколько типов оперативной памяти, он также содержит несколько отдельных куч с разными возможностями. Распределитель памяти на основе запрошенных возможностей позволяет приложениям выделять кучу для различных целей. В большинстве случаев функции выделения памяти malloc() и calloc() стандартной библиотеки C можно использовать для выделения блоков памяти в куче без каких-либо особых соображений. Однако, чтобы в полной мере использовать все типы памяти и их характеристики, ESP-IDF также имеет возможность распределять память кучи на основе запрошенных возможностей MALLOC_CAP_XXX. Если вы хотите получить блок памяти с заранее определенными свойствами (например, память с поддержкой DMA или память во внешнем чипе), вы должны будете создать ИЛИ-маску необходимых возможностей MALLOC_CAP_XXX и передать ее в malloc() или calloc().

Поскольку ESP-IDF (и Arduino ESP32 тоже!) представляет собой многопоточную среду FreeRTOS, каждая задача RTOS имеет свой собственный стек. По умолчанию каждый из этих стеков выделяется из кучи при создании задачи (но возможно использование альтернативной функции, в которой стеки распределяются статически – подробнее об этом изложено в другой статье).

 


Использование динамической памяти в проектах ESP32

Если вы посещали форумы для начинающих arduin-щиков, то наверняка сталкивались с рекомендациями не использовать для хранения изменяемых строк динамическую память. Аргументировано это, как правило, тем, что на микроконтроллерах “сборка мусора” отсутствует как класс, а частое выделение и освобождение небольших блоков памяти приводит к её быстрой фрагментации (когда небольшие свободные блоки памяти густо перемешаны с занятыми блоками памяти, что делает невозможным выделение больших по размеру блоков памяти). В итоге очень часто в примерах для работы со строками используются заранее выделенные (на этапе компиляции) статические буферы заведомо большего размера. На самом деле это отчасти правильно – это и значительно быстрее (не тратится процессорное время на выделение и освобождение памяти) и не приводит к фрагментации кучи.

Но с ростом сложности программы это становится проблемой – строк и массивов ( с заранее неизвестной длиной ) становится столь много, что выделять под каждый значительного размера буфер становится нереальным. Поясню на примере. Допустим, нам требуется отправить в mqtt сообщение. Большая часть сообщений будет занимать 10-20 символов. Но иногда требуется отправить сообщение и значительно большего размера, когда генерируется большое и сложный json-пакет, например длиной 3-4 килобайта. И если под буфер отправки выделить статический буфер не проблема, то “собрать из кусочков” такой JSON без использования кучи становится нереально – понадобится несколько десятков таких буферов с заранее неизвестным большим размером. Которые, к тому же, будут реально использоваться очень редко, а статически выделенная память выделена все время. Если мы попытаемся так сделать, то свободная оперативная память очень-очень быстро закончится под такие вот буферы. А с динамическим выделением память расходуется гораздо более экономно. Нужно только очень внимательно следить за освобождением выделенной памяти, иначе будут возникать так называемые утечки памяти. При этом желательно освобождать память в обратном порядке выделению, чтобы избежать ее фрагментации (не всегда это возможно, но нужно стараться).

На самом деле ESP-IDF почти везде использует динамическое выделение памяти под различные данные. И мои прошивки в большинстве своем используют также динамическое выделение памяти. И при этом устройства работают месяцами без сбоев и перезагрузок. Это говорит о том, что динамическое выделение памяти в общем-то не так и страшно.

Но в некоторых случаях я всё-таки предпочитаю использовать статическое выделение памяти, когда это возможно и оправдано. Например: почти все задачи, очереди и другие объекты FreeRTOS я предпочитаю выделять статически. А смысл выделять их динамически? Они запускаются один раз при старте системы и работают постоянно. Буферы, которые используются постоянно, тоже лучше выделить статически. В общем, нужно подходить к методу выделения памяти вдумчиво, в зависимости от поставленной задачи.

 


Особенности использования RAM на ESP32

Во первых, всегда следует помнить, что ESP32 всегда использует FreeRTOS. То есть какую бы IDE вы не запускали для программирования ESP32, какую бы платформу не использовали – Arduino, MicroPython или ESP-IDF – FreeRTOS будет запускаться поверх вашего кода. Ещё раз (почему-то для многих это загвоздка) – практически все платформы (фреймворки), с помощью которых можно программировать ESP32, в том или ином виде используют FreeRTOS. Так задумано разработчиками микроконтроллера. Это нельзя отключить или отменить по вашему желанию! (Прим. автора: наверное все-таки можно, но очень сложно – напишите свой загрузчик, свою систему управления flash, свои драйвера, api и т.д.(явно не для новичков).) И это очень даже не плохо, лично на мой взгляд.

Даже если вы программируете Python – интерпретатор MicroPython написан на FreeRTOS. Даже если вы в примитивной Arduino IDE запустили простейший скетч “Hello world” – используется FreeRTOS! Отличие только одно – ESP-IDF использует FreeRTOS явно и в полной мере, а Arduino32 – делает это неявно, но всё равно этот фреймворк точно также основан на FreeRTOS!

Что это значит на практике? А это значит, что даже простейший скетч “Hello world” после компиляции будет неприлично много “весить”, а при запуске занимать довольно много оперативной памяти.

А все потому, что ваш скетч “Hello world” – это не одинокая программа для вашего микроконтроллера, а всего лишь одна из задач, запускаемых на одном из двух его ядер (если у вас двухядерный вариант, конечно). А помимо этого на этом самом контроллере будут запущены:

  • собственно сама FreeRTOS, то есть планировщик задач и ядро
  • WDT или сторожевой таймер, контролирующий остальные задачи
  • службы обмена данными между ядрами
  • служба программных таймеров FreeRTOS и (или) EDP-IDF
  • службы IDLE-процессов
  • возможно что-то ещё …
  • ваш маленький скетчик

Как видите, – ваш скетч – далеко не единственное, что скомпилировано и выполняется на вашей ESP32. Отсюда и накладные расходы. Будьте готовы к расходу fllash и RAM!

Отсюда же можно сделать вывод – что ESP32 явно разработан не для небольших проектов, для которых не требуется RTOS, подключение к сети и BT. Для всего этого есть другие микроконтроллеры, хорошие и более оптимальные. Если вам совсем не нужен WiFi или BT, и вам очень не хочется разбираться с FreeRTOS, то вероятно, вам не стоит связываться с ESP32. Вообще. В принципе. Ну а если вы готовы к этому – идем дальше.

Если обратиться к спецификациям на ESP32, то можно увидеть, что данный микроконтроллер имеет несколько разных типов оперативной памяти ( RAM ). Например для модуля ESP32-WROOM-32D/U ( ESP32-D0WD-V3 ) это выглядит примерно так:

  • 520 КБ встроенной SRAM для данных и инструкций
  • 8 КБ SRAM в RTC, которая называется RTC FAST Memory и может использоваться основным процессором во время загрузки RTC из режима глубокого сна
  • 8 КБ SRAM в RTC, которая называется RTC SLOW Memory и может быть доступна только сопроцессору ULP в режиме глубокого сна
  • 1 Кбит eFuse: 256 бит используются для системы (MAC-адрес и конфигурация чипа), а остальные 768 бит зарезервированы для клиентских приложений, включая флэш-шифрование и идентификатор чипа

Некоторые модули, например ESP32-WROVER-IE (ESP32-D0WD-V3), имеют также некоторое количество “внешней” оперативной памяти (и FLASH), подключенной через интерфейс QSPI, которая поэтому иногда так и называется – SPIRAM. Но она отображается в адресное пространство CPU не вся сразу, а постранично, через буфер; поэтому доступ к ней происходит заметно медленнее, чем к основной памяти. Об тонкостях своих “танцев с бубном и ESP32-WROVER” я планирую рассказать позже, если не забуду.

Для большинства приложений, мы можем использовать только первый тип RAM – 520 КБ встроенной SRAM. Казалось бы: 520 КБ это достаточно много, хватит на всё ( где-то это мы уже слышали, ага ). Но, если вы захотите проверить, сколько памяти вам доступно из вашей программы, то обнаружите, что общий размер доступной памяти значительно меньше, порядка 270~300 КБ:

"heap_kb": {
  "total": 271.6, 
  "errors": 0, 
  "free": 151.7, 
  "free_percents": 55.9, 
  "free_min": 112.5, 
  "free_min_percents": 41.4
}

Это именно общий размер памяти MEMORY_CAP_DEFAULT (тип памяти при вызове malloc(), calloc()), в том числе выделяемый и для нужд FreeRTOS. Куда делись около 200 КБ памяти!!!??? Дело в том, что общий блок памяти 520 КБ делится на два (источники: тыц и тыц):

  • IRAM 192 КБ – Instruction RAM (сегмент кода, также называемый текстовым сегментом, в котором скомпилированная программа находится в памяти)
  • DRAM 328 КБ – Data RAM (используется для BSS, данных, кучи)

То есть на самом деле для размещения данных доступно только 328 КБ. DRAM распределяется следующим образом:

  • Первые 8 КБ используются в качестве памяти данных для некоторых функций ПЗУ
  • Затем компоновщик помещает инициализированный сегмент данных после этой первой памяти объемом 8 КБ
  • Далее следуют сегмент BSS («block started by symbol», также называемый сегментом неинициализированных данных, где хранятся глобальные и статические переменные с нулевой инициализацией)
  • Сегмент данных (также называемый сегментом инициализированных данных, где хранятся инициализированные глобальные и статические переменные)
  • Память, оставшаяся после выделения данных и сегментов BSS, настроена на использование в виде кучи. Вот где обычно происходит динамическое распределение памяти
  • Стек вызовов, в котором хранятся параметры функций, локальные переменные и другая информация, относящаяся к функциям

Картинка из свободного доступа

Обратите внимание, что размер сегментов данных и BSS зависит от приложения. Но, насколько я понимаю, описанная структура памяти не является какой-то особенностью ESP32, это свойственно вообще любым программам. Из-за технических ограничений ESP32 максимальное использование статически выделенной памяти BSS составляет 160 КБ. Остальное отдается под общую кучу. Во время выполнения доступная динамическая память кучи может быть меньше, чем рассчитано во время компиляции, поскольку при запуске часть памяти выделяется из кучи до запуска планировщика FreeRTOS (включая память для стеков начальных задач FreeRTOS).

Таким образом, каждое приложение, в зависимости от используемых им компонентов и вызываемых им API, изначально имеет разный доступный размер кучи. В моей прошивке (с учетом примерно 8~10 статически выделяемых задач) это обычно что-то около 270 КБ.

 


Куда же попадет ваша переменная после компиляции?

Куда же попадет ваша переменная после компиляции?

  • Локальная переменная, например самый простой int внутри какой-нибудь функции или задачи, попадет в стек – общий (если это однопоточный Arduino) или стек задачи (если эта переменная внутри задачи FreeRTOS). Но её преимущество – она будет автоматически уничтожена сразу после того, как покинет область видимости, то есть станет не нужна. И на её место может спокойно лечь другая такая же переменная, что есть хорошо.
  • Глобальная переменная или помеченная как static – попадёт либо в сегмент BSS, либо в сегмент инициализированных данных (что, в общем-то без особой разницы, они находятся рядышком) и будет там находится постоянно. Основное преимущество – они находятся “ниже” общей кучи и не вносят вклад в её фрагментацию. Но если бы будете использовать глобальные или статические переменные в программе всего пару раз – то это и есть абсолютное зло, так как место под них всё остальное время будет занято впустую.
  • Динамические переменные, то есть размещаемые в памяти с помощью malloc() или calloc(), расположены в общей куче (heap). Для минимизации фрагментации памяти желательно, чтобы такие переменные “жили” как можно меньше по времени. Чем дольше по времени используется динамическая переменная, тем больший вклад в фрагментацию памяти она может вносить (но может и не вносить, если она расположена в самом начале кучи, например).

В общем думайте, прежде чем создать переменную, как вы будете её использовать в своей программе. Бездумное назначение переменных в некоторых случаях может приводить к проблемам в работе программы.

 


Выделение стека для задач FreeRTOS

Переполнение стека – одна из проблем, с которыми сталкиваются пользователи, которые только начали осваивать FreeRTOS. Дело в том, что FreeRTOS требует выделение отдельного стека вызовов для каждой задачи, которую вы создаете.

Стек – это динамическая память , выделяемая потоком управления программой. Стек вызовов отслеживает все активные функции (те, которые были вызваны, но еще не завершились) от начала программы (задачи) до текущей точки выполнения и обрабатывает размещение всех параметров функций и локальных переменных.

Каждая задача FreeRTOS содержит собственный стек, общий размер которого указывается при создании задачи. Стек задачи выдается из общего пула памяти кучи. Создали десять задач – будьте добреньки выделить десять блоков памяти из общей кучи под стек.

Этот момент является основным поводом для критики со стороны программистов Arduino, не желающих разбираться с FreeRTOS. Я очень часто встречал подобные комментарии на различных форумах.

Как подобрать размер стека для задачи? Если выделить слишком большой стек для задачи – останется меньше памяти под кучу и для других задач. Если выделить слишком маленький стек – в какой-то момент времени (не обязательно сразу) мы получим перезагрузку контроллера из-за переполнения стека. Переполнение стека (stack overflow) наиболее частая причина нестабильности приложения.

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

Espressif рекомендует не делать размер стека меньше чем 2048 байт, но у меня некоторые из задач (например “пищалка” и “мигалка светодиодами” стабильно работают при размере стека всего в 1024 байт (1КБ).

Как уменьшить размер используемого стека?

При использовании стандартных библиотечных функций Cи стек может использоваться очень интенсивно, особенно при вводе/выводе и поддержке строковых функций, таких как sprintf(). Но при размещении данных в областях памяти, выделяемых динамически с помощью функций malloc() и calloc(), стек задачи не используется – в этом случае данные размещаются в общей куче (и это может использоваться при передаче данных между разными задачами).

 

Кроме того, стек достаточно интенсивно используется при вызове функций внутри задачи, когда вызываемой функции передается большое количество “тяжёлых” параметров. Все эти параметры, включая адрес самой функции, размещаются в стеке. Исходя из этого при необходимости передачи большого количества переменных в какую-либо функцию гораздо лучше объявить структуру данных при помощи typedef struct, объявить переменную-указатель, выделить под нее память в куче, заполнить ее данными и только после этого передать в функцию указатель на переменную. В этом случае в стек вызовов попадет только указатель 32 бит / 4 байта. Если размер параметров функции не превышает 4 байт, то никакого смысла это не имеет.

Возьмем пример одной из библиотечных функции ESP-IDF:

esp_err_t esp_mqtt_set_config(esp_mqtt_client_handle_t client, const esp_mqtt_client_config_t *config)

где *config – указатель на довольно громоздкую структуру esp_mqtt_client_config_t.

В этом случае вызов данной функции происходит гораздо экономнее в плане стека, чем если бы мы “затолкали” все данные из структуры esp_mqtt_client_config_t непосредственно в стек.

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

 

Избегайте размещения больших переменных в стеке. В си любая большая структура или массив, выделенные как «автоматическая» переменная (т. е. область действия объявления C по умолчанию), будут использовать пространство в стеке. Минимизируйте их размеры, выделяйте их статически и/или посмотрите, сможете ли вы сэкономить память, выделяя их из кучи только тогда, когда они необходимы. На примере той же структуры esp_mqtt_client_config_t рассмотрим, куда попадает переменная в зависимости от того, как мы ее объявили.

1. Если объявить esp_mqtt_client_config_t как локальную переменную:
esp_mqtt_client_config_t mqtt_config;
mqtt_config.host = "m1.wqtt.ru"
mqtt_config.port = 8888;
....
esp_mqtt_set_config (&client, &mqtt_config);

В этом случае переменная mqtt_config попадет в стек задачи, из которой мы выполнили вызов этого кода. Но после того, как она уйдет из области видимости, она будет автоматически удалена компилятором из стека. Вполне допустимый вариант, хотя и не оптимальный.

2. Если объявить esp_mqtt_client_config_t как статическую переменную:
static esp_mqtt_client_config_t mqtt_config;
mqtt_config.host = "m1.wqtt.ru"
mqtt_config.port = 8888;
....
esp_mqtt_set_config (&client, &mqtt_config);

В этом случае переменная mqtt_config попадает в сегмент BSS и хранится там всегда. На мой взгляд это очень неразумное расходование памяти. Хотя стек не пострадает.

3. Если объявить esp_mqtt_client_config_t как статическую переменную с инициализацией:
static esp_mqtt_client_config_t mqtt_config = {
.host = "m1.wqtt.ru",
.port = 8888,
};
esp_mqtt_set_config (&client, &mqtt_config);

В этом случае переменная mqtt_config попадает в сегмент данных (после BSS) и хранится там всегда. Тоже не лучше.

4. Самый сложный (с точки зрения программиста) вариант:
esp_mqtt_client_config_t* mqtt_config;
mqtt_config = (esp_mqtt_client_config_t*)calloc(1, sizeof(esp_mqtt_client_config_t));
mqtt_config->host = "m1.wqtt.ru"
mqtt_config->port = 8888;
....
esp_mqtt_set_config (&client, mqtt_config);
free(mqtt_config);

В этом случае стек задачи для хранения данных конфигурации не используется, а переменную нужно выделить в куче “вручную” и не забыть её удалить, когда она не нужна. На мой взгляд, это самый правильный вариант.

 

Если вы будете использовать TLS-соединения, то следует учесть, что ESP32 требует достаточно много памяти при установке соединения. По умолчанию ESP-IDF настроена так, что память под буферы TLS выделяется статически. Если перенастроить ESP_IDF на использование динамических буферов TLS, то можно существенно сэкономить на свободной куче: что-то около 16-22 КБ. Но при этом мы немного потеряем в скорости соединения и, возможно, немного увеличим фрагментацию кучи. Более подробно я об этом расскажу в соответствующей статье.

 


Фрагментация кучи и борьба с ней

Очень часто можно услышать мнение, что использование динамического выделения памяти из кучи на микроконтроллерах – абсолютное зло, ибо фрагментация кучи все испортит и мы все умрем. На самом деле, есть такая проблема, но “не так страшен чёрт как его малюют”. Можно спорить много и бесконечно, но мой опыт говорит о том, что причина фрагментации кроется чаще в неграмотном использовании этой самой памяти.

Допустим, мы сделали следующие шаги: разместили в куче несколько переменных – A, B, C, D. Затем запустили какую-либо задачу с выделением стека для неё из общей кучи. И в конце разместили в куче ещё парочку переменных. У нас получится (весьма условно, конечно) примерно такая картина:

Теперь, если нам понадобится удалить переменную B, мы получим маленькую “дырку” в области памяти:

Если после этого ваша программа попытается заново выделить память размером в 4 “условных экселевских блока“, то менеджер памяти легко может отдать эту свободную область. А если нужно больше памяти? Тогда это место вроде бы и свободно, но использовать его не получится.

Другой вариант: для новой переменной нам требуется уже не 4 блока, а только 3. В этом случае снова получим свободный “хвостик”, который никуда не получится использовать:

То же самое может произойти и с переменной E, если она будет освобождена / удалена, а переменная F – останется.

В итоге может случиться такая ситуация, что свободных блоков много, но они сильно раздроблены, и выделить один большой непрерывный блок не представляется возможным.

На самом деле не все так страшно. Даже взять ту же ESP-IDF – внутри стандартный библиотек динамическая память используется очень активно. Покопайтесь в исходниках, благо они открытые. Если бы проблема с фрагментацией действительно была бы столь катастрофической, я думаю, в Espressif давно озаботились бы этим. Но ничего…

Как бороться с фрагментацией?

Всего парочка достаточно очевидных советов:

Всегда старайтесь размещать постоянно используемые переменные и объекты как статические. То есть задачи, очереди, буферы, да вообще любые переменные, которые создаются при запуске программы и используется бесконечно долго. Тем более, что во FreeRTOS для очень многих объектов предусмотрено статическое выделение памяти.

Ещё раз – если вы при написании программы понимаете, что та или иная переменная (даже строка), будет нужна с момента старта и постоянно – её стоит пометить как static. Это заставит компилятор поместить её не в кучу или стек, а в секцию BSS, и она не будет вносить вклад в общую фрагментацию.

И наоборот, если переменная “живет” относительно недолго и используется нечасто – ей самый прямой путь в кучу (как это было описано выше). Ну или стандартный способ – в стек (обычные локальные переменные).

Никогда не забывайте удалять короткоживущие динамические объекты после использования – строки, параметры конфигурации и т.д. Иначе вы получите не только утечку памяти, но и увеличите её фрагментацию.

 


Практические выводы

На основании своего личного опыта, что выделение отдельного стека для каждой задачи – не проблема. Да, имеется небольшой “геморрой” на старте, пока вы подбираете его размер. Но в дальнейшем никаких проблем обычно не наблюдается (если вы в коде не накосячите, что вероятнее). В качестве примера могу привести список задач на одном из моих устройств (охранно-пожарная сигнализация, телеметрия температуры в доме и управление котлом):

mqtt_task (ядро 1), stack size ???, stack_minimum 1412
led_system (ядро 1), stack size 1024, stack_minimum 452
led_alarm (ядро 1), stack size 1024, stack_minimum 528
flasher (ядро 1), stack size 1024, stack_minimum 560
buzzer (ядро 1), stack size 1024, stack_minimum 488
siren (ядро 1), stack size 1024, stack_minimum 568
sensors (ядро 1), stack size 4096, stack_minimum 1736
alarm (ядро 1), stack size 4096, stack_minimum 2404
http_send (ядро 1), stack size 3584, stack_minimum 716
pinger (ядро 1), stack size 3072, stack_minimum 1024
tg_send (ядро 1), stack size 3584, stack_minimum 771
wifi (ядро 0), stack size ???, stack_minimum 4588

Я опустил из списка некоторые системные задачи, которые запускаются автоматически, вроде IDLE и IPC, и на которые я не могу повлиять.

Свободная память в этом случае – чуть менее 50%. Устройство стабильно работает долгое время без перезапусков.

График свободной памяти на устройстве


 


А что с PROGMEM?

Если вы программировали на Arduino, то наверняка сталкивались с макросами препроцессора PROGMEM, PSTR и F, которые заставляют компоновщик изменять место хранения переменной в программе. Например PROGMEM помещает переменную во FLASH память. А что с ними на ESP32?

А на ESP32 макросы PROGMEM, PSTR и F отсутствуют!

То есть просто отсутствуют. Они нигде не встречаются в исходном коде ESP-IDF и в документации. 

Комментарий от Дмитрия:

В avr-gcc все данные (в том числе и static const) по умолчанию размещаются в SRAM. А так как размер RAM в avr очень маленький, то использовался атрибут PROGMEM, который позволял разместить static const данные во flash (но при этом при непосредственном использовании все равно происходило копирование в SRAM – pgm_read….).
В ESP32 весь код по умолчанию размещается уже в external flash, кроме того в external flash размещаются все static const данные (при этом обращение к этим данным происходит непосредственно без всяких pgm_read), и таким образом атрибут PROGMEM становится ненужным.

Однако возникает противоположная проблема – в определенных режимах (например в обработчиках прерываний и т.д.) external flash недоступен, поэтому код, который работает в таких режимах должен быть размещен в SRAM (конкретно в IRAM). Для этого используется атрибут IRAM_ATTR. Код функций помеченный этим аттрибутом, будет размещен в IRAM. Однако размещение кода в IRAM не гарантирует также и размещение данных в RAM, т.е. например если внутри функции помеченной атрибутом IRAM_ATTR у нас будет static const массив, то этот массив компилятор все равно сможет разместить в external flash. Для того чтобы этого избежать и явно указать компилятору что static const данные нужно тоже размещать в RAM (конкретно в DRAM) используется атрибут DRAM_ATTR.

Все доступные для ESP32 атрибуты размещены в файле esp_attr.h – там вы сможете найти много интересных и полезных макросов.

 


Полезные ссылки

Как всегда, в заключении привожу список полезных ссылок и источников информации для статьи:

  1. ESP-IDF :: Heap Memory Allocation
  2. ESP-IDF :: Heap Memory Debugging
  3. ESP-IDF :: Minimizing RAM Usage
  4. ESP-IDF :: Minimizing Binary Size
  5. Mahavir Jain :: ESP32 Memory Analysis — Case Study
  6. Amey Inamdar :: ESP32 Programmers’ Memory Model
  7. Канал на Дзене
  8. Мои репозитории

На этом пока всё, до встречи на сайте и на telegram-канале! 

💠 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 6 оценок ]

10 комментариев для “Распределение памяти в ESP32”

  1. Александр

    Александр, огромное спасибо Вам за проделанную работу! Собрать воедино информацию из оф. документации, форумов и гита можно, но, не имея достаточного опыта, сложно. Зачастую и правильно понять перевод сложно. Управление памятью – это вообще “переход на следующий уровень”. Но больше всего, спасибо за то, что делитесь своим опытом. С удовольствием подпишусь на Ваш сайт. С уважением, ESP-шник-любитель на пенсии, в прошлом С++,С# 🙂

    1. Благодарю за теплые слова, это очень стимулирует на написание дальнейших статей.
      На мой взгляд, главная проблема даже не в том, что статьи на английском. В конце концов сейчас онлайн переводчики неплохо справляются со своей работой. Главная проблема – найти эти статьи. Потому что поисковый запрос чаще пишется не на английском, а на русском. Соответственно и в результате мы получаем ссылки в основном на русскоязычные источники, а таких мало. Например статью esp32 programmers memory model я нашел не в поиске, а мне ее подсказали на форуме platfromio.

  2. Столкнулся с тем, что при работе программы, данные записываются в случайные переменный, которые не участвую ни коим образом в работе этого участка кода.
    Происходит это при вызове touch_pad_read_filtered – библиотечная функция.
    Результат вызова может записать в случайную переменную, никак не связанную и не указанную при вызове функции.
    Не встречались с таким?

    1. Добрый день!
      Нет, не сталкивался.
      Это похоже на переполнение стека задачи или переполнение какой-либо переменной

  3. Добрый день. Спасибо, за Ваши статьи, много полезного узнал.
    С толкнулся с неординарной проблемой.
    Исходя из Вашего опыта, может подскажете в какую хоть сторону искать.
    ESP32 Dev Module (16Mb Flash)
    Скетч под 2Mb в bin файле.
    Web соединение настроено WiFi и как резервное GPRS (SIM800)
    Файловая система FFAT (3Mb APP / 9.9Mb FATFS)
    Настроено OTA обновление с сервера в интернете.
    При WiFi – httpUpdate.update( client, “http://….”);
    При GPRS – скачиваю файл в FFAT, потом void updateFromFS()

    Все работает хорошо, но до тех пор пока WIFI_STA
    НО Если WIFI_AP или WIFI_AP_STA FFAT не стартует.
    Помогает только заремить – // httpUpdate.update( client, “http://….”);
    При чем, процедура где это заремлено не вызывается, так как обновление по GPRS в другой процедуре.

    Может Вы сталкивались с подобным и подскажете в чем проблема.

    1. К сожалению, не могу подсказать. FFAT я пока вообще не пользовался, так как просто не было необходимости.
      А случайно в вашем проекте ADC входы не используются? Дело в том, что ADC1 конфликтуют с WiFi режимами…
      Может быть в этом причина?

      1. Спасибо, за ответ.
        Решил проблему переходом на SPIFFS, но в стандарте были маленькие размеры самой SPIFFS. Файл не помещался. Пришлось изменить разделение памяти.
        В целом, почему я на эту статью и набрел.
        Но ответ на нашел в другом источнике
        http://www.sevsait.com/esp32_part_tabl.html .
        Спасибо в любом случае за Ваши статьи.

  4. Дмитрий

    По поводу PROGMEM
    В avr-gcc все данные (в том числе и static const) по умолчанию размещаются в SRAM, т.к размер RAM в avr очень маленький, то использовался аттрибут PROGMEM, который позволял разместить static const данные во flash (при этом при непосредственном использовании все равно происходило копирование в SRAM – pgm_read….)
    В ESP-32 весь код по умолчанию размещается в external flash, кроме того в external flash размещаются все static const данные (при этом обращение к этим данным происходит непосредтсвенно без всяких pgm_read), те аттрибут PROGMEM становится ненужным.
    Однако возникает противоположная проблема – в определенных режимах (например обработчики прерываний и др.) external flash недоступен, поэтому код который работает в таких режимах должен быть размещен в SRAM (конкретно в IRAM). Для этого используется аттрибут IRAM_ATTR. Код функций помеченных этим аттрибутом будет размещен в IRAM. Однако размещение кода в IRAM не гарантирует размещение данных также в RAM, т.е если внутри функции помеченой аттрибутом IRAM_ATTR у нас будет static const массив, то этот массив компилятор все равно может разместить в external flash. Для того чтобы этого избежать и явно указать компилятору что static const даные нужно тоже размещать в RAM (конкретно в DRAM) используется аттрибут DRAM_ATTR

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *