# 应用程序的启动流程

[English]

本文将会介绍 ESP32-H21 从上电到运行 `app_main` 函数中间所经历的步骤（即启动流程）。

宏观上，该启动流程可以分为如下 3 个步骤：

1. 一级 (ROM) 引导加载程序 被固化在了 ESP32-H21 内部的 ROM 中，它会从 flash 的 0x0 偏移地址处加载二级引导加载程序至 RAM (IRAM & DRAM) 中。

2. 二级引导加载程序 从 flash 中加载分区表和主程序镜像至内存中，主程序中包含了 RAM 段和通过 flash 高速缓存映射的只读段。

3. 应用程序启动阶段 运行，这时 RTOS 调度器启动，接着运行 `main_task`，从而执行 `app_main`。

下面会对上述过程进行更为详细的阐述。

## 一级 (ROM) 引导加载程序

SoC 复位后，CPU 会立即开始运行，执行所有的初始化操作。复位向量代码位于 ESP32-H21 芯片掩膜 ROM 处，且不能被修改。

复位向量调用的启动代码会根据 `GPIO_STRAP_REG` 寄存器的值来确定 ESP32-H21 的启动模式，该寄存器保存着复位后 bootstrap 引脚的电平状态。根据不同的复位原因，程序会执行如下操作：

1. 从深度睡眠模式复位：如果 `RTC_CNTL_STORE6_REG` 寄存器的值非零，且 `RTC_CNTL_STORE7_REG` 寄存器中的 RTC 内存的 CRC 校验值有效，那么程序会使用 `RTC_CNTL_STORE6_REG` 寄存器的值作为入口地址，并立即跳转到该地址运行。如果 `RTC_CNTL_STORE6_REG` 的值为零，或 `RTC_CNTL_STORE7_REG` 中的 CRC 校验值无效，又或通过 `RTC_CNTL_STORE6_REG` 调用的代码返回，那么则像上电复位一样继续启动。 注意：如果想在这里运行自定义的代码，可以参考 深度睡眠 文档里面介绍的深度睡眠存根机制方法。

2. 上电复位、软件 SoC 复位、看门狗 SoC 复位：检查 `GPIO_STRAP_REG` 寄存器，判断是否请求自定义启动模式，如 UART 下载模式。如果是，ROM 会执行此自定义加载模式，否则会像软件 CPU 复位一样继续启动。请参考 ESP32-H21 技术规格书了解 SoC 启动模式以及具体执行过程。

3. 软件 CPU 复位、看门狗 CPU 复位：根据 EFUSE 中的值配置 SPI flash，然后尝试从 flash 中加载代码，这部分将会在后面一小节详细介绍。

备注

- 正常启动模式下会使能 RTC 看门狗，因此，如果进程中断或停止，看门狗将自动重置 SOC 并重复启动过程。如果 strapping GPIOs 已更改，则可能导致 SoC 陷入新的启动模式。

- 芯片在其生命周期内可能会进行多次工程变更 (ECO)，如工程变更不影响 ROM 行为，例如，仅用于修复数字逻辑、调整模拟电路或优化生产工艺等， ROM 固件往往不会变动。因此启动日志中 ROM 打印的版本号可能低于实际芯片版本号。

## 二级引导加载程序

在 ESP-IDF 中，存放在 flash 的 0x0 偏移地址处的二进制镜像就是二级引导加载程序。二级引导加载程序的源码可以在 ESP-IDF 的 components/bootloader 目录下找到。ESP-IDF 使用二级引导加载程序可以增加 flash 分区的灵活性（使用分区表），并且方便实现 flash 加密，安全引导和空中升级 (OTA) 等功能。

当一级 (ROM) 引导加载程序校验并加载完二级引导加载程序后，它会从二进制镜像的头部找到二级引导加载程序的入口点，并跳转过去运行。

二级引导加载程序默认从 flash 的 0x8000 偏移地址处（可配置的值）读取分区表。请参考 分区表 获取详细信息。引导加载程序会寻找工厂分区和 OTA 应用程序分区。如果在分区表中找到了 OTA 应用程序分区，引导加载程序将查询 `otadata` 分区以确定应引导哪个分区。更多信息请参考 空中升级 (OTA)。

关于 ESP-IDF 引导加载程序可用的配置选项，请参考 引导加载程序 (Bootloader)。

