В этой документации (перевод [1]) объясняется, что происходит перед тем, как будет запущена функция app_main приложения ESP-IDF.
На высоком уровне процесс запуска (startup) можно рассматривать следующим образом:
1. First stage bootloader. Это код загрузчика первой стадии, находящийся в ROM. Он загружает из SPI-flash образ загрузчика второй стадии в ОЗУ (IRAM и/или DRAM). Код загрузчика второй стадии находится в SPI-flash со смещением 0x1000.
2. Second stage bootloader. Это загрузчик второй стадии, он загружает из flash таблицу разделов [2] и образ основного приложения. Включает в себя как сегменты кода в ОЗУ, так и сегменты только для чтения, отображаемые в адресное пространство CPU через кэш flash.
3. Application startup. В этой точке запускается второй CPU и планировщик FreeRTOS [3].
Этот процесс запуска описывается в последующих секциях.
[First stage bootloader]
После сброса SoC немедленно запустится ядро PRO CPU и начнет выполнить код по вектору сброса (reset vector), в то время как APP CPU будет удерживаться в состоянии сброса. Во время процесса startup ядро PRO CPU делает всю начальную инициализцию. Сброс APP CPU снимается функцией call_start_cpu0, вызываемой кодом startup приложения. Код вектора сброса находится в mask ROM кристалла ESP32, и он не может быть модифицирован.
Код startup, вызываемый вектором сброса, определяет режим загрузки (boot mode) путем проверки регистра GPIO_STRAP_REG для оценки состояния ножек управления загрузкой (bootstrap pin states). В зависимо от причины сброса, может происходить следующее:
1. Сброс из состояния глубокого сна (deep sleep reset): если значение в RTC_CNTL_STORE6_REG ненулевое, и значение CRC памяти RTC в RTC_CNTL_STORE7_REG правильное, то используется RTC_CNTL_STORE6_REG в качестве адреса точки входа, и на него делается безусловный переход. Если RTC_CNTL_STORE6_REG равно 0, или RTC_CNTL_STORE7_REG содержит неправильную CRC, или код, на который указывает RTC_CNTL_STORE6_REG, выполняет возврат, то выполняется такая же загрузка, как загрузка после сброса/включения питания (power-on reset). Замечание: для запуска пользовательского кода в этой точке предоставляется механизм заглушки глубокого сна (deep sleep stub mechanism). Подробности см. в документации [4].
2. Для сброса от включения питания (power-on reset), программного сброса (software SOC reset) и сброса от сторожевого таймера (watchdog SOC reset): проверяется регистр GPIO_STRAP_REG, чтобы определить, запрашивается ли пользовательский режим загрузки (custom boot mode, такой как UART Download Mode). Если этот случай, то custom loader mode выполняется из ROM. Иначе происходит такая же загрузка, как для программного сброса (software CPU reset). Проконсультируйтесь с даташитом ESP32 для получения описания режимов загрузки кристалла (SoC boot modes), и как их запускать.
3. Для программного сброса (software CPU reset) сброса от сторожевого таймера (watchdog CPU reset): конфигурируется SPI flash на основе значений EFUSE, и делается попытка загрузить код из flash. Этот шаг более подробно описан далее.
Важное замечание: во время режимов нормальной загрузки RTC watchdog разрешен, так что если процесс загрузки прервется или будет приостановлен, то сторожевой таймер автоматически сбросит SOC, и процесс загрузки запустится заново. Это может привести к тому, что SoC переключится в новый режим загрузки, если состояние ножек управления загрузкой (strapping GPIO) поменялось.
Двоичный образ второй стадии загрузки грузится из flash с адреса 0x1000. Если используется Secure Boot, то первый сектор flash 4 kB используется для хранения secure boot IV и цифровой подписи (digest) образа загрузчика (bootloader image). Иначе этот сектор не используется.
[Second stage bootloader]
В ESP-IDF двоичный образ, находящийся в памяти SPI-flash со смещением 0x1000, является загрузчиком второй стадии (second stage bootloader). Исходный код загрузчика второй стадии находится в директории components/bootloader среды разработки ESP-IDF [5]. Загрузчик второй стадии в ESP-IDF используется для добавления гибкости в распределении пространства flash-памяти (с помощью таблицы разделов), что позволяет осуществлять различные потоки загрузки, связанные с шифрованием (flash encryption), безопасной загрузкой (secure boot) и обновлением по радио (over-the-air updates, OTA).
Когда загрузчик первой стадии завершит проверку и загрузку загрузчика второй стадии, он делает безусловный переход на точку входа, которая находится в заголовке двоичного образа.
Загрузчик второй стадии считывает таблицу разделов, которая по умолчанию находится в памяти flash со смещением 0x8000 (это значение конфигурируется). Подробную информацию см. в описании таблицы разделов [2]. Загрузчик в этой таблице находит разделы factory и OTA app. Если были найдены разделы OTA, то загрузчик сверяется с информацией, записанной в разделе otadata, чтобы определить, из какого раздела следует загружать образ приложения. Для дополнительной информации см. описание технологии OTA [6].
Полное описание опций конфигурации, доступных для загрузчика ESP-IDF, см. в документации [7].
Для выбранного раздела загрузчик второй стадии считывает двоичный образ из flash по одному сегменту за один раз:
• Для сегментов с адресом загрузки во внуреннее ОЗУ IRAM (Instruction RAM) или DRAM (Data RAM), содержимое копируется из flash в адрес загрузки. • Для сегментов, у которых адрес загрузки находится в регионах DROM (данные сохранены в flash) или IROM (код выполняется из flash), конфигурируется flash MMU для предоставления корректного отображения областей flash в адрес загрузки.
Обратите внимание, что загрузчик второй стадии конфигурирует flash MMU для обоих ядер PRO CPU и APP CPU, однако разрешает flash MMU только для PRO CPU. Причина такого поведения в том, что bootloader второй стадии загружается в регион памяти, который используется как кэш для APP CPU. Задача разрешения кэша для APP CPU перекладывается на приложение.
Как только все сегменты были обработаны - т. е. код был загружен, и flash MMU настроен - загрузчик второй стадии проверяет целостность загруженного приложения, затем выполняет безусловный переход на точку входа в приложения, которая находится а заголовке двоичного образа.
[Application startup]
Application startup обозначаются все процессы, которые происходят между моментом перехода на точку входа в приложение и моментом запуска функции app_main, которая находится в теле главной задачи FreeRTOS (main task). Процессы application startup делятся на 3 стадии:
• Инициализация порта поддержки аппаратуры и базовой среды выполнения кода языка C (C runtime environment). • Инициализация системных программных сервисов и FreeRTOS. • Запуск main task и вызов функции app_main.
Важное замечание: понимание всех стадий инициализации приложения ESP-IDF не является обязательным требованием для создания корректно работающего приложения. Чтобы понимать инициализацию с точки зрения разработчика, пропустите дальнейшее описание до секции "Запуск main task".
Инициализация порта поддержки аппаратуры. Точка входа в приложение ESP-IDF вызывает функцию call_start_cpu0, код которой находится в components/esp_system/port/cpu_start.c. Эта функция выполняется загрузчиком второй стадии, и из неё никогда не делается возврат.
Эта функция инициализации слоя порта настраивает базовый C Runtime Environment (CRT) и выполняет начальную конфигурацию внутренней аппаратуры SoC:
• Переконфигурирует исключения CPU для приложения, что позволяет запускаться обработчикам прерываний приложения, и инициирует обработку неустранимых ошибок (Fatal Errors) в соответствии с опциями, сконфигурированными для приложения вместо того, чтобы использовался упрощенный обработчик ошибок, предоставленный кодом из ROM. • Если опция CONFIG_BOOTLOADER_WDT_ENABLE не установлена, то сторожевой таймер (RTC watchdog) будет запрещен. • Инициализируется внутренняя память (data & bss). • Завершение конфигурирования кэша MMU. • Разрешение PSRAM, если это сконфигурировано. • Настраивается тактирование CPU на частоты, сконфигурированные для проекта. • Переконфигурируется main SPI flash на основе настроек из заголовка приложения (app header). Это необходимо для обеспечения совместимости с версиями загрузчика до ESP-IDF V4.0, см. секцию "Совместимость загрузчика" документации [7]. • Если приложение сконфигурировано для работы на нескольких ядрах, запускается другое ядро и происходит ожидание завершения его инициализации (происходит аналогично инициализации "слоя порта" в функции call_start_cpu1).
Как когда call_start_cpu0 завершает все эти операции, она вызывает инициализацию "системного слоя" (system layer) в функции start_cpu0, которая находится в components/esp_system/startup.c. Другие ядра будут также завершать инициализацию слоя порта и вызывать start_other_cores, которая находится в том же файле.
Инициализация системы. Основную инициализацию системы выполняет функция start_cpu0. По умолчанию эта функция получает weak-линковку на функцию start_cpu0_default. Это значит, что эту функцию можно переопределить в коде пользователя, чтобы добавить дополнительные шаги по инициализации.
Стадия первичной инициализации системы включает следующие действия:
• Вывод в лог информации о приложении (project name, App Version, и т. п.), если это разрешено уровнем лога по умолчанию. • Инициализируется управление кучей (heap allocator). До этой точки все выделения памяти должны быть либо статическими, либо память должна выделяться через стек. • Инициализация компонента newlib syscalls и функций времени. • Конфигурируется детектор сбоя по питанию (brownout detector). • Настраиваются libc-потоки ввода/вывода stdin, stdout и stderr в соответствии с конфигурацией последовательной консоли. • Выполняются проверки, связанные с обеспечением безопасности, включая прошивку efuses, которые должны быть прошиты для этой конфигурации (это включает запрет режима загрузки ROM на ESP32 V3, CONFIG_ESP32_DISABLE_BASIC_ROM_CONSOLE). • Инициализируется поддержка SPI flash API. • Производится вызов глобальных конструкторов C++ и любых других функций C, помеченных как __attribute__((constructor)).
Вторичная инициализации системы позволяет настроить отдельные компоненты. Если у компонента есть функция инициализации, анонсированная макросом ESP_SYSTEM_INIT_FN, то она будет вызвана как часть процесса вторичной инициализации.
Запуск main task. После того, как все другие компоненты были инициализированы, создается main task, и запускается планировщик FreeRTOS. После некоторых подготовительных действий (необходимых для запуска планировщика), main task запустит функцию app_main из кода firmware. Содержимое функции определяется разработчиком приложения, в ней может находится как основной алгоритм приложения, так и создаваться другие задачи приложения.
Задача main task, которая запускает функцию app_main, имеет фиксированный приоритет RTOS (на единицу выше минимального приоритета), и конфигурируемый размер стека. Привязка main task к определенному ядру (core affinity) также конфигурируется: CONFIG_ESP_MAIN_TASK_AFFINITY.
В отличие от обычных задач FreeRTOS (которые реализованы как традиционные main-функции языка C), задаче app_main разрешено выполнять возврат. Если это произошло, то задача очищается, и система FreeRTOS продолжает выполнять запущенные из тела app_main задачи традиционным для RTOS образом. Таким образом, можно реализовать app_main либо как функцию, которая просто создает все другие задачи приложения, либо можно использовать app_main для выполнения самого кода приложения.
Second core startup. Это процесс запуска второго ядра APP CPU, который происходит аналогично, однако он несколько проще, чем запуск первого ядра:
Когда работает инициализация системы, код PRO CPU устанавливает точку входа для APP CPU, снимает сброс для APP CPU и ждет установки глобального флага, который должен установить APP CPU (установка флага покажет, что ядро APP CPU запустилось). Как только это было выполнено, APP CPU делает переход в функцию call_start_cpu1, которая находится в components/esp_system/port/cpu_start.c.
В течение выполнения инициализации, которую ядро PRO CPU делает в коде start_cpu0, ядро APP CPU выполняет функцию start_cpu_other_cores. Подобно start_cpu0, эта функция также ликована как weak, и её реализация по умолчанию находится в функции start_cpu_other_cores_default (эта функция может быть заменена пользователем на другую функцию приложения с таким же именем).
Функция start_cpu_other_cores_default делает некоторую специфичную для ядра системную инициализацию и ждет, когда ядро PRO CPU запустит планировщик FreeRTOS, в этот момент выполняется esp_startup_start_app_other_cores. Это другая weak-линкованная функция, реализация которой находится в esp_startup_start_app_other_cores_default.
По умолчанию esp_startup_start_app_other_cores_default ничего не делает, просто прокручивает цикл ожидания (busy-waiting loop) до тех пор, пока планировщик PRO CPU не вызовет прерывание для запуска планировщика на APP CPU.
[Словарик]
APP CPU, PRO CPU условное обозначение двух ядер ESP32, на которые в примерах среды разработки ESP-IDF возлагаются различные задачи приложения.
MMU Memory Management Unit, блок управления памятью.
OTA Over The Air, технология загрузки (обновления) по радиоканалу.
PSRAM Pseudostatic RAM (которая также может называться PSDRAM) это динамическое ОЗУ со встроенным контроллером обновления и схемой управления адресами, которые предоставляют интерфейс доступа, аналогичные статическому ОЗУ (static RAM, SRAM).
RTC Real Timer Clock, таймер реального времени.
SoC System on Chip, система на кристалле.
SRAM static RAM, статическое ОЗУ.
[Ссылки]
1. ESP32 Application Startup Flow site:docs.espressif.com. 2. ESP32: таблицы разделов. 3. ESP-IDF FreeRTOS Task API. 4. ESP32 Deep Sleep Wake Stubs site:docs.espressif.com. 5. Установка среды разработки ESP-IDF для ESP32. 6. ESP32: обновление по радиоканалу (OTA). 7. ESP32 Bootloader. 8. ESP32 How To Use PSRAM site:thingpulse.com. |