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

Внутренняя кухня ESP32: система сборки и загрузки прошивки – как это работает?

Доброго здравия, уважаемые читатели!

Когда вы нажимаете кнопку Upload в Arduino IDE или запускаете команду ESP-IDF: Build project в Visual Studio code, обычно начинается магия и волшебство… В терминал выводятся сотни  сообщений, что-то крутится, компилируется и копируется, и в результате контроллер вдруг начинает мигать светодиодиками или делать что-то другое, но очень полезное. В этой статье попробуем совсем немного заглянуть под капот процесса: то есть обсудим систему сборки программ для ESP на основе C/C++ – как компилируется и собирается проект, как готовый файл загружается в чип (а точнее на подключенную к чипу flash-память), как подключается к компьютеру, и вот эти все знакомые-незнакомые слова: Strapping pins, idf.py, toolchain, esptool, bootloader, stub и т.д. 


Особенности хранения и запуска приложений на ESP

Прежде чем переходить к тонкостям компиляции и прошивки, необходимо вначале немного разобраться с некоторыми аппаратными тонкостями ESP32: для чего нужен ROM и FLASH, что такое ROM-bootloader (ROM-загрузчик, загрузчик первой очереди), second stage bootloader (загрузчик второй очереди) и т.д.

Как вы наверное уже знаете, ESP – это далеко не один чип, а целое семейство чипов, довольно сильно различающихся между собой (про которые вы можете почитать в другой статье). Если вы немного знакомы с продукцией Espressif, то могли заметить, что все эти различные SoC ESP имеют одну общую особенность – на чипе с микроконтроллером расположено довольно скромное по размерам ПЗУ (ROM) – от 128 до 576 килобайт, чего явно не достаточно, дабы “впихнуть” большинство пользовательских программ. На самом деле 448 КБ ROM на классическом ESP32 — это не так уж и мало, но для здесь очень важно понять назначение этой памяти, а не её размер. Например в новых сериях вроде ESP32-C5 размер ROM даже меньше, чем у “классики” — всего 320 КБ, а в ESP32-H2 и ещё меньше — 128 КБ . Но это отнюдь не является «недостатком», так как размер ROM оптимизируется под конкретную архитектуру и задачи, и он не требуется для хранения пользовательских программ.

Весь фокус в том, что на ESP ваши программы никогда не записываются и не хранятся в ROM (ПЗУ), как собственно и какие-либо ваши пользовательские данные. В ROM хранится одна-единственная программа: навсегда поселившийся там с завода-изготовителя ROM-bootloader, или “загрузчик первой очереди“. Все остальные данные: программы, константы, переменные, файлы хранятся на внешней flash-памяти, подключенной к микроконтроллеру по интерфейсу SPI (для этого обычно используются аппаратные контроллеры SPI0 и SPI1). Вот поэтому на всех платах с ESP32 (и ESP8266) всегда присутствует отдельная микросхема flash-памяти: он может быть распаяна на плате отдельно от чипа, может быть внутри модуля или даже интегрирован в корпус SoC, но сути это никак не влияет – “электрически” она отдельная. На типичных платах разработчика её объём составляет 4 МБ или 8 МБ, но может быть и 2 МБ и 16 МБ (вот это уже наш размерчик!). Сама Flash-память, как и жесткий диск компьютера, может быть разделена на один или несколько разделов, каждый из которых хранит свой тип информации: прошивку, данные, файлы и т.д., обо всем этом вы можете подробнее почитать в другой статье.

ROM-загрузчик прошит в чип на заводе и не может быть изменён. Он запускается первым при подаче питания или сбросе CPU и его единственная задача — либо запустить загрузчик второй очереди из Flash, либо принять новую прошивку через порт UART/USB. Загрузчик первой очереди считывает состояние специальных strapping-пинов и решает, что делать дальше: загрузить программу из Flash или принять новую. Здесь же в ROM находятся и базовые драйверы “железа”: примитивные низкоуровневые функции для работы с регистрами, SPI Flash, UART и некоторыми другими периферийными блоками, чтобы загрузчик мог ими оперировать. А также имеются и криптографические функции: базовые алгоритмы для проверки подписи при безопасной загрузке Secure Boot.

Если вы немного знакомы с устройством компьютеров, то можно сразу заметить аналогию: ROM-bootloader – это почти прямой аналог BIOS на вашем компьютере, а Flash-память – это “жесткий диск”, который хранит все остальное. Но в отличие от BIOS компьютера, у ROM-загрузчика нет меню настроек и пользовательского интерфейса: это просто минимально необходимый код, который умеет ровно две вещи — загрузить программу из flash по строго определенному адресу или принять новую извне. Все «настройки» чипа (частота, режим загрузки, защита) хранится либо в eFuse (специальные одноразовые конфигурационные ячейки памяти), либо в самой прошивке.

На компьютере BIOS загружает вторичный загрузчик операционной системы (GRUB или Windows Boot Manager), а тот уже в свою очередь загружает запускает ядро операционной системы. На ESP32 реализована очень похожая схема работы: ROM-загрузчик загружает и запускает загрузчик второй очереди из Flash-памяти, а уже тот — ваше приложение из соответствующего раздела. 