对于选定的分区，二级引导加载程序将从 flash 逐段读取二进制镜像：

- 对于在内部 IRAM（指令 RAM） 或 DRAM（数据 RAM） 中具有加载地址的段，将把数据从 flash 复制到它们的加载地址处。

- 对于一些加载地址位于 DROM（数据存储在 flash 中） 或 IROM（代码从 flash 中运行） 区域的段，通过配置 flash MMU，可为从 flash 到加载地址提供正确的映射。

一旦处理完所有段（即加载了代码并设置了 flash MMU），二级引导加载程序将验证应用程序的完整性，并从二进制镜像文件的头部寻找入口地址，然后跳转到该地址处运行。

## 应用程序启动阶段

应用程序启动包含了从应用程序开始执行到 `app_main` 函数在主任务内部运行前的所有过程。可分为三个阶段：

- 硬件和基本 C 语言运行环境的端口初始化。

- 软件服务和 FreeRTOS 的系统初始化。

- 运行主任务并调用 `app_main`。

备注

通常不需要了解 ESP-IDF 应用程序初始化的所有阶段。如果需要仅从应用程序开发人员的角度了解初始化，请跳至 运行主任务。

### 端口初始化

ESP-IDF 应用程序的入口是 components/esp_system/port/cpu_start.c 文件中的 `call_start_cpu0` 函数。这个函数由二级引导加载程序执行，并且从不返回。

该端口层的初始化功能会初始化基本的 C 运行环境 ("CRT")，并对 SoC 的内部硬件进行了初始配置。

- 为应用程序重新配置 CPU 异常（允许应用程序中断处理程序运行，并使用为应用程序配置的选项来处理 严重错误，而不是使用 ROM 提供的简易版错误处理程序处理。

- 如果没有设置选项 CONFIG_BOOTLOADER_WDT_ENABLE，则不使能 RTC 看门狗定时器。

- 初始化内部存储器（数据和 bss）。

- 完成 MMU 高速缓存配置。

- 将 CPU 时钟设置为项目配置的频率。

`call_start_cpu0` 完成运行后，将调用在 components/esp_system/startup.c 中找到的“系统层”初始化函数 `start_cpu0`。

### 系统初始化

主要的系统初始化函数是 `start_cpu0`。默认情况下，这个函数与 `start_cpu0_default` 函数弱链接。这意味着可以覆盖这个函数，增加一些额外的初始化步骤。

主要的系统初始化阶段包括：

- 如果默认的日志级别允许，则记录该应用程序的相关信息（项目名称、应用程序版本 等）。

- 初始化堆分配器（在这之前，所有分配必须是静态的或在堆栈上）。

- 初始化 esp_libc 组件的系统调用和时间函数。

- 配置断电检测器。

- 根据 串行控制台配置 设置 libc stdin、stdout、和 stderr。

- 执行与安全有关的检查，包括为该配置烧录 efuse（包括 永久限制 ROM 下载模式)。

- 初始化 SPI flash API 支持。

- 调用全局 C++ 构造函数和任何标有 `__attribute__((constructor))` 的 C 函数。

二级系统初始化允许单个组件被初始化。如果一个组件有一个用 `ESP_SYSTEM_INIT_FN` 宏注释的初始化函数，它将作为二级初始化的一部分被调用。

### 运行主任务

在所有其他组件都初始化后，主任务会被创建，FreeRTOS 调度器开始运行。

做完一些初始化任务后（需要启动调度器），主任务在固件中运行应用程序提供的函数 `app_main`。

运行 `app_main` 的主任务有一个固定的 RTOS 优先级（比最小值高）和一个 可配置的堆栈大小。

与普通的 FreeRTOS 任务（或嵌入式 C 的 `main` 函数）不同，`app_main` 任务可以返回。如果 `app_main` 函数返回，那么主任务将会被删除。系统将继续运行其他的 RTOS 任务。因此可以将 `app_main` 实现为一个创建其他应用任务然后返回的函数，或主应用任务本身。

此文档对您有帮助吗？

- 反馈已收到，谢谢！
- 如果您有其他意见，欢迎填写乐鑫文档反馈表。

- 我们重视您的反馈。
- 您可以填写乐鑫文档反馈表告诉我们如何改进该文档。