Embedded HandbookПриложения
Все разделы › XVIII. ESP32: второй микроконтроллер
ESP32 · уровень базовый · 15 мин чтения
Открыть в интерактивном справочнике

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.

ESP32 изнутри: два ядра, внутреннее ОЗУ команд и данных, кэш к внешней флеш-памяти, память RTC, радио и периферия


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

Три шага загрузки ESP32 и раскладка флеш-памяти: ПЗУ чипа смотрит strapping-выводы, загрузчик ESP-IDF по 0x1000 размером 25 984 байта читает таблицу разделов по 0x8000 и грузит приложение; разделы nvs с 0x9000 на 24 КБ, phy_init с 0xF000 на 4 КБ и factory с 0x10000 на 1 МБ идут впритык

Для обновления по воздуху таблицу меняют на две области приложения — об этом дальше в этой части. Значения по умолчанию, с которыми собирается любой проект:

Параметр По умолчанию
частота ядра 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 Итоги

Полные исходники

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, эмулятор.

Открыть в интерактивном справочнике