150. ESP32 как платформа: чип, модуль, память, выводы, загрузка
Вся книга до этой части — про STM32. Часть XVIII — про второй микроконтроллер, с которым сталкивается каждый, кто делает устройства с Wi-Fi и Bluetooth: ESP32. Это не «ещё одна серия», а другой мир: два ядра не той архитектуры, программа во внешней флеш-памяти через кэш, радио на кристалле, своя операционная система в комплекте и своя система сборки. Здесь всё, что в предыдущих частях было про регистры и HAL, придётся выучить заново — но принципы те же, и раздел показывает, где именно они совпадают, а где нет.
Платы для опытов нет, и она не нужна: ESP-IDF поставляется вместе с
эмулятором QEMU для ESP32. Каждая программа этой части собирается
настоящим компилятором Espressif и запускается в эмуляторе в контейнере
espressif/idf:v5.4.1; числа в тексте — из этих прогонов и из заголовков
самой ESP-IDF (_verify/esp150/). Что эмулятор не умеет — радио, АЦП,
сон, — проверяется сборкой, расчётом или моделью, и это сказано явно.
Как это называется в документации
Оригинал Перевод SoC (system on chip) система на кристалле: ядра, память, радио и периферия в одном корпусе module (ESP32-WROOM-32) модуль: чип, флеш-память, кварц и антенна на одной платке с экраном development kit (DevKit) отладочная плата: модуль плюс USB-UART, стабилизатор и кнопки Xtensa LX6, dual core архитектура ядер и два ядра: PRO_CPU и APP_CPU IRAM, DRAM внутреннее ОЗУ команд и данных IROM, DROM окна кэша, через которые виден код и константы во флеш-памяти RTC fast / slow memory память малопотребляющего домена, живёт в глубоком сне strapping pins выводы, уровень которых при сбросе задаёт режим загрузки eFuse однократно программируемые биты: ревизия, MAC, защита first / second stage bootloader загрузчик в ПЗУ чипа и загрузчик ESP-IDF во флеш partition table таблица разделов флеш-памяти app image, segment образ приложения и его сегменты по областям памяти chip revision ревизия кристалла (v0.0 … v3.x)
150.1 Чип, модуль, плата
ESP32 продаётся в трёх видах, и путать их нельзя. Чип — корпус QFN с двумя ядрами Xtensa LX6, ОЗУ, радио и периферией, но, как правило, без флеш-памяти (ESP32-D0WD): программа хранится снаружи, в микросхеме SPI-флеш. Исключения — варианты с флеш-памятью в корпусе (D2WD, U4WDH) и сборки PICO, где внутри уже флеш-память и кварц. Модуль ESP32-WROOM-32 — чип, флеш-память на 4 МБ, кварц 40 МГц и печатная антенна под металлическим экраном; это то, что ставят на плату изделия. Отладочная плата (DevKit) — модуль плюс мост USB-UART, стабилизатор 3,3 В, кнопки EN и BOOT и штыревые разъёмы; с неё начинают.
Программа узнаёт, на чём работает, через esp_chip_info:
esp_chip_info(&ci);
esp_flash_get_size(NULL, &flash);
printf("chip: model %d, cores %d, revision v%d.%d, wifi %d, bt %d, ble %d\n", ci.model, ci.cores,
ci.revision / 100, ci.revision % 100, !!(ci.features & CHIP_FEATURE_WIFI_BGN),
!!(ci.features & CHIP_FEATURE_BT), !!(ci.features & CHIP_FEATURE_BLE));
chip: model 1, cores 2, revision v0.0, wifi 1, bt 1, ble 1
flash: 2048 KiB, cpu: 160 MHz
Модель 1 — CHIP_ESP32 (у S2 — 2, у S3 — 9, у C3 — 5, у C6 — 13:
семейство большое и неоднородное, эта часть — про классический ESP32).
Ревизия v0.0 — потому что это эмулятор; у настоящих чипов v1.0 и v3.x, и
ревизию читают из eFuse. Размер флеш-памяти — тот, что записан в
заголовок образа при сборке, а не тот, что на модуле: по умолчанию
ESP-IDF пишет 2 МБ, и на модуле с 4 МБ половина остаётся невидимой, пока
не поправить menuconfig. Частота ядра по умолчанию 160 МГц, предел —
240.
150.2 Сколько чего: числа из заголовков
Все возможности чипа ESP-IDF описывает в одном заголовке soc_caps.h,
и цифры лучше брать оттуда, а не из памяти:
| Что | Сколько | Заголовок |
|---|---|---|
| ядра | 2 | SOC_CPU_CORES_NUM |
| номера GPIO | 0–39, из них нет 24, 28–31; 34–39 только вход; GPIO20 выведен не у всех корпусов | SOC_GPIO_VALID_*_MASK |
| АЦП | 2 блока: 8 + 10 каналов | SOC_ADC_CHANNEL_NUM |
| ЦАП | 2 канала по 8 бит | SOC_DAC_CHAN_NUM, SOC_DAC_RESOLUTION |
| ШИМ LEDC | 8 каналов в каждом из двух режимов, таймер до 20 бит | SOC_LEDC_* |
| ШИМ MCPWM | 2 блока × 3 оператора × 2 генератора | SOC_MCPWM_* |
| таймеры | 2 группы × 2, счётчик 64 бита | SOC_TIMER_GROUP_* |
| UART / SPI / I²C / I²S | 3 / 3 / 2 / 2 | SOC_*_NUM |
| TWAI (CAN) | 1 | SOC_TWAI_CONTROLLER_NUM |
| RMT / PCNT | по 8 каналов | SOC_RMT_*, SOC_PCNT_* |
| сенсорные входы | 10 | SOC_TOUCH_SENSOR_NUM |
| Wi-Fi, Bluetooth, Ethernet MAC, SD-хост, ULP | есть | SOC_*_SUPPORTED |
Из трёх SPI для своих устройств свободны два — SPI2_HOST и SPI3_HOST
(у ESP32 их старые имена HSPI_HOST и VSPI_HOST). Третий, SPI1, работает
с флеш-памятью программы: её выводы 6–11 в заголовке spi_pins.h названы
MSPI_IOMUX_PIN_NUM_*, и трогать их на модуле нельзя — на них сидит
программа. Из АЦП второй блок не
работает, пока включён Wi-Fi (раздел 154).
По маскам заголовка существуют 35 номеров, из них 29 с выходом. GPIO20 среди них, но выведен он только у части корпусов, например ESP32-PICO-V3; у обычного чипа и модуля WROOM-32 его нет. Так что «40 GPIO» в рекламе превращаются в 34 вывода, из них 28 с выходом, и ещё шесть из них заняты флеш-памятью.
150.3 Память: где что лежит
У STM32 программа лежит во внутренней флеш-памяти и выполняется прямо
из неё. У ESP32 флеш-память внешняя, и ядро её напрямую не видит: код и
константы читаются через кэш, а адреса 0x400D… и 0x3F40… — это
окна, в которые кэш подставляет содержимое флеш-памяти. Всё, что должно
работать быстро или пока кэш выключен, кладут во внутреннее ОЗУ атрибутом
IRAM_ATTR. Кэш выключается на время записи флеш-памяти, и обычные
обработчики прерываний в это время просто ждут. Работают только
зарегистрированные с флагом ESP_INTR_FLAG_IRAM — и всё, что такой
обработчик вызывает и читает, обязано лежать в IRAM и DRAM (IRAM_ATTR,
DRAM_ATTR).
Программа этого раздела печатает адрес каждого своего объекта и по
границам из soc.h называет область:
static const char rodata_text[] = "константа"; /* .rodata — во флеше, читается через кэш */
static int data_var = 42; /* .data — во внутреннем ОЗУ данных */
static int bss_var; /* .bss — там же, обнуляется при старте */
RTC_DATA_ATTR static int rtc_var = 7; /* медленная память RTC */
static IRAM_ATTR int in_iram(int x) { return x + 1; } /* код во внутреннем ОЗУ команд */
static int in_flash(int x) { return x + 2; } /* код во флеше через кэш */
regions: DROM 0x3f400000-0x3f800000 IROM 0x400d0000-0x40400000 IRAM 0x40080000-0x400aa000 DRAM 0x3ffae000-0x40000000 RTC 0x50000000-0x50002000
константа 0x3f407aac DROM: флеш, данные
data_var 0x3ffb0308 DRAM: ОЗУ данных
bss_var 0x3ffb2990 DRAM: ОЗУ данных
rtc_var 0x50000000 RTC: медленная память
in_iram() 0x40084474 IRAM: ОЗУ команд
in_flash() 0x400e232c IROM: флеш, код
локальная 0x3ffb3e50 DRAM: ОЗУ данных
malloc(1000) 0x3ffb4b4c DRAM: ОЗУ данных
| Область | Адреса | Что там |
|---|---|---|
| DROM | 0x3F40_0000 – 0x3F80_0000 | окно кэша: константы из флеш-памяти (.rodata) |
| DRAM | 0x3FFA_E000 – 0x4000_0000 | внутреннее ОЗУ данных: .data, .bss, стек, куча |
| IRAM | 0x4008_0000 – 0x400A_A000 | внутреннее ОЗУ команд: IRAM_ATTR, обработчики |
| IROM | 0x400D_0000 – 0x4040_0000 | окно кэша: код из флеш-памяти |
| RTC fast | 0x3FF8_0000 – 0x3FF8_2000 (8 КБ) | код и данные для пробуждения, только ядро PRO |
| RTC slow | 0x5000_0000 – 0x5000_2000 (8 КБ) | RTC_DATA_ATTR, программа ULP |
Куча — тоже внутреннее ОЗУ, и её размер программа видит сама:
heap: free 305420, internal 383172, largest block 172032
Свободно 305 КБ обычной кучи, а «внутренней» — 383 КБ: разница в 78 КБ
— это остаток ОЗУ команд, который выдаётся только под запросы с
MALLOC_CAP_32BIT, потому что по байтам туда обращаться нельзя. Самый
большой цельный кусок — 172 КБ: ОЗУ данных состоит из нескольких банков,
и один malloc на 300 КБ не пройдёт даже при 305 КБ свободных. Внешнего
ОЗУ (PSRAM) у WROOM-32 нет — оно есть у модулей WROVER.
150.4 Образ приложения: сегменты
Компоновщик раскладывает программу по областям, и esptool.py image_info
показывает, что получилось:
Entry point: 0x400811b4
Segments: 7
0 0x09a2c 0x3f400020 0x00000018 DROM
1 0x0001c 0x3ff80000 0x00009a4c RTC_DRAM
2 0x022cc 0x3ffb0000 0x00009a70 BYTE_ACCESSIBLE, DRAM
3 0x042cc 0x40080000 0x0000bd44 IRAM
4 0x136bc 0x400d0020 0x00010018 IROM
5 0x089fc 0x400842cc 0x000236dc IRAM
6 0x00004 0x50000000 0x0002c0e0 RTC_DATA
Сегменты DROM и IROM не копируются никуда: они лежат во флеш-памяти на
своём месте, а кэш отображает их по адресам 0x3F40… и 0x400D…, и
поэтому в образе они выровнены так, чтобы смещение во флеш-памяти
совпадало со смещением в окне. Сегменты DRAM и IRAM загрузчик копирует
из флеш-памяти в ОЗУ при старте — это те 8,9 и 17 + 35 КБ, что занимают
.data и код с IRAM_ATTR. Сегмент RTC_DATA из четырёх байт — наша
переменная rtc_var. Точка входа 0x400811b4 — в IRAM: старт должен
работать до того, как кэш настроен. Итоговый размер:
│ Flash Code │ 79546 │ 2.38 │ 3262758 │ 3342304 │
│ IRAM │ 52423 │ 40.0 │ 78649 │ 131072 │
│ Flash Data │ 39468 │ 0.94 │ 4154804 │ 4194272 │
│ DRAM │ 11148 │ 6.17 │ 169588 │ 180736 │
Пустая программа с одной печатью занимает 40 % ОЗУ команд — это обработчики, драйвер флеш-памяти и ядро FreeRTOS, которым нельзя жить в кэше. Это главное отличие от STM32: не флеш, а IRAM — первый ресурс, который кончается.
150.5 Загрузка: ПЗУ, загрузчик, разделы
Старт идёт в три шага. Первый загрузчик — в ПЗУ чипа, его не
изменить: по уровням strapping-выводов он решает, грузить программу из
флеш-памяти или ждать прошивку по UART, и печатает в консоль строку вида
rst:0x1 (POWERON_RESET),boot:0x12 (SPI_FAST_FLASH_BOOT), где boot —
как раз считанные выводы. Главный из них — GPIO0: низкий уровень при
сбросе означает «режим прошивки», и кнопка BOOT на плате замыкает его
на землю; на DevKit мост USB-UART дёргает EN и GPIO0 сам, поэтому
idf.py flash не требует кнопок. Остальные strapping-выводы (2, 5, 12,
15) задают напряжение флеш-памяти и вывод отладочного журнала — на них
при сбросе не должно висеть ничего, что тянет уровень.
Второй загрузчик — программа ESP-IDF по смещению 0x1000, 25 984
байта в этой сборке. Он читает таблицу разделов по смещению 0x8000,
находит раздел приложения, проверяет его образ и переносит сегменты в
ОЗУ. Таблица по умолчанию — три раздела:
nvs,data,nvs,0x9000,24K,
phy_init,data,phy,0xf000,4K,
factory,app,factory,0x10000,1M,
nvs — хранилище настроек (раздел 155), phy_init — калибровка радио,
factory — приложение с 0x10000 размером 1 МБ. Программа видит ту же
таблицу через esp_partition_find:
part nvs type 1 subtype 0x02 offset 0x009000 size 0x006000
part phy_init type 1 subtype 0x01 offset 0x00f000 size 0x001000
part factory type 0 subtype 0x00 offset 0x010000 size 0x100000
Для обновления по воздуху таблицу меняют на две области приложения — об этом дальше в этой части. Значения по умолчанию, с которыми собирается любой проект:
| Параметр | По умолчанию |
|---|---|
| частота ядра | 160 МГц |
| тик FreeRTOS | 100 Гц |
стек задачи app_main |
3584 байта |
| сторож задач (TWDT) | 5 с |
| сторож прерываний | 300 мс |
| порог просадки питания (brownout) | 2,43 В |
| консоль | UART0, 115 200 бод |
| размер флеш-памяти в заголовке | 2 МБ |
Наша задача app_main работает на ядре 0 с приоритетом 1, и из 3584
байт стека к концу программы свободно 2064 — печать с %p и разбор
разделов стоили полутора килобайт.
150.6 Что переносится с STM32, а что нет
| Понятие | STM32 | ESP32 |
|---|---|---|
| ядро | Cortex-M, одно | Xtensa LX6, два (PRO и APP) |
| программа | внутренняя флеш, XIP | внешняя флеш через кэш; горячее — в IRAM |
| ОЗУ | одно SRAM (плюс CCM) | DRAM 328 КБ + IRAM + RTC; куча по банкам |
| HAL | CMSIS + HAL/LL, любой RTOS | ESP-IDF с FreeRTOS внутри, без ОС не бывает |
| сборка | Keil, CubeIDE, Makefile | CMake через idf.py, menuconfig |
| прошивка | ST-Link, SWD | UART через ПЗУ-загрузчик, esptool; JTAG отдельно |
| настройки | своя область флеш, EEPROM снаружи | NVS в таблице разделов |
| радио | внешний модуль | Wi-Fi и Bluetooth на кристалле |
| сон | Stop, Standby, резервный домен | light/deep sleep, память RTC, сопроцессор ULP |
| отладка без платы | Renode, QEMU для Cortex-M | QEMU от Espressif в комплекте с ESP-IDF |
Общее — всё, что было в частях I–IX про схемотехнику, интерфейсы, датчики и питание: I²C остаётся I²C, а закон Ома — законом Ома. Меняются регистры, API и ограничения выводов.
150.7 Грабли
| Симптом | Причина |
|---|---|
| модуль на 4 МБ, а программа видит 2 | размер флеш-памяти в menuconfig по умолчанию 2 МБ — задать 4 |
Guru Meditation (Cache disabled but cached memory region accessed) при записи в NVS или обновлении |
обработчик с флагом ESP_INTR_FLAG_IRAM зовёт функцию без IRAM_ATTR или читает константу без DRAM_ATTR, а кэш на время записи выключен; обычные обработчики в это время просто ждут |
malloc на 200 КБ возвращает NULL при 300 КБ свободных |
куча из банков, самый большой цельный — 172 КБ |
| «GPIO 6…11 не работают» | это флеш-память модуля |
| GPIO 34–39 не переключаются в выход | только входы, и без подтяжек |
| плата не стартует с подключённым датчиком | датчик тянет strapping-вывод (0, 2, 5, 12, 15) при сбросе |
boot:0x… и бесконечный waiting for download |
GPIO0 в нуле при сбросе — кнопка BOOT или внешняя подтяжка |
| перезагрузки при включении Wi-Fi | просадка ниже 2,43 В: слабый стабилизатор или кабель |
| ревизия чипа в логе v0.0 | эмулятор; у настоящего — из eFuse |
150.8 Итоги
- Чип без флеш-памяти, модуль WROOM-32 с 4 МБ и антенной, DevKit с USB-UART и кнопками.
- Два ядра Xtensa LX6, 160 МГц по умолчанию; код и константы во внешней флеш-памяти через окна кэша IROM/DROM, горячий код — в IRAM.
soc_caps.h: 35 номеров GPIO, 29 с выходом; у WROOM-32 нет GPIO20 — 34 вывода, 28 с выходом, 6 заняты флеш-памятью; АЦП 8 + 10, ЦАП 2 × 8 бит, LEDC 8 + 8, MCPWM 12, UART 3, SPI 3, I²C 2, I²S 2, TWAI 1.- Куча 305 КБ, самый большой кусок 172 КБ; пустая программа — 40 % IRAM.
- Загрузка: ПЗУ по strapping (GPIO0) → загрузчик ESP-IDF с
0x1000→ таблица разделов с0x8000→ приложение с0x10000; 7 сегментов образа. - По умолчанию: тик 100 Гц, TWDT 5 с, стек
app_main3584, brownout 2,43 В.
Полные исходники
main.c — что за чип, где что лежит в памяти и как разбита флеш-память.
/* Раздел 150: что за чип, где что лежит в памяти, как разбита флеш-память.
* Границы областей берутся из заголовка soc/soc.h ESP-IDF, а не записаны руками. */
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include "esp_app_desc.h"
#include "esp_attr.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_heap_caps.h"
#include "esp_partition.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/soc.h"
static const char rodata_text[] = "константа"; /* .rodata — во флеше, читается через кэш */
static int data_var = 42; /* .data — во внутреннем ОЗУ данных */
static int bss_var; /* .bss — там же, обнуляется при старте */
RTC_DATA_ATTR static int rtc_var = 7; /* медленная память RTC */
static IRAM_ATTR int in_iram(int x) { return x + 1; } /* код во внутреннем ОЗУ команд */
static int in_flash(int x) { return x + 2; } /* код во флеше через кэш */
static const char *region(uintptr_t a)
{
if (a >= SOC_DROM_LOW && a < SOC_DROM_HIGH) return "DROM: флеш, данные";
if (a >= SOC_IROM_LOW && a < SOC_IROM_HIGH) return "IROM: флеш, код";
if (a >= SOC_IRAM_LOW && a < SOC_IRAM_HIGH) return "IRAM: ОЗУ команд";
if (a >= SOC_DRAM_LOW && a < SOC_DRAM_HIGH) return "DRAM: ОЗУ данных";
if (a >= SOC_RTC_DATA_LOW && a < SOC_RTC_DATA_HIGH) return "RTC: медленная память";
return "?";
}
static void where(const char *what, const void *p)
{
printf("%-18s 0x%08" PRIxPTR " %s\n", what, (uintptr_t)p, region((uintptr_t)p));
}
void app_main(void)
{
esp_chip_info_t ci;
uint32_t flash = 0;
int local = 0;
esp_chip_info(&ci);
esp_flash_get_size(NULL, &flash);
printf("chip: model %d, cores %d, revision v%d.%d, wifi %d, bt %d, ble %d\n", ci.model, ci.cores,
ci.revision / 100, ci.revision % 100, !!(ci.features & CHIP_FEATURE_WIFI_BGN),
!!(ci.features & CHIP_FEATURE_BT), !!(ci.features & CHIP_FEATURE_BLE));
printf("flash: %" PRIu32 " KiB, cpu: %d MHz\n", flash / 1024, CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ);
printf("regions: DROM 0x%08x-0x%08x IROM 0x%08x-0x%08x IRAM 0x%08x-0x%08x DRAM 0x%08x-0x%08x RTC 0x%08x-0x%08x\n",
SOC_DROM_LOW, SOC_DROM_HIGH, SOC_IROM_LOW, SOC_IROM_HIGH, SOC_IRAM_LOW, SOC_IRAM_HIGH,
SOC_DRAM_LOW, SOC_DRAM_HIGH, SOC_RTC_DATA_LOW, SOC_RTC_DATA_HIGH);
where("константа", rodata_text);
where("data_var", &data_var);
where("bss_var", &bss_var);
where("rtc_var", &rtc_var);
where("in_iram()", (const void *)in_iram);
where("in_flash()", (const void *)in_flash);
where("локальная", &local);
void *h = malloc(1000);
where("malloc(1000)", h);
free(h);
printf("heap: free %u, internal %u, largest block %u\n", (unsigned)esp_get_free_heap_size(),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL),
(unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
while (it) { /* на последнем шаге итератор освобождается сам */
const esp_partition_t *p = esp_partition_get(it);
printf("part %-9s type %d subtype 0x%02x offset 0x%06" PRIx32 " size 0x%06" PRIx32 "\n",
p->label, p->type, p->subtype, p->address, p->size);
it = esp_partition_next(it);
}
const esp_app_desc_t *app = esp_app_get_description();
printf("app: %s, esp-idf %s\n", app->project_name, app->idf_ver);
printf("task: core %d, priority %u, stack left %u bytes\n", xPortGetCoreID(),
(unsigned)uxTaskPriorityGet(NULL), (unsigned)uxTaskGetStackHighWaterMark(NULL));
printf("in_iram(1) = %d, in_flash(1) = %d, data_var = %d, bss_var = %d, rtc_var = %d\n",
in_iram(1), in_flash(1), data_var, bss_var, rtc_var);
printf("done\n");
}
Дальше: раздел 151 — ESP-IDF и инструменты: idf.py, menuconfig, компоненты, esptool, эмулятор.