ROM-загрузчик умеет загружать и запускать на выполнение код из Flash только с фиксированного смещения 0x1000*. Он в душе не знает, что такое таблица разделов, и не умеет выбирать разделы между OTA-обновлениями. Поэтому он просто запускает загрузчик второй очереди, который компилируется и прошивается на Flash вместе с прошивкой. Последний обычно как раз и находится по адресу 0x1000.
* Примечание: адрес 0x1000 справедлив только на классическом ESP32; на новых сериях адрес загрузчика второй очереди может быть другим, и ROM-загрузчик определяет его по заголовку.

Именно загрузчик второй очереди делает всю «умную» работу:

  • Читает таблицу разделов из Flash (по умолчанию она записана по адресу 0x8000). В этой таблице записано, где находится ваша программа (factory), где лежат OTA-слоты для обновлений, и где хранятся данные (например, настройки Wi-Fi в NVS). Более подробно об этом рассказано в другой статье.
  • Если в таблице есть OTA-разделы, загрузчик смотрит в специальный раздел otadata, чтобы понять, какую именно программу нужно запустить: заводскую или одну из обновлённых по воздуху.
  • Второй загрузчик настраивает кэш и MMU (блок управления памятью), чтобы процессор мог быстро выполнять код прямо из Flash-памяти. Он копирует нужные куски кода в быструю DRAM и «подключает» остальные части через кэш.
  • Также вторая ступень загрузчика проверяет хэш (SHA-256) вашего приложения для проверки его целостности. Если включён Secure Boot, она также проверяет криптографическую подпись, чтобы убедиться, что прошивку не подменили.
  • После этого управление передается программе из выбранного раздела OTA.

Стоит упомянуть, что загрузчик второй ступени компилируется вместе с прошивкой и загружается на flash вместе с основной прошивкой только по кабелю, но никогда не обновляется по OTA. Если вы обновите приложение «по воздуху», загрузчик останется старым. Как, собственно, и таблица разделов памяти. Поэтому Espressif рекомендует не изменять его параметры без крайней необходимости, а если и менять — тщательно тестировать, так как ошибка в загрузчике может «окирпичить» устройство.

Также учтите, при включении Secure Boot или Flash Encryption размер загрузчика заметно растёт, и нужно следить, чтобы он не превысил отведённое ему место (обычно 32 КБ), иначе сборка упадёт с ошибкой.


Инструменты, используемые для сборки и прошивки ESP

Теперь нам необходимо познакомится с программными инструментами, которые используются для компиляции ваших программ (скетчей) и сборки их в двоичные образы, а также для загрузки скомпилированных файлов прошивок в чип. Здесь многое будет зависеть от того, какой фреймворк (платформу) вы используете для создания ваших программ: ESP-IDF, Arduino core или PlatformIO. 

 


ESP-IDF

В основе ESP-IDF лежит система сборки на базе СМake и Ninja. Что это значит?

  • CMake (от англ. cross-platform make — «кроссплатформенный make») — это не компилятор, это система автоматизации сборки. Основной файл сценария для CMake — файлы CMakeLists.txt, в которых описывается структура проекта: какие файлы исходников используются, какие библиотеки нужны, какие цели (исполняемый файл, библиотека). На основе этого описания CMake генерирует файлы, понятные конкретной системе сборки. 
  • На следующем этапе запускается вторая ступень системы сборки: Ninja, которая выполняет инструкции, созданные на предыдущем этапе. Но было бы ошибкой считать, что ninja – это и есть компилятор. Ninja — это просто исполнитель задач. Он ничего не знает про C/C++ или ESP32; он просто запускает внешние команды (компиляторы, скрипты, утилиты), которые прописаны в правилах сборки. Он используется, так как это более быстрая система сборки (когда-то разработанная Google для проекта Chromium) по сравнению со “штатным” Make. Таким образом, CMake и Ninja дополняют друг друга: первый упрощает настройку проекта, второй обеспечивает быструю сборку.
  • Для непосредственной компиляции файлов проекта Ninja, в свою очередь, вызывает специальный кросс-компилятор, называемый Toolchain (цепочка инструментов), который зависит от архитектуры целевого процессора, для которой выполняется сборка – xtensa или risc-v.

Теоретически, вы можете запускать CMake и Ninja вручную (см. перевод официальной документации системы сборки), но на практике этим занимается утилита idf.py. Это специальный скрипт, написанный на python 3, который полностью настраивает процесс сборки прошивки в зависимости от архитектуры выбранного чипа и управляет им. А кроме того, умеет выполнять многие сервисные функции: создание пустого проекта, создание и подключение библиотек и т.д..

Следующий скрипт esptool.py — это низкоуровневый «мост» между компьютером и чипом. Он не собирает код, а доставляет готовый результат в чип и умеет «общаться» с ним напрямую. Именно esptool (а не idf.py) «дёргает» пины моста DTR/RTS для сброса, загружает stub и записывает байты во Flash. Если вы будете использовать аппаратный JTAG-отладчик, например SP-Prog, то esptool уже не справится с этим и вам понадобится другой инструмент OpenOCD для связи с чипом.

Таким образом, компиляция и сборка проекта выполняется в несколько этапов:

  1. ESP‑IDF использует CMake как систему конфигурации: при запуске команды idf.py build внутри вызывается CMake, который читает CMakeLists.txt и генерирует файлы сборки под конкретный генератор (по умолчанию — Ninja).
  2. CMake формирует правила сборки — по сути, это команды вида «возьми файл main.c, передай его в xtensa-esp32-elf-gcc с такими-то флагами, получи main.o». Эти правила записываются в файлы .ninja.
  3. Ninja исполняет эти правила: он читает .ninja-файлы и непосредственно запускает исполняемые файлы с помощью toolchain (компилятор, ассемблер, линковщик и т. д.) параллельно и эффективно. В результате этого создается скомпилированный файл .elf.
  4. Esptool преобразует .elf в двоичный файл .bin и загружает его на flash-память ESP. 

Картинка из официальной документации ESP-IDF

Можно также представить следующую аналогию:

  • idf.py — это менеджер, он знает все правила, как собирать те или иные конструкции и отдаёт приказы исполнителям.
  • CMake и Ninja — это инженеры и технологи, они превращают ваш код (чертежи) в четкий план работ.
  • Toolchain (gcc/ld) — это рабочие, они отвечают за последовательную обработку и изготовление деталей, а затем и сборку конструкций в единое целое по чертежам и плану работ (компиляция).
  • esptool.py — это транспорт и монтажники, они отвечают за доставку собранных конструкций на место эксплуатации (прошивку).

 


idf.py — это не просто «сборщик», а полноценный интерфейс управления проектом на базе ESP-IDF (frontend), который объединяет конфигурацию, сборку, прошивку и мониторинг в единую цепочку. Это специальный скрипт, написанный на python 3, который настраивает процесс сборки прошивки в зависимости от архитектуры выбранного чипа и полностью управляет им. Он перекрывает весь жизненный цикл разработки — от создания проекта до его запуска на чипе. Причем он управляет не только CMake, Ninja, но и утилитой для доставки прошивки esptool. Когда ninja завершил работу и файлы .bin готовы, команда idf.py flash вызывает esptool, который заливает их в чип. Более подробно об этом можно почитать в переводе официальной документации на эту тему.

Создание и настройка проекта

  • idf.py create-project <name> — создаёт новый проект с базовой структурой каталогов и файлов.
  • idf.py create-component <name> — создаёт новый компонент (модуль, библиотеку) с минимально необходимыми файлами.
  • idf.py set-target <target> — ключевая команда для выбора конкретного целевого чипа (например, esp32, esp32c3, esp32s3). Она очищает папку сборки и пересоздаёт sdkconfig под выбранную архитектуру, что автоматически подтягивает правильный toolchain.
  • idf.py menuconfig — запускает графический интерфейс конфигурации проекта, где настраиваются параметры Wi-Fi, памяти, FreeRTOS и т.д.

Сборка проекта

  • idf.py build — главная команда сборки. Она выполняет всю необходимую цепочку действий: при необходимости создаёт папку build, запускает CMake для генерации файлов сборки, а затем вызывает Ninja (или Make) для компиляции и линковки. Сборка инкрементальная — если ничего не менялось, она ничего не делает.
  • idf.py clean / fullclean — очистка промежуточных файлов (clean) или полное удаление папки сборки вместе с кэшем CMake (fullclean) .

Прошивка и отладка

  • idf.py flash — прошивает чип. При необходимости сначала автоматически запускает сборку, если проект ещё не собран.
  • idf.py erase_flash — стирает всю flash-память. Это необходимо после изменения таблицы разделов flash-памяти.
  • idf.py monitor — открывает последовательный монитор для просмотра вывода printf из чипа.

Дополнительные полезные команды

  • idf.py size — показывает, сколько RAM и Flash занимает ваша программа (и каждый компонент в отдельности).
  • idf.py reconfigure — принудительно перезапускает CMake (полезно, если вы добавили новые файлы вручную).
  • idf.py docs — открывает документацию для вашего чипа в браузере.

Команды можно комбинировать и объединять в одну строку. Например, команда idf.py build flash monitor выполнит сборку, прошивку и откроет монитор в правильном порядке.

 


Toolchain (инструментальная цепочка) — это целый набор программных инструментов, которые работают вместе, чтобы превратить ваш исходный код в готовую программу для конкретного чипа. Это не одна программа, а именно «цепочка» (chain), где каждый инструмент выполняет свою часть работы и передаёт результат своей работы следующему инструменту в цепочке. Иногда этот набор называют кросс-компилятором, но в официальной документации Espressif он так и называется – toolchain и включает в себя несколько ключевых компонентов:

  • Компилятор GCC, который превращает ваш C/C++ код в ассемблерные инструкции, обычно cc1.
  • Ассемблер as превращает ассемблерный код (сгенерированный компилятором) в машинные инструкции – объектные файлы.
  • Линкер ld собирает все объектные файлы и библиотеки в один исполняемый файл (.elf), расставляя код и данные по адресам памяти согласно скрипту линкера.
  • Стандартные библиотеки: например, libc (стандартная библиотека C) и libgcc (низкоуровневые функции), заранее скомпилированные специально для соответствующей архитектуры.
  • Библиотека newlib: облегчённая версия стандартной библиотеки C, предназначенная для встраиваемых систем.

Для каждой архитектуры (xtensa, risc-v) имеется свой набор настроек и инструментов, который вызывается с помощью специального файла – “драйвера”, например для классической ESP32 это будет драйвер xtensa-esp32-elf-gcc. Подобно idf.py, это всего-лишь ещё одна программа-диспетчер, которая вызывает другие инструменты. Компилятор, ассемблер и линкер — это отдельные исполняемые файлы внутри toolchain-а, но система сборки запускает именно …-elf-gcc, который уже сам решает, кого и с какими аргументами запускать.

Разные архитектуры CPU означают разный набор машинных инструкций. Компилятор для Xtensa не сможет сгенерировать код, который поймёт процессор RISC-V, и наоборот. Это не просто разные флаги или настройки — это разные «переводчики» с фундаментально разной логикой работы. Именно поэтому, когда вы выполняете команду idf.py set-target esp32c3, система сборки автоматически подтягивает тулчейн riscv32-esp-elf, а для команды idf.py set-target esp32s3 — xtensa-esp-elf. Вам не нужно переключать их вручную, но важно понимать, что под капотом происходит смена всего инструментария.

Когда CMake/Ninja запускает xtensa-esp32-elf-gcc для сборки проекта, этот драйвер действует как «диспетчер»:

  • Для каждого файла проекта gcc вызывает внутренний компилятор C (обычно cc1), который превращает код в ассемблер. Затем gcc автоматически вызывает ассемблер (as), чтобы получить объектный файл main.o.
  • Когда все объектные файлы готовы, gcc (если ему передать соответствующие флаги или если он вызывается для финальной сборки) вызывает линкер (ld или collect2, который затем вызывает ld), чтобы собрать всё в .elf.

То есть gcc — это не монолитный “черный ящик”, а тоже обёртка (подобно idf.py), которая просто знает правильную последовательность вызовов других инструментов и нужные флаги для каждой архитектуры. В документации по классическим тулчейнам это описывается именно так: «C compiler as we know it is actually a driver for a chain of tools»

 


esptool — это ещё одна утилита от Espressif, созданная для прошивки, настройки и взаимодействия с чипами Espressif через последовательный порт.

Если idf.py — это по сути «менеджер проекта», который управляет сборкой, то esptool — это «курьер», который доставляет готовый бинарник в чип и умеет общаться с ним напрямую по последовательному порту. Он входит в полный набор инструментов вместе с espefuse (инструмент для работы с eFuse) и espsecure (инструмент для шифрования и подписи прошивки) и умеет следующее:

  • Читать, записывать, стирать и проверять данные во Flash-памяти.
  • Читать информацию о чипе: MAC-адрес, ID флеш-памяти, ревизию чипа.
  • Готовить бинарники к прошивке: например, конвертировать .elf в .bin командой elf2image.
  • Объединять образы: собирать несколько бинарников в один файл для удобной прошивки.

Некоторые команды, которые умеет выполнять esptool:

  • esptool -p PORT flash-id — прочитать ID флеш-памяти и её размер.
  • esptool -p PORT write-flash <адрес> <файл.bin> — главная команда прошивки: записывает бинарник по указанному адресу (например, 0x1000 для загрузчика или 0x10000 для приложения).
  • esptool erase-flash — полностью стереть Flash (все байты станут 0xFF): полезно при смене разметки разделов.
  • esptool read-flash <адрес> <размер> <файл> — считать содержимое Flash в файл (например, для бэкапа).
  • esptool elf2image my_app.elf — Превратить .elf (результат работы линкера) в .bin, который можно прошить в устройство. Эту команду idf.py вызывает автоматически при сборке.

 

Кроме собственно загрузки прошивки в чип, эта утилитка умеет самостоятельно переводить ESP в режим прошивки и выполнять hard reset чипа по окончании оной с помощью вспомогательных сигналов UART интерфейса DTR и RTS, но только при условии, если линии DTR и RTS микросхемы USB-последовательного порта (например, CP210x, CH340) подключены к контактам GPIO0 (Boot) и EN (CHIP_PU) чипа соответственно. Перед загрузкой приложения esptool отправляет в порт специальную последовательность для переключения уровней сигналов на DTR и RTS, чтобы сбросить ESP и при выходе из сброса CPU обнаружил низкий уровень на GPIO0, тем самым войдя в режим загрузки. После успешного завершения загрузки прошивки, esptool опять “дергает” RTS, заставляя чип перезагрузится и запустить только что прошитую программу. Более подробно про этот механизм рассказано здесь.

Для чипов с встроенным USB интерфейсом USB-JTAG (например, ESP32-S3, C3 и др.) esptool использует другой механизм. esptool автоматически определяет подключение к чипу через USB-JTAG и отправляет в порт специальную последовательность команд для сброса чипа.

 


Flasher Stub 

При загрузке прошивки esptool использует небольшую вспомогательную программу, которая называется Flasher Stub или просто Stub. Логика работы здесь такая:

  • esptool подключается к ROM-загрузчику чипа (который, как мы помним, нельзя обновить).
  • Далее он загружает в оперативную память (RAM) чипа крошечную программу — Flasher Stub и передает ей управление.
  • Stub перехватывает управление и выполняет все дальнейшие операции по загрузке прошивки (прием данных, запись/чтение Flash) самостоятельно, используя более оптимизированные процедуры UART.

Зачем это нужно: Stub работает быстрее и обходит некоторые баги и ограничения заводского ROM-загрузчика первой очереди. Если по какой-то причине вам нужно работать напрямую через ROM-ootloader, можно использовать флаг --no-stub, но тогда скорость прошивки будет несколько ниже.


Arduino core

Хотя Arduino core и основан на той же самой ESP-IDF, сборка в Arduino core для ESP32 устроена принципиально иначе. Если сборка ESP-IDF — это «прозрачный» процесс, где при желании вы видите каждый шаг сборки (CMake → Ninja → toolchain), то Arduino core скрывает почти всё. Но под капотом она использует те же самые инструменты, просто в другом порядке и с другим уровнем контроля пользователем.

Ключевой факт сборки скетча Arduino для ESP32: Arduino core никогда не перекомпилирует API ESP-IDF “с нуля” из исходников, как это делает система сборки ESP-IDF. Вместо этого он использует предварительно скомпилированные статические библиотеки ESP-IDF. Иными словами, когда Espressif выпускает новую версию Arduino core, они сначала собирают ESP-IDF с определёнными настройками (теми самыми, которые в ESP-IDF вы могли бы поменять через menuconfig), а затем «запекают» все это в виде готовых к использованию .a файлов.

Именно поэтому в Arduino IDE нельзя изменить sdkconfig (например, подключить cert bundle или изменить настройки Wi-Fi) без пересборки самого core через специальный Lib Builder. Вы получаете ровно те настройки проекта, которые Espressif сделали за вас. Примечание авт.: и это основная и главная причина, почему я не использую Arduino core в рабочих устройствах.

Процесс сборки скетча

В отличие от ESP-IDF, где idf.py управляет всем с помощью CMake и Ninja, Arduino IDE использует собственную внутреннюю логику сборки (в Arduino IDE 2 она реализована на Java/TypeScript, а в arduino-cli — на Go). 

Когда вы устанавливаете ESP32 core через Boards Manager, вы скачиваете пакет, который содержит:

  • Toolchain — тот же самый xtensa-esp32-elf (или riscv32-esp-elf для C3/C6), который мы обсуждали выше. Arduino core точно также использует его для компиляции вашего скетча.
  • Предварительно собранные библиотеки ESP-IDF — файлы .a для всех низкоуровневых API: Wi-Fi, FreeRTOS, драйверов и т.д.
  • esptool — утилита для прошивки (та же самая, что использует idf.py, описание см. выше).
  • Скрипты сборки — файлы platform.txt и boards.txt, которые содержат готовые «рецепты» — какие флаги компиляции передавать, какие адреса использовать для прошивки различных плат.

Процесс сборки в этом случае выглядит так:

  • IDE берёт ваш .ino файл, добавляет к нему объявления функций API (прототипы) и генерирует временный .cpp файл.
  • Затем вызывается компилятор из toolchain-а (например, xtensa-esp32-elf-gcc), который собирает только ваш код и код Arduino-обёртки (классы Serial, WiFi и т.д.).
  • Линковка. Ваш скомпилированный код линкуется с заранее собранными статическими библиотеками ESP-IDF, которые уже лежат в пакете Arduino core в виде .а файлов.
  • Загрузка в чип. esptool загружает готовый к употреблению файл на flash-память ESP32. Arduino IDE сама вызывает esptool с нужными аргументами, чтобы прошить все эти файлы по правильным адресам.

То есть, если в ESP-IDF вы собираете всё из исходников, то в Arduino IDE вы собираете только свой скетч, а всё остальное просто подключается как готовые «кирпичи». И делается это, увы, заметно медленнее, чем это делается с помощью CMake и Ninja.

Arduino core для ESP32 — это не отдельная вселенная, а «фастфуд». В Espressif заранее приготовили все сложные блюда (драйверы, Wi-Fi, FreeRTOS) и упаковали их в готовые полуфабрикаты (статические библиотеки). Arduino IDE просто берёт ваш скетч, смешивает его с этими полуфабрикатами и разогревает. В ESP-IDF вы сами решаете, какие ингредиенты положить и как их приготовить, а в Arduino — получаете готовый набор, который работает, но который сложно изменить под свои вкусы.


PlatformIO

У PlatformIO имеется своя собственная, универсальная система сборки — SCons, которая позиционируется как «усовершенствованная кроссплатформенная замена классическому Make». Она работает поверх любых фреймворков (Arduino, ESP-IDF, mbed и т.д.), но для ESP-IDF ей приходится «договариваться» с CMake, но не заменять его полностью.

  • Для ESP-IDF CMake + Ninja работают, но не полностью. PlatformIO использует CMake и Ninja для генерации нужных “вещей” на этапе предварительной компиляции (например, таблицы разделов, сгенерированных заголовков) и дальше “передаёт” это в SCons. SCons выполняет всю тяжёлую работу и сам управляет компиляцией и линковкой вашего кода, используя toolchain. То есть это скорее «адаптер», а не полная замена.
    Один из разработчиков PlatformIO отметил, что скрипт сборки для ESP-IDF — это единственный известный ему пример, где SCons-скрипт PlatformIO взаимодействует с CMake. Для других фреймворков (например, mbed) логика сборки просто воспроизводится в SCons или пишется адаптер.
  • Для Arduino core такой необходимости уже нет, и PlatformIO использует только свои собственные скрипты SCons, которые полностью компилируют скетч и линкуют его с предварительно собранными библиотеками.

PlatformIO — это «универсальный пульт» для разных микроконтроллеров и фреймворков. Если вы работаете в нем с ESP-IDF, PlatformIO не выбрасывает CMake и Ninja полностью, а использует их для подготовительных шагов, а всю основную сборку берёт на себя SCons. Поэтому в этом случае PlatformIO — это не «замена CMake», а «надстройка», которая ради универсальности готова мириться с существованием CMake внутри ESP-IDF, но не позволяет ему управлять всем процессом. Поэтому работать с ESP-IDF из PlatformIO в целом сложнее, чем напрямую в ESP IDE. Но такова цена «универсальности».


Как происходит загрузка программ (прошивка)?

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

Способы прошивки ESP32 с использованием кабеля

Для прошивки ESP с помощью кабеля существует несколько вариантов:

  • через UART0 и USB-UART мост (не важно – встроенный в плату или внешний)
  • через встроенный в чип USB-JTAG-интерфейс (только для новых серий S2, S3, C3, C6 и т.д.)
  • аппаратный JTAG-адаптер

UART — это «база», которая есть всегда. USB — это удобство для новых чипов. JTAG — это скальпель для глубокой отладки. Кроме этого, существует четвертная возможность обновления ранее залитой прошивки “по воздуху”, про который я уже рассказывал здесь, поэтому сегодня не будем заострять на этом внимание.

1. UART (универсальный и самый распространённый способ)

Это классический метод, который работает на всех чипах ESP32, включая классику и ESP8266. Суть этого метода заключается в том, что всё общение чипа с компьютером происходит для специально зарезервированный для этого аппаратный контроллер универсального последовательного порта – UART0. Для подключения такого порта к компьютеру вам потребуется адаптер-переходник USB-UART (их еще называют USB-TTL, USB-мост, и т.д.). Как правило эти переходники сделаны на чипах CH340 или CP2102, но могут быть и еще варианты – почитать про такие адаптеры можно здесь.

Подобный адаптер вам понадобится далеко не всегда: на большинстве плат разработки (типа DevKit) уже стоит такой переходник, и вам достаточно просто подключить плату по USB кабелем. Режим загрузки при этом часто включается автоматически (esptool сделает это самостоятельно через сигналы DTR/RTS и “волшебные транзисторы”).

Если же на используемой вами плате вообще нет USB-порта, а есть только разъем с маркировкой UART, то адаптер таки вам нужен – выберите подходящий по вкусу и параметрам. Подключать его можно только тремя проводами (RX, TX и GND), но тогда придется переводить чип в режим программирования “вручную” – как это сделать описано ниже.

 

2. USB (только для новых чипов: S2, S3, C3, C6 и т.д.)

Этот способ прошивки предполагает наличие на плате USB-интерфейса, который подключен специальными выводам чипа USB D+ и USB D- (USB-Serial-JTAG). В этом случае вы просто подключаете кабель напрямую в USB-порт чипа, если он выведен на плате. Никаких переходников не нужно – этот интерфейс умеет притворяться и последовательным портом (для прошивки), и JTAG-отладчиком.

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

 

3. Аппаратный JTAG (для отладки и продвинутых пользователей)

Это не столько способ «прошить» ESP, сколько способ отладить программу, то есть выявить в ней всевозможные ошибки и баги. В этом случае вы подключаете внешний JTAG-отладчик (например, ESP-Prog или J-Link) к специальным пинам чипа. Затем через JTAG можно не только залить прошивку, но и останавливать процессор, смотреть значения переменных, ставить точки останова и выполнять код по шагам.

Это инструмент для профессионалов, когда обычного printf в консоль уже не хватает для отладки. В Arduino IDE этот механизм не используется, а в ESP-IDF — это мощный инструмент отладки.


Перевод ESP в режим прошивки вручную

Как мы уже обсуждали выше, при запуске CPU запускается загрузчик первой очереди и первое что он делает, когда малость оклемается – считывает и запоминает состояние специальных служебных “выводов обвязки” или Strapping pins. С их помощью любой инженегр может настраивать режимы работы чипа еще до его запуска: например переключить напряжение питания flash-памяти или включить режим загрузки прошивки. Это своеобразные переключатели на пульте управления. Состояние всех Strapping pins после запуска записывается в специальные регистры-“защелки”, что дает вам возможность использовать эти выводы для любых своих целей. Но при запуске CPU изменять их состояние без понимания того, что вы делаете – я бы не советовал.

Strapping pins на этой схеме помечены желтыми прямоугольниками

Один из Strapping pins (иногда их несколько) управляет переключением ROM-загрузчика в режим ожидания прошивки. На ESP32 классической серии – это всем известный GPIO0 (и еще GPIO2 ему в помощь) – по низкому уровню на этих выводах загрузчик понимает, что сейчас будут новые данные через USB или UART и готовится к их приему. Обычно на отладочных платах с ESP32 к выводу GPIO0 подключена кнопка boot или download или gpio0. Из этого следует, что для перевода чипа (точнее – загрузчика) в режим загрузки прошивки “вручную” необходимо выполнить следующие манипуляции:

  • Нажать и удерживать кнопку boot или подтянуть GPIO0 к земле
  • Нажать кнопку reset (иногда en) или подать питание на плату
  • Отпустить кнопку boot

Последовательность сигналов на входах GPIO0 и EN, необходимая для перевода ESP в режим загрузки

После этого в мониторе терминала, если он подключен к плате, появится сообщение “waiting for download“, это как раз и означает, что ROM-bootloader готов к принятию данных прошивки. 

rst:0x1 (POWERON_RESET),boot:0x3 (DOWNLOAD_BOOT(UART0/UART1/SDIO_REI_REO_V2))
waiting for download

 

Однако выполнять эти нехитрые манипуляции далеко не всегда необходимо: если вы используете вместо “чистого” модуля какую-либо “плату разработчика” типа ESP32-DevKit (например как на рисунке выше), оснащенную USB-to-UART мостом с со схемой перевода в режим прошивки (иногда ее называют “волшебные транзисторы Espressif”, подробнее можно почитать в другой статье), или USB-JTAG портом, то ничего нажимать не нужно – все происходит автоматически.

Как работает эта магия – мы обсудим ниже.


Схема управления переводом чипа в режим прошивки

Для того, чтобы esptool могла самостоятельно управлять процессом прошивки, необходимо чтобы контакт DTR микросхемы моста USB-UART (например, CP210x, CH340) был подключен к контакту GPIO0 чипа ESP, а сигнал RTS был соединен с выводом EN (CHIP_PU) ESP. Активный уровень сигналов в обоих случаях должен быть низкий. Изменяя уровни на этих выводах в правильной последовательности, esptool сможет перевести ESP в режим загрузки или сбросить её.

Для справки: эти сигналы являются частью “расширенной” версии COM-интерфейсов и по изначальной задумке используются для аппаратного управления потоком данных:

  • RTS – запрос на передачу (Request to Send): выход запроса передачи данных, используется в аппаратном контроле передачи данных по каналу связи RTS/CTS
  • DTR – готовность приемника данных (Data Terminal Ready): выход сигнала готовности данного терминала к обмену данными

Теоретически подключать DTR и RTS напрямую к ESP можно, но с оговорками и рисками. Проблема прямого подключения в том, что esptool использует активный низкий уровень сигналов DTR и RTS (что, собственно, чипу и требуется). Если соединить их напрямую:

  • Когда esptool не активен (или последовательный порт только открыт), обе линии могут оказаться в низком уровне. Это приведет к тому, что EN (сброс) будет прижат к земле, и чип постоянно будет находится в состоянии сброса, не имея возможности работать. То есть как только вы откроете любой терминал (или любая другая программа инициализирует тот же самый порт), есть большой риск, что обе линии одновременно уйдут в низкий уровень, и чип «зависнет» в сбросе.
  • Проблемы с некоторыми драйверами/ОС: В документации Espressif прямо сказано, что при прямом подключении RTS и DTR к EN и GPIO0 могут возникнуть проблемы, когда ESP32 не загружается и не выдаёт логи, пока вы вручную не отключите эти линии в терминале.

Большинство официальных плат (включая все платы Espressif) спроектированы так, чтобы этой проблемы не было. Для этого используется специальная схема, которую часто называют «автоматическим загрузчиком» (auto-reset circuit) или иногда «схема на волшебных транзисторах». Их задача — создать «исключающее ИЛИ» (XOR) для сигналов DTR и RTS, чтобы никогда не позволить линиям EN (сброс) и GPIO0 (загрузка) оказаться в низком уровне одновременно.

Открытый Q1 блокирует работу Q2 и наоборот – открытый Q2 блокирует Q1

Транзисторная схема гарантирует, что если оба сигнала (DTR и RTS) находятся в одинаковом состоянии (оба уровня высокие или оба низкие), то обе линии EN и GPIO0 остаются с высокими уровнями (подтянутыми к VCC через резисторы). Чип спокойно работает, и случайное открытие монитора порта не вызовет его перезагрузку. Но esptool по прежнему может ими управлять, дергая их по отдельности.

Внимательные читатели, почитавшие предыдущий раздел, могут возразить: а как же Гриша? работает перевод в режим прошивки, ведь для этого нужно одновременно подтянуть GPIO0 и EN к земле и потом “отпустить” в правильной последовательности? Эта проблема решается с помощью обычного конденсатора, подключенного к выводу EN – он немного “задерживает” отключение сигнала сброса и esptool успевает за это время прижать GPIO0 к земле. Но иногда емкости конденсатора не достаточно, и схема не работает “как надо. Обо всем этом вы можете почитать в другой статье более подробно.


Процесс загрузки двоичных данных прошивки на ESP

Отлично, мы подошли к самому интересному моменту. У нас всё готово: программу (или скетч) скомпилировали, “бинарник” (файл с кодом прошивки в машинных кодах) .bin получили. Плату к компьютеру подключили и научились её сбрасывать и переводить в режим прошивки. Как же происходит непосредственно процесс загрузки .bin в чип?

Когда вы запускаете команду idf.py flash, esptool получает команду записать три (или больше) файла по строго определённым адресам во Flash. Адреса не случайны — они прописаны в вашей (или не вашей) таблице разделов и зашиты в логику загрузчика. Типичная команда выглядит так (пример):

esptool write_flash 0x1000 bootloader.bin 0x8000 partition-table.bin 0x10000 app.bin

Что это значит:

  • 0x1000: Сюда ложится загрузчик второй очереди, именно его первым делом найдёт и запустит ROM-загрузчик.
  • 0x8000: Сюда ложится таблица разделов (по умолчанию для большинства конфигураций, но это можно изменить). Это «карта» Flash, где написано, что по адресу 0x10000 начинается раздел factory, а, скажем, по адресу 0x21000 — раздел ota_0.
  • 0x10000: Сюда ложится ваш основной .bin с программой. Но esptool не «знает», что это – factory или OTA. Он просто пишет файл по адресу, который ему указали в команде, ничего более.

Ключевой момент: При обычной прошивке по кабелю (idf.py flash) вы всегда записываете программу в один и тот же раздел, который был указан в команде (обычно это factory или ota_0, если factory нет). Это не esptool «умеет автоматически определять раздел», а система сборки ESP-IDF заранее вычислила адрес и передала его esptool в качестве параметра.

Когда вы запускаете idf.py flash, система сборки (CMake/Ninja) уже прочитала вашу таблицу разделов (CSV-файл) и знает:

  • Если в таблице раздело определён factory, и он первый, то адрес для прошивки по кабелю будет указывать на него, например 0x10000.
  • Если factory не определён, а первым идёт ota_0, система сборки всё вычислит фактический адрес ota_0 и передаст уже его адрес.

Поэтому можно совершенно спокойно не определять factory в таблице разделов и использовать только ota_0 + ota_1. Система сборки всё равно вычислит адрес и прошьёт прошивку в ota_0, а загрузчик при отсутствии действительной информации OTA загрузит ota_0 как «первый найденный app».

Stub: временный «суслик» в RAM

Мы уже говорили, что ROM-загрузчик нельзя обновить. Именно поэтому esptool использует трюк: он загружает в оперативную память (IRAM) чипа крошечную программу — Flasher Stub. Это не часть вашей прошивки, она живёт только в RAM, пока идёт процесс записи.

Представьте, что ROM-загрузчик — это «заводской робот», который умеет делать свою работу, но довольно медленно и «не всему научен». Stub — это «временный высококвалифицированный специалист», которого вы наняли на 5 минут. Он использует более оптимизированные процедуры UART и умеет обходить баги заводского робота. Как только вы отключаете питание или делаете сброс, этот специалист исчезает навсегда.

После загрузки Stub в RAM, esptool отправляет команду MEM_END с адресом точки входа. Stub запускается и отправляет хосту специальный пакет OHAI (в hex: 0xC0 0x4F 0x48 0x41 0x49 0xC0). Это единственный «непрошеный» пакет, который чип отправляет хосту, как бы говоря: «Привет, я на месте, готов работать». Его вы не видите в мониторе, но он есть.

 

Вычисление контрольных сумм: как проверяется целостность

Когда вы записываете .bin файл, esptool не просто непрерывно «льёт» байты в порт. Он также заботится о том, чтобы данные дошли без искажений. В мире последовательных портов это критично, потому что помехи или сбои питания могут испортить байты.

  • Файл прошивки разбивается на блоки (обычно по 16 КБ). В команде FLASH_BEGIN esptool указывает, сколько таких блоков будет отправлено.
  • Отправка данных: затем в порт идут команды FLASH_DATA, каждая из которых содержит блоки с данными.
  • После того как все данные отправлены и записаны во Flash, esptool не доверяет тому, что всё прошло гладко. Он запрашивает у чипа MD5-хеш записанной области (команда FLASH_MD5SUM). На стороне компьютера esptool также вычисляет MD5 от исходного файла. Если хеши совпадают — всё отлично. Если нет — esptool сообщит об ошибке, и прошивка будет считаться неудачной.

 

Финальная перезагрузка

Если все прошло гладко, esptool инициирует перезагрузку чипа, дабы ROM-загрузчик смог запустить только что записанные данные. Esptool имеет отдельный параметр -after hard_reset, но не как отдельный «придаток» к команде записи, а как опция esptool по умолчанию -after hard-reset срабатывает автоматически после завершения любой операции, включая write-flash, если только вы явно не переопределили поведение.

В результате esptool после записи всех файлов автоматически дёргает RTS (или выполняет другую последовательность сброса для USB), чтобы чип вышел из режима загрузчика и запустил вашу программу. А в консоли вы в это время увидите:

Hard resetting via RTS pin...

Это не ошибка, это информационное сообщение! Это просто признак того, что ваша ESP сейчас перезагрузится.


Ну а на этом можно завершить данное повествование. Засим с вами прощаюсь, до новых встреч. Всем добра.

 

 

Оцените по шкале от 1 до 5 - насколько эта статья была Вам полезна?
[ 5 из 5, всего 1 оценок ]

-= Каталог статей (список по разделам) =-   -= Архив статей (плитки, все подряд) =-

Небольшой анонимный опрос: какие статьи вы предпочитаете?

1 комментарий для “Внутренняя кухня ESP32: система сборки и загрузки прошивки – как это работает?”

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

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

65  +    =  67