第二章 FreeRTOS ESP32应用程序的启动流程
在学习3个步骤之前,先了解下ESP32双核架构,下面表格整理了PRO_CPU和APP_CPU的核心特点与分工,可以让我们快速了解双核架构。特性维度PRO_CPU (协议CPU, 通常为 Core 0)APP_CPU (应用CPU, 通常为 Core 1)启动顺序首先启动,负责整个系统的初始化和引导稍后启动,由PRO_CPU在系统初始化阶段解除其复位状态默认主要职责处理无线网络协议栈
本章主要分析ESP32系统启动流程,会介绍 ESP32-S3 从上电到运行app_main 函数中间所经历的步骤(即启动流程),并结合代码分析相关流程。
本章分为如下几部分:
2.1 ESP32双核系统架构介绍
2.2 ESP32应用程序的启动3个步骤
2.3 结合代码分析启动流程
2.1 ESP32双核系统架构介绍
在学习3个步骤之前,先了解下ESP32双核架构,下面表格整理了PRO_CPU和APP_CPU的核心特点与分工,可以让我们快速了解双核架构。
|
特性维度 |
PRO_CPU (协议CPU, 通常为 Core 0) |
APP_CPU (应用CPU, 通常为 Core 1) |
|---|---|---|
|
启动顺序 |
首先启动,负责整个系统的初始化和引导 |
稍后启动,由PRO_CPU在系统初始化阶段解除其复位状态 |
|
默认主要职责 |
处理无线网络协议栈(Wi-Fi/蓝牙)、系统关键任务 |
运行用户应用程序的主要逻辑、上层业务代码 |
|
中断处理 |
支持所有类型的中断 |
支持部分中断 |
|
性能优化侧重 |
高实时性、低延迟的通信任务 |
计算密集型或复杂的应用逻辑 |
表 2.1.1 ESP32双核架构介绍
(1)双核的协同工作
PRO_CPU和APP_CPU是两个独立的Xtensa LX6核心,它们不是主从关系,而是协同工作的伙伴。
- 对称地址空间:两个核心对内存和外围设备的地址映射是对称的,意味着它们使用相同的地址访问相同的内存区域和外设,为编程带来了便利;
- 共享内存通信:核心间主要通过共享内存进行数据交换。意味着一块内存区域可以被两个核心同时访问,从而实现数据共享;
- 需注意数据同步:共享内存也带来了挑战,最主要的是数据竞争问题。当两个核心试图同时修改同一块数据时,可能导致数据损坏或程序行为异常。因此,必须使用互斥锁(Mutex)、信号量(Semaphore) 或队列(Queue) 等同步机制来保护共享资源。
(2)实际应用与配置
在ESP-IDF开发框架中,可以精细地控制任务在哪个核心上运行。
- 任务绑定核心:使用 xTaskCreatePinnedToCore()函数,你可以将特定的任务固定到RO_CPU或APP_CPU上执行。这让你能够根据任务的性质(如实时性要求、计算量)合理分配计算资源;
- 默认调度:如果不指定核心,FreeRTOS调度器会自行决定任务在哪个核心上运行,通常会优先考虑平衡负载;
- 获取核心ID:在代码中,可以使用 xPortGetCoreID()函数来获取当前任务正在哪个核心上运行,这对于调试和优化很有帮助。
(3)多核编程注意事项
要稳定高效地利用双核,需要注意以下几点:
- 缓存一致性:每个核心都有独立的缓存,当一方修改了共享内存中的数据后,需要确保另一方能看到最新的数据。ESP-IDF提供了如 esp_cache_writeback_addr()等API来管理缓存一致性。
- 避免核间死锁:如果两个核心互相等待对方持有的锁,就会导致死锁。设计时应避免复杂的锁依赖关系,并考虑使用带超时机制的锁获取函数。
总结:PRO_CPU像公司的后勤和通信保障部门,确保基础运营和内外联络畅通无阻;而APP_CPU则像是核心业务部门,专注于完成主要的产品和业务逻辑。 两者各司其职,又紧密协作,共同支撑起复杂的应用。
2.2 ESP32应用程序的启动3个步骤
ESP32-S3的启动过程是一个多阶段过程,从上电复位到执行熟悉的app_main函数,其间完成了硬件初始化、系统引导和操作系统加载等一系列关键操作,乐鑫官方也做了详细说明。下面这张流程图直观地展示了这一过程的三个主要阶段,可以先通过它建立一个整体印象。

图 2.1.1 ESP32应用程序的启动3个步骤
宏观上,该启动流程可以分为如下 3 个步骤:
- 一级 (ROM) 引导加载程序:被固化在了 ESP32-S3 内部的 ROM 中,它会从 flash 的 0x0 偏移地址处加载二级引导加载程序至 RAM (IRAM & DRAM) 中;
- 二级引导加载程序: 从 flash 中加载分区表和主程序镜像至内存中,主程序中包含了 RAM 段和通过 flash 高速缓存映射的只读段;
- 应用程序启动阶段:运行,这时第二个 CPU 和 RTOS 调度器启动,接着运行 main_task,从而执行 app_main。
(1) 一级引导加载程序(ROM Bootloader)
这个阶段由芯片内部掩膜ROM(Read-Only Memory)中固化的代码执行,用户无法修改。
复位与CPU初始化:SoC 复位后,PRO CPU 会立即开始运行,执行复位向量代码,而 APP CPU 仍然保持复位状态,在启动初期,所有初始化操作均由PRO CPU完成。在启动过程中,PRO CPU 会执行所有的初始化操作。APP CPU 的复位状态会在应用程序启动代码的 call_start_cpu0 函数中失效。复位向量代码位于 ESP32-S3 芯片掩膜 ROM 处,且不能被修改。
启动模式判断:复位向量调用的启动代码会根据 GPIO_STRAP_REG 寄存器的值来确定 ESP32-S3 的启动模式,该寄存器保存着复位后 bootstrap 引脚的电平状态。根据不同的复位原因,程序会执行如下操作:
- 从深度睡眠模式复位
如果 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 调用的代码返回,那么则像上电复位一样继续启动。 注意:如果想在这里运行自定义的代码,可以参考 深度睡眠 文档里面介绍的深度睡眠存根机制方法。
- 上电复位、软件 SoC 复位、看门狗 SoC 复位
检查 GPIO_STRAP_REG 寄存器,判断是否请求自定义启动模式,如 UART 下载模式。如果是,ROM 会执行此自定义加载模式,否则会像软件 CPU 复位一样继续启动。
- 软件 CPU 复位、看门狗 CPU 复位
根据 EFUSE 中的值配置 SPI flash,然后尝试从 flash 中加载代码。
备注:正常启动模式下会使能 RTC 看门狗,因此,如果进程中断或停止,看门狗将自动重置 SOC 并重复启动过程。如果 strapping GPIOs 已更改,则可能导致 SoC 陷入新的启动模式。
加载二级引导程序:在正常启动模式下,ROM程序会根据eFuse中的配置初始化SPI Flash,然后从Flash的 0x0偏移地址处将二级引导程序加载到内部RAM(IRAM/DRAM)中,并跳转执行。
(2) 二级引导加载程序 (Bootloader)
这个阶段的程序存储在外部Flash中,是ESP-IDF框架的一部分,开发者可以对其进行定制。
读取分区表:二级引导程序会从Flash的 0x8000偏移地址(此地址可配置)读取分区表(Partition Table)。分区表定义了Flash中各种映像(如应用程序、文件系统、数据等)的位置和大小。在 ESP-IDF 中,存放在 flash 的 0x0 偏移地址处的二进制镜像就是二级引导加载程序。二级引导加载程序的源码可以在 ESP-IDF 的 components/bootloader 目录下找到。ESP-IDF 使用二级引导加载程序可以增加 flash 分区的灵活性(使用分区表),并且方便实现 flash 加密,安全引导和空中升级 (OTA)等功能。
当一级 (ROM) 引导加载程序校验并加载完二级引导加载程序后,它会从二进制镜像的头部找到二级引导加载程序的入口点,并跳转过去运行。
二级引导加载程序默认从 flash 的 0x8000 偏移地址处(可配置的值)读取分区表。请参考 分区表 获取详细信息。引导加载程序会寻找工厂分区和 OTA 应用程序分区。如果在分区表中找到了 OTA 应用程序分区,引导加载程序将查询 otadata 分区以确定应引导哪个分区。更多信息请参考 空中升级 (OTA)。
关于 ESP-IDF 引导加载程序可用的配置选项,请参考 引导加载程序 (Bootloader)。
加载应用程序镜像:根据选定分区,二级引导程序会将应用程序镜像的各个段(Segment)从Flash加载到它们指定的内存地址:
- 对于在内部 IRAM(指令 RAM) 或 DRAM(数据 RAM) 中具有加载地址的段,将把数据从 flash 复制到它们的加载地址处;
- 对于一些加载地址位于 DROM(数据存储在 flash 中) 或 IROM(代码从 flash 中运行) 区域的段,通过配置 flash MMU,可为从 flash 到加载地址提供正确的映射。
跳转至应用程序:一旦处理完所有段(即加载了代码并设置了 flash MMU),二级引导加载程序将验证应用程序的完整性,并从二进制镜像文件的头部寻找入口地址,然后跳转到该地址处运行,将控制权交给应用程序。
(3)应用程序启动阶段
这是应用程序代码开始执行的过程,最终调用到我们编写的app_main函数。
应用程序启动包含了从应用程序开始执行到 app_main 函数在主任务内部运行前的所有过程。可分为三个阶段:
- 硬件和基本 C 语言运行环境的端口初始化;
- 软件服务和 FreeRTOS 的系统初始化;
- 运行主任务并调用 app_main。
1)端口初始化 (call_start_cpu0):
ESP-IDF 应用程序的入口是 components/esp_system/port/cpu_start.c 文件中的 call_start_cpu0 函数。这个函数由二级引导加载程序执行,并且从不返回。
该端口层的初始化功能会初始化基本的 C 运行环境 ("CRT"),并对 SoC 的内部硬件进行了初始配置。
- 为应用程序重新配置 CPU 异常(允许应用程序中断处理程序运行,并使用为应用程序配置的选项来处理 严重错误,而不是使用 ROM 提供的简易版错误处理程序处理;
- 如果没有设置选项 CONFIG_BOOTLOADER_WDT_ENABLE,则不使能 RTC 看门狗定时器;
- 初始化内部存储器(数据和 bss);
- 完成 MMU 高速缓存配置;
- 如果配置了 PSRAM,则使能 PSRAM;
- 将 CPU 时钟设置为项目配置的频率;
- 如果配置了内存保护,则初始化内存保护;
- 如果应用程序被配置为在多个内核上运行,则启动另一个内核并等待其初始化(在类似的“端口层”初始化函数 call_start_cpu1 内)。
call_start_cpu0 完成运行后,将调用在 components/esp_system/startup.c 中找到的“系统层”初始化函数 start_cpu0。其他内核也将完成端口层的初始化,并调用同一文件中的 start_other_cores。
2)系统初始化:
主要的系统初始化函数是 start_cpu0。默认情况下,这个函数与 start_cpu0_default 函数弱链接。这意味着可以覆盖这个函数,增加一些额外的初始化步骤。
主要的系统初始化阶段包括:
- 如果默认的日志级别允许,则记录该应用程序的相关信息(项目名称、应用程序版本 等);
- 初始化堆分配器(在这之前,所有分配必须是静态的或在堆栈上);
- 初始化 newlib 组件的系统调用和时间函数;
- 配置断电检测器;
- 根据 串行控制台配置 设置 libc stdin、stdout、和 stderr;
- 执行与安全有关的检查,包括为该配置烧录 efuse(包括 永久限制 ROM 下载模式);
- 初始化 SPI flash API 支持;
- 调用全局 C++ 构造函数和任何标有 __attribute__((constructor)) 的 C 函数。
二级系统初始化允许单个组件被初始化。如果一个组件有一个用 ESP_SYSTEM_INIT_FN 宏注释的初始化函数,它将作为二级初始化的一部分被调用。
3)运行主任务:
在所有其他组件都初始化后,主任务会被创建,FreeRTOS 调度器开始运行。
做完一些初始化任务后(需要启动调度器),主任务在固件中运行应用程序提供的函数app_main。
运行 app_main 的主任务有一个固定的 RTOS 优先级(比最小值高)和一个 可配置的堆栈大小。
主任务的内核亲和性也是可以配置的,请参考 CONFIG_ESP_MAIN_TASK_AFFINITY。
与普通的 FreeRTOS 任务(或嵌入式 C 的 main 函数)不同,app_main 任务可以返回。如果 app_main 函数返回,那么主任务将会被删除。系统将继续运行其他的 RTOS 任务。因此可以将 app_main 实现为一个创建其他应用任务然后返回的函数,或主应用任务本身。
4)APP CPU 的内核启动流程:
APP CPU 的启动流程类似但更简单:
当运行系统初始化时,PRO CPU 上的代码会给 APP CPU 设置好入口地址,解除其复位状态,然后等待 APP CPU 上运行的代码设置一个全局标志,以表明 APP CPU 已经正常启动。 完成后,APP CPU 跳转到 components/esp_system/port/cpu_start.c 中的 call_start_cpu1 函数。
当 start_cpu0 函数对 PRO CPU 进行初始化的时候,APP CPU 运行 start_cpu_other_cores 函数。与 start_cpu0 函数类似,start_cpu_other_cores 函数是弱链接的,默认为 start_cpu_other_cores_default 函数,但可以由应用程序替换为不同的函数。start_cpu_other_cores_default 函数做了一些与内核相关的系统初始化,然后等待 PRO CPU 启动 FreeRTOS 的调度器,启动完成后,它会执行 esp_startup_start_app_other_cores 函数,这是另一个默认为 esp_startup_start_app_other_cores_default 的弱链接函数。
默认情况下,esp_startup_start_app_other_cores_default 只会自旋,直到 PRO CPU 上的调度器触发中断,以启动 APP CPU 上的 RTOS 调度器。
2.3 结合代码分析启动流程
分析思路是从app_main()函数一层层往上找,最终找到相关调用链。分析调用关系,有两个关键的文件夹,分别为idf安装目录下的frameworks\esp-idf-v5.4.2\components\freertos 和 frameworks\esp-idf-v5.4.2\components\esp_system,分别用VSCode打开着两个文件夹。
调用链:app_main() -> main_task() -> esp_startup_start_app() ->start_cpu0_default() -> start_cpu0() -> g_startup_fn -> SYS_STARTUP_FN() -> call_start_cpu0() -> ENTRY(call_start_cpu0)
(1)freertos文件夹中函数分析
该文件中涉及的调用链:app_main() -> main_task() -> esp_startup_start_app(),涉及main_task() 和 esp_startup_start_app()函数都在app_startup.c文件中。
main_task()是连接 ESP32 系统启动流程和你的应用程序(app_main)的关键桥梁,下面为函数的代码实现,我们逐段分析它的工作流程和重要性。
static const char* MAIN_TAG = "main_task";
#if !CONFIG_FREERTOS_UNICORE
static volatile bool s_other_cpu_startup_done = false;
static bool other_cpu_startup_idle_hook_cb(void)
{
s_other_cpu_startup_done = true;
return true;
}
#endif
static void main_task(void* args)
{
ESP_LOGI(MAIN_TAG, "Started on CPU%d", (int)xPortGetCoreID());
#if !CONFIG_FREERTOS_UNICORE
// Wait for FreeRTOS initialization to finish on other core, before replacing its startup stack
esp_register_freertos_idle_hook_for_cpu(other_cpu_startup_idle_hook_cb, !xPortGetCoreID());
while (!s_other_cpu_startup_done) {
;
}
esp_deregister_freertos_idle_hook_for_cpu(other_cpu_startup_idle_hook_cb, !xPortGetCoreID());
#endif
// [refactor-todo] check if there is a way to move the following block to esp_system startup
heap_caps_enable_nonos_stack_heaps();
// Now we have startup stack RAM available for heap, enable any DMA pool memory
#if CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL
if (esp_psram_is_initialized()) {
esp_err_t r = esp_psram_extram_reserve_dma_pool(CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL);
if (r != ESP_OK) {
ESP_LOGE(MAIN_TAG, "Could not reserve internal/DMA pool (error 0x%x)", r);
abort();
}
}
#endif
// Initialize TWDT if configured to do so
#if CONFIG_ESP_TASK_WDT_INIT
esp_task_wdt_config_t twdt_config = {
.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000,
.idle_core_mask = 0,
#if CONFIG_ESP_TASK_WDT_PANIC
.trigger_panic = true,
#endif
};
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0
twdt_config.idle_core_mask |= (1 << 0);
#endif
#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1
twdt_config.idle_core_mask |= (1 << 1);
#endif
ESP_ERROR_CHECK(esp_task_wdt_init(&twdt_config));
#endif // CONFIG_ESP_TASK_WDT
/*
Note: Be careful when changing the "Calling app_main()" log below as multiple pytest scripts expect this log as a
start-of-application marker.
*/
ESP_LOGI(MAIN_TAG, "Calling app_main()");
extern void app_main(void);
app_main();
ESP_LOGI(MAIN_TAG, "Returned from app_main()");
vTaskDelete(NULL);
}
函数角色与多核同步:
角色定位:main_task是 ESP-IDF 启动过程中由系统创建的第一个 FreeRTOS 任务。它运行在 PRO CPU(CPU0)上,负责完成最后的系统初始化,并最终调用用户程序的入口函数 app_main。
多核同步:代码中 #if !CONFIG_FREERTOS_UNICORE之间的部分是为了确保在双核模式(默认)下,两个 CPU 核心的初始化工作能安全、有序地完成。
- 目的:等待 APP CPU(CPU1)完成其启动流程。这是必要的,因为在 APP CPU 完全启动前,其启动时使用的栈内存可能还被占用,PRO CPU 不能立即将其回收用于动态内存分配(堆);
- 机制:PRO CPU 注册一个空闲钩子函数 (other_cpu_startup_idle_hook_cb),然后循环等待一个全局标志 (s_other_cpu_startup_done)。当 APP CPU 启动完毕并进入空闲状态时,会触发这个钩子函数来设置标志位,通知 PRO CPU 可以继续执行。
内存管理初始化:
在双核同步完成后,函数着手优化系统的内存配置:
- heap_caps_enable_nonos_stack_heaps():回收启动阶段的栈内存供堆使用。在系统启动初期,一些临时使用的栈内存(例如 APP CPU 的启动栈)在初始化完成后就不再需要了。此函数将这些内存释放,并入系统的堆内存池,从而增加应用程序可用的动态内存总量。
- DMA 内存池预留:#if CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL区块处理的是使用外部 SPI RAM (PSRAM) 时的情况。有些 DMA(直接内存访问)操作要求内存必须位于片内 RAM。此配置项的作用就是从内部 RAM 中专门预留一块内存,作为 DMA 操作的专用池,确保即使用户配置了主要从 PSRAM 分配内存,DMA 操作也能有可用的内部内存。
看门狗配置与应用入口调用:
接下来,函数初始化了重要的系统安全机制——看门狗定时器:
任务看门狗 (TWDT) 配置:看门狗用于监控任务是否“卡住”。如果某个任务长时间无法执行(例如陷入了死循环),看门狗会触发系统复位,防止设备彻底无响应。这里的配置结构体 twdt_config设置了超时时间、是否在超时时引发系统崩溃(Panic)以及是否监控空闲任务等。
调用 app_main():这是整个函数的核心使命。在打印 "Calling app_main()" 日志后,它直接调用了你编写的 app_main()函数。需要特别注意的是:
- 你的程序逻辑(如创建任务、初始化硬件)从此处开始执行;
- 与 C 语言的 main函数不同,app_main()返回是允许的。返回后,main_task任务会打印日志并删除自身;
- main_task的删除不会导致系统停止,因为此时 FreeRTOS 调度器已经启动,其他由你创建的任务或系统任务(如 Wi-Fi 任务、事件任务)会继续正常运行。
核心流程总结:简单来说,main_task就像一位尽职的项目经理,它的工作流程清晰明确:
- 团队就位:确保双核CPU的启动工作都已完成,协调同步;
- 资源分配:优化和配置好内存、DMA等关键资源;
- 安全保障:启动看门狗定时器,为系统稳定性加上安全锁;
- 项目交接:调用你的 app_main(),将控制权正式移交给你的应用程序代码;
- 功成身退:在你的 app_main()返回后,它便完成使命,自行退出。
esp_startup_start_app()函数实现分析,下面为函数代码实现,在该函数中创了main_task()任务:
void esp_startup_start_app(void)
{
#if CONFIG_ESP_INT_WDT
esp_int_wdt_init();
// Initialize the interrupt watch dog for CPU0.
esp_int_wdt_cpu_init();
#elif CONFIG_ESP32_ECO3_CACHE_LOCK_FIX
// If the INT WDT isn't enabled on ESP32 ECO3, issue an error regarding the cache lock bug
assert(!soc_has_cache_lock_bug() && "ESP32 Rev 3 + Dual Core + PSRAM requires INT WDT enabled in project config!");
#endif
// Initialize the cross-core interrupt on CPU0
esp_crosscore_int_init();
#if CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME
void esp_gdbstub_init(void);
esp_gdbstub_init();
#endif // CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME
BaseType_t res = xTaskCreatePinnedToCore(main_task, "main",
ESP_TASK_MAIN_STACK, NULL,
ESP_TASK_MAIN_PRIO, NULL, ESP_TASK_MAIN_CORE);
assert(res == pdTRUE);
(void)res;
/*
If a particular FreeRTOS port has port/arch specific OS startup behavior, they can implement a function of type
"void port_start_app_hook(void)" in their `port.c` files. This function will be called below, thus allowing each
FreeRTOS port to implement port specific app startup behavior.
*/
void __attribute__((weak)) port_start_app_hook(void);
if (port_start_app_hook != NULL) {
port_start_app_hook();
}
ESP_EARLY_LOGD(APP_START_TAG, "Starting scheduler on CPU0");
vTaskStartScheduler();
}
核心任务是创建主任务并启动 FreeRTOS 调度器,从而将控制权交给你的 app_main函数,为了更直观地理解 esp_startup_start_app的承上启下作用,参考下面的简化流程图,它展示了从二级引导程序到你的 app_main被调用的关键步骤:

下面详细解析它的每一部分工作:
看门狗与系统安全检查:
#if CONFIG_ESP_INT_WDT
esp_int_wdt_init();
// Initialize the interrupt watch dog for CPU0.
esp_int_wdt_cpu_init();
#elif CONFIG_ESP32_ECO3_CACHE_LOCK_FIX
assert(!soc_has_cache_lock_bug() && "ESP32 Rev 3 + Dual Core + PSRAM requires INT WDT enabled in project config!");
#endif
- 中断看门狗:如果通过菜单配置 (CONFIG_ESP_INT_WDT) 使能了中断看门狗,这里会进行初始化。它的作用是监控 CPU 是否被长时间阻塞,防止系统“卡死”;
- 硬件 Bug 检查:对于某些版本的 ESP32 芯片(如 Rev 3)且在双核与 PSRAM 同时使用的特定情况下,存在一个硬件缓存锁缺陷。此代码段确保在存在此风险时,中断看门狗必须被开启,否则会触发断言失败,提醒开发者修改配置。
多核与调试支持初始化:
// Initialize the cross-core interrupt on CPU0
esp_crosscore_int_init();
#if CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME
void esp_gdbstub_init(void);
esp_gdbstub_init();
#endif // CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME
- 跨核中断:初始化 CPU0(PRO_CPU)的跨核中断能力。这对于双核系统中,一个核心通知或控制另一个核心至关重要;
- GDB 调试支持:如果使能了运行时 GDB 调试功能,这里会初始化 GDB 桩,允许你通过调试器连接到正在运行的系统。
创建主任务:
BaseType_t res = xTaskCreatePinnedToCore(main_task, "main",
ESP_TASK_MAIN_STACK, NULL,
ESP_TASK_MAIN_PRIO, NULL, ESP_TASK_MAIN_CORE);
assert(res == pdTRUE);
(void)res;
这是函数最核心的一步:使用 FreeRTOS 的 API 创建名为 "main" 的任务。
- 任务函数:main_task。后续的应用程序将在这个任务中启动;
- 栈大小与优先级:使用预定义的 ESP_TASK_MAIN_STACK和 ESP_TASK_MAIN_PRIO;
- 核心绑定:通过 ESP_TASK_MAIN_CORE将任务固定到特定的 CPU 核心上运行(通常是 PRO_CPU,即 CPU0);
- 创建检查:使用 assert确保任务创建成功,失败则系统挂起。
启动调度器与钩子函数:
void __attribute__((weak)) port_start_app_hook(void);
if (port_start_app_hook != NULL) {
port_start_app_hook();
}
ESP_EARLY_LOGD(APP_START_TAG, "Starting scheduler on CPU0");
vTaskStartScheduler();
- 端口特定钩子:这是一个弱符号定义的钩子函数。如果特定端口的实现(如 port.c)需要定义额外的启动行为,可以在这里实现 port_start_app_hook函数,它将在启动调度器前被调用;
- 启动调度器:调用 vTaskStartScheduler()启动 FreeRTOS 内核调度器。至此,系统正式开始进行多任务调度,刚刚创建的 "main" 任务就绪,等待被执行。
(2)esp_system文件夹中函数分析
该文件中涉及的调用链:esp_startup_start_app()(freertos文件夹中函数) ->start_cpu0_default() -> start_cpu0() -> g_startup_fn -> SYS_STARTUP_FN() -> call_start_cpu0() -> ENTRY(call_start_cpu0)
start_cpu0_default()函数实现分析,函数是 ESP32-S3 应用程序启动阶段系统初始化的核心,它在硬件和基本 C 运行环境(由 call_start_cpu0完成)准备好之后,负责拉起所有软件服务、组件,并最终启动 FreeRTOS 调度器和你的 app_main任务。下面为函数代码实现:
static void start_cpu0_default(void)
{
// Initialize core components and services.
do_core_init();
// Execute constructors.
do_global_ctors();
// Execute init functions of other components; blocks
// until all cores finish (when !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE).
do_secondary_init();
#if SOC_CPU_CORES_NUM > 1 && !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
s_system_full_inited = true;
#endif
esp_startup_start_app();
ESP_INFINITE_LOOP();
}
可以参考下面这个简化的流程图:

下面详细解析它的每一部分工作:
初始化核心组件与服务 do_core_init():
这个函数完成了最基础的软件运行环境搭建 ,主要包括:
- 堆分配器初始化:这是非常关键的一步。在此函数调用之前,所有的内存分配都必须是静态的(全局变量)或在栈上完成的。在此之后,你就可以安全地使用 malloc等动态内存分配函数了;
- newlib 初始化:为 C 标准库设置系统调用接口和时间函数,使得如 printf、read、write等函数能够正常工作;
- 安全与认证检查:执行一系列安全检查,例如验证 efuse 中的配置,包括是否要永久禁用 ROM 下载模式以增强安全性等;
- 记录系统信息:如果日志级别允许,会打印应用程序的名称、版本等信息,方便调试和确认当前运行的固件版本。
执行全局构造函数 do_global_ctors():
这个函数负责调用所有标记为 __attribute__((constructor))的 C 函数,以及 C++ 的全局静态对象的构造函数 。这确保了在 app_main执行前,所有必要的全局资源已经构造完毕。
次级初始化与多核协调 do_secondary_init():
这个函数负责初始化系统中其他的软件组件 。更重要的是,在双核系统(SOC_CPU_CORES_NUM > 1且未配置为单核模式)中,它会等待 APP CPU(CPU1)也完成其端口初始化,确保所有核心的初始化工作都同步完成 。此外,任何使用 ESP_SYSTEM_INIT_FN宏注册的组件初始化函数,也会在此期间被调用 。
函数特性与无限循环ESP_INFINITE_LOOP():
void start_cpu0(void) __attribute__((weak, alias("start_cpu0_default"))) __attribute__((noreturn));
- 弱函数别名:在源码中,start_cpu0被定义为 start_cpu0_default的弱别名(__attribute__((weak, alias("start_cpu0_default")))) 。这意味着如果你有特殊的初始化需求,可以在自己的代码中重新实现一个强类型的 start_cpu0函数,从而覆盖这个默认实现,增加自定义的初始化步骤;
- 最后的无限循环 ESP_INFINITE_LOOP():你可能注意到函数最后是一个无限循环。这是因为在 esp_startup_start_app()中启动了 FreeRTOS 调度器后,PRO CPU 的执行权就交给了调度器。这个无限循环是为了防止 PRO CPU 的执行流意外跑飞而设置的一个安全措施。正常情况下,调度器启动后,代码就不会再执行到这一行了。
start_cpu0() -> g_startup_fn -> SYS_STARTUP_FN() -> call_start_cpu0(),最终分析call_start_cpu0()这个函数实现分析,函数代码实现如下:
void IRAM_ATTR call_start_cpu0(void)
{
#if !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
soc_reset_reason_t rst_reas[SOC_CPU_CORES_NUM];
#else
soc_reset_reason_t __attribute__((unused)) rst_reas[1];
#endif
#ifdef __riscv
if (esp_cpu_dbgr_is_attached()) {
/* Let debugger some time to detect that target started, halt it, enable ebreaks and resume.
500ms should be enough. */
for (uint32_t ms_num = 0; ms_num < 2; ms_num++) {
esp_rom_delay_us(100000);
}
}
// Configure the global pointer register
// (This should be the first thing IDF app does, as any other piece of code could be
// relaxed by the linker to access something relative to __global_pointer$)
__asm__ __volatile__(
".option push\n"
".option norelax\n"
"la gp, __global_pointer$\n"
".option pop"
);
#endif
#if SOC_BRANCH_PREDICTOR_SUPPORTED
esp_cpu_branch_prediction_enable();
#endif
// Move exception vectors to IRAM
esp_cpu_intr_set_ivt_addr(&_vector_table);
#if SOC_INT_CLIC_SUPPORTED
/* When hardware vectored interrupts are enabled in CLIC,
* the CPU jumps to this base address + 4 * interrupt_id.
*/
esp_cpu_intr_set_mtvt_addr(&_mtvt_table);
#endif
#if SOC_CPU_SUPPORT_WFE
rv_utils_disable_wfe_mode();
#endif
rst_reas[0] = esp_rom_get_reset_reason(0);
#if !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
rst_reas[1] = esp_rom_get_reset_reason(1);
#endif
//Clear BSS. Please do not attempt to do any complex stuff (like early logging) before this.
#if SOC_MEM_NON_CONTIGUOUS_SRAM
memset(&_bss_start_low, 0, (&_bss_end_low - &_bss_start_low) * sizeof(_bss_start_low));
memset(&_bss_start_high, 0, (&_bss_end_high - &_bss_start_high) * sizeof(_bss_start_high));
#else
memset(&_bss_start, 0, (&_bss_end - &_bss_start) * sizeof(_bss_start));
#endif // SOC_MEM_NON_CONTIGUOUS_SRAM
#if CONFIG_BT_LE_RELEASE_IRAM_SUPPORTED
// Clear Bluetooth bss
memset(&_bss_bt_start, 0, (&_bss_bt_end - &_bss_bt_start) * sizeof(_bss_bt_start));
#endif // CONFIG_BT_LE_RELEASE_IRAM_SUPPORTED
#if defined(CONFIG_IDF_TARGET_ESP32) && defined(CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY)
// Clear IRAM BSS
memset(&_iram_bss_start, 0, (&_iram_bss_end - &_iram_bss_start) * sizeof(_iram_bss_start));
#endif
#if SOC_RTC_FAST_MEM_SUPPORTED || SOC_RTC_SLOW_MEM_SUPPORTED
/* Unless waking from deep sleep (implying RTC memory is intact), clear RTC bss */
if (rst_reas[0] != RESET_REASON_CORE_DEEP_SLEEP) {
memset(&_rtc_bss_start, 0, (&_rtc_bss_end - &_rtc_bss_start) * sizeof(_rtc_bss_start));
}
#endif
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP && !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE && !SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE
// It helps to fix missed cache settings for other cores. It happens when bootloader is unicore.
do_multicore_settings();
#endif
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
//cache hal ctx needs to be initialised
cache_hal_init();
#endif
// When the APP is loaded into ram for execution, some hardware initialization behaviors
// in the bootloader are still necessary
#if CONFIG_APP_BUILD_TYPE_RAM
bootloader_init();
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
bootloader_flash_hardware_init();
#endif //#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
#endif //#if CONFIG_APP_BUILD_TYPE_RAM
#if CONFIG_IDF_TARGET_ESP32P4
#define RWDT_RESET RESET_REASON_CORE_RWDT
#define MWDT_RESET RESET_REASON_CORE_MWDT
#else
#define RWDT_RESET RESET_REASON_CORE_RTC_WDT
#define MWDT_RESET RESET_REASON_CORE_MWDT0
#endif
#ifndef CONFIG_BOOTLOADER_WDT_ENABLE
// from panic handler we can be reset by RWDT or TG0WDT
if (rst_reas[0] == RWDT_RESET || rst_reas[0] == MWDT_RESET
#if !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
|| rst_reas[1] == RWDT_RESET || rst_reas[1] == MWDT_RESET
#endif
) {
wdt_hal_context_t rtc_wdt_ctx = RWDT_HAL_CONTEXT_DEFAULT();
wdt_hal_write_protect_disable(&rtc_wdt_ctx);
wdt_hal_disable(&rtc_wdt_ctx);
wdt_hal_write_protect_enable(&rtc_wdt_ctx);
}
#endif
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
#if CONFIG_IDF_TARGET_ESP32S2
/* Configure the mode of instruction cache : cache size, cache associated ways, cache line size. */
extern void esp_config_instruction_cache_mode(void);
esp_config_instruction_cache_mode();
/* If we need use SPIRAM, we should use data cache, or if we want to access rodata, we also should use data cache.
Configure the mode of data : cache size, cache associated ways, cache line size.
Enable data cache, so if we don't use SPIRAM, it just works. */
extern void esp_config_data_cache_mode(void);
esp_config_data_cache_mode();
Cache_Enable_DCache(0);
#endif
#if CONFIG_IDF_TARGET_ESP32S3
/* Configure the mode of instruction cache : cache size, cache line size. */
extern void rom_config_instruction_cache_mode(uint32_t cfg_cache_size, uint8_t cfg_cache_ways, uint8_t cfg_cache_line_size);
rom_config_instruction_cache_mode(CONFIG_ESP32S3_INSTRUCTION_CACHE_SIZE, CONFIG_ESP32S3_ICACHE_ASSOCIATED_WAYS, CONFIG_ESP32S3_INSTRUCTION_CACHE_LINE_SIZE);
/* If we need use SPIRAM, we should use data cache.
Configure the mode of data : cache size, cache line size.*/
Cache_Suspend_DCache();
extern void rom_config_data_cache_mode(uint32_t cfg_cache_size, uint8_t cfg_cache_ways, uint8_t cfg_cache_line_size);
rom_config_data_cache_mode(CONFIG_ESP32S3_DATA_CACHE_SIZE, CONFIG_ESP32S3_DCACHE_ASSOCIATED_WAYS, CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE);
Cache_Resume_DCache(0);
#endif // CONFIG_IDF_TARGET_ESP32S3
#if CONFIG_IDF_TARGET_ESP32P4
//TODO: IDF-5670, add cache init API
extern void esp_config_l2_cache_mode(void);
esp_config_l2_cache_mode();
#endif
#if ESP_ROM_NEEDS_SET_CACHE_MMU_SIZE
#if CONFIG_APP_BUILD_TYPE_ELF_RAM
// For RAM loadable ELF case, we don't need to reserve IROM/DROM as instructions and data
// are all in internal RAM. If the RAM loadable ELF has any requirement to memory map the
// external flash then it should use flash or partition mmap APIs.
uint32_t cache_mmu_irom_size = 0;
__attribute__((unused)) uint32_t cache_mmu_drom_size = 0;
#else // CONFIG_APP_BUILD_TYPE_ELF_RAM
uint32_t _instruction_size = (uint32_t)&_instruction_reserved_end - (uint32_t)&_instruction_reserved_start;
uint32_t cache_mmu_irom_size = ((_instruction_size + SPI_FLASH_MMU_PAGE_SIZE - 1) / SPI_FLASH_MMU_PAGE_SIZE) * sizeof(uint32_t);
uint32_t _rodata_size = (uint32_t)&_rodata_reserved_end - (uint32_t)&_rodata_reserved_start;
__attribute__((unused)) uint32_t cache_mmu_drom_size = ((_rodata_size + SPI_FLASH_MMU_PAGE_SIZE - 1) / SPI_FLASH_MMU_PAGE_SIZE) * sizeof(uint32_t);
#endif // !CONFIG_APP_BUILD_TYPE_ELF_RAM
/* Configure the Cache MMU size for instruction and rodata in flash. */
Cache_Set_IDROM_MMU_Size(cache_mmu_irom_size, CACHE_DROM_MMU_MAX_END - cache_mmu_irom_size);
#endif // ESP_ROM_NEEDS_SET_CACHE_MMU_SIZE
#if CONFIG_ESPTOOLPY_OCT_FLASH && !CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT
bool efuse_opflash_en = efuse_ll_get_flash_type();
if (!efuse_opflash_en) {
ESP_DRAM_LOGE(TAG, "Octal Flash option selected, but EFUSE not configured!");
abort();
}
#endif
esp_mspi_pin_init();
// For Octal flash, it's hard to implement a read_id function in OPI mode for all vendors.
// So we have to read it here in SPI mode, before entering the OPI mode.
bootloader_flash_update_id();
// Configure the power related stuff. After this the MSPI timing tuning can be done.
esp_rtc_init();
/**
* This function initialise the Flash chip to the user-defined settings.
*
* In bootloader, we only init Flash (and MSPI) to a preliminary state, for being flexible to
* different chips.
* In this stage, we re-configure the Flash (and MSPI) to required configuration
*/
spi_flash_init_chip_state();
#if SOC_MEMSPI_SRC_FREQ_120M
// This function needs to be called when PLL is enabled. Needs to be called after spi_flash_init_chip_state in case
// some state of flash is modified.
mspi_timing_flash_tuning();
#endif
esp_mmu_map_init();
#if !CONFIG_APP_BUILD_TYPE_ELF_RAM
#if CONFIG_SPIRAM_FLASH_LOAD_TO_PSRAM
ESP_ERROR_CHECK(image_process());
#endif
#endif
#if CONFIG_SPIRAM_BOOT_INIT
if (esp_psram_init() != ESP_OK) {
#if CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY
ESP_DRAM_LOGE(TAG, "Failed to init external RAM, needed for external .bss segment");
abort();
#endif
#if CONFIG_SPIRAM_IGNORE_NOTFOUND
ESP_EARLY_LOGI(TAG, "Failed to init external RAM; continuing without it.");
#else
ESP_DRAM_LOGE(TAG, "Failed to init external RAM!");
abort();
#endif
}
#endif
//----------------------------------Separator-----------------------------//
/**
* @note
* After this stage, you can access the flash through the cache, i.e. run code which is not placed in IRAM
* or print string which locates on flash
*/
esp_mspi_pin_reserve();
#endif // !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
#if CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
ESP_EARLY_LOGI(TAG, "Unicore app");
#else
ESP_EARLY_LOGI(TAG, "Multicore app");
#endif
bootloader_init_mem();
#if !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
s_cpu_up[0] = true;
#endif
ESP_EARLY_LOGD(TAG, "Pro cpu up");
#if SOC_CPU_CORES_NUM > 1 // there is no 'single-core mode' for natively single-core processors
#if !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
start_other_core();
#else
ESP_EARLY_LOGI(TAG, "Single core mode");
#if CONFIG_IDF_TARGET_ESP32
DPORT_CLEAR_PERI_REG_MASK(DPORT_APPCPU_CTRL_B_REG, DPORT_APPCPU_CLKGATE_EN); // stop the other core
#elif CONFIG_IDF_TARGET_ESP32S3
REG_CLR_BIT(SYSTEM_CORE_1_CONTROL_0_REG, SYSTEM_CONTROL_CORE_1_CLKGATE_EN);
#if SOC_APPCPU_HAS_CLOCK_GATING_BUG
/* The clock gating signal of the App core is invalid. We use RUNSTALL and RESETTING
signals to ensure that the App core stops running in single-core mode. */
REG_SET_BIT(SYSTEM_CORE_1_CONTROL_0_REG, SYSTEM_CONTROL_CORE_1_RUNSTALL);
REG_CLR_BIT(SYSTEM_CORE_1_CONTROL_0_REG, SYSTEM_CONTROL_CORE_1_RESETING);
#endif
#elif CONFIG_IDF_TARGET_ESP32P4
REG_CLR_BIT(HP_SYS_CLKRST_SOC_CLK_CTRL0_REG, HP_SYS_CLKRST_REG_CORE1_CPU_CLK_EN);
REG_SET_BIT(HP_SYS_CLKRST_HP_RST_EN0_REG, HP_SYS_CLKRST_REG_RST_EN_CORE1_GLOBAL);
#endif // CONFIG_IDF_TARGET_ESP32
#endif // !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
#endif // SOC_CPU_CORES_NUM > 1
#if CONFIG_SPIRAM_MEMTEST
if (esp_psram_is_initialized()) {
bool ext_ram_ok = esp_psram_extram_test();
if (!ext_ram_ok) {
ESP_EARLY_LOGE(TAG, "External RAM failed memory test!");
abort();
}
}
#endif //CONFIG_SPIRAM_MEMTEST
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
//TODO: IDF-5023, replace with MMU driver
#if CONFIG_IDF_TARGET_ESP32S3
int s_instr_flash2spiram_off = 0;
int s_rodata_flash2spiram_off = 0;
#if CONFIG_SPIRAM_FETCH_INSTRUCTIONS
s_instr_flash2spiram_off = instruction_flash2spiram_offset();
#endif
#if CONFIG_SPIRAM_RODATA
s_rodata_flash2spiram_off = rodata_flash2spiram_offset();
#endif
Cache_Set_IDROM_MMU_Info(cache_mmu_irom_size / sizeof(uint32_t), \
cache_mmu_drom_size / sizeof(uint32_t), \
(uint32_t)&_rodata_reserved_start, \
(uint32_t)&_rodata_reserved_end, \
s_instr_flash2spiram_off, \
s_rodata_flash2spiram_off);
#endif // CONFIG_IDF_TARGET_ESP32S3
#if CONFIG_ESP32S2_INSTRUCTION_CACHE_WRAP || CONFIG_ESP32S2_DATA_CACHE_WRAP || \
CONFIG_ESP32S3_INSTRUCTION_CACHE_WRAP || CONFIG_ESP32S3_DATA_CACHE_WRAP
uint32_t icache_wrap_enable = 0, dcache_wrap_enable = 0;
#if CONFIG_ESP32S2_INSTRUCTION_CACHE_WRAP || CONFIG_ESP32S3_INSTRUCTION_CACHE_WRAP
icache_wrap_enable = 1;
#endif
#if CONFIG_ESP32S2_DATA_CACHE_WRAP || CONFIG_ESP32S3_DATA_CACHE_WRAP
dcache_wrap_enable = 1;
#endif
esp_enable_cache_wrap(icache_wrap_enable, dcache_wrap_enable);
#endif
#if CONFIG_ESP32S3_DATA_CACHE_16KB
Cache_Invalidate_DCache_All();
Cache_Occupy_Addr(SOC_DROM_LOW, 0x4000);
#endif
#if CONFIG_IDF_TARGET_ESP32C2
// TODO : IDF-5020
#if CONFIG_ESP32C2_INSTRUCTION_CACHE_WRAP
esp_enable_cache_wrap(1);
#endif
#endif
#endif // !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
#if CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY
esp_psram_bss_init();
#endif
//Enable trace memory and immediately start trace.
#if CONFIG_ESP32_TRAX || CONFIG_ESP32S2_TRAX || CONFIG_ESP32S3_TRAX
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S3
#if CONFIG_ESP32_TRAX_TWOBANKS || CONFIG_ESP32S3_TRAX_TWOBANKS
trax_enable(TRAX_ENA_PRO_APP);
#else
trax_enable(TRAX_ENA_PRO);
#endif
#elif CONFIG_IDF_TARGET_ESP32S2
trax_enable(TRAX_ENA_PRO);
#endif
trax_start_trace(TRAX_DOWNCOUNT_WORDS);
#endif // CONFIG_ESP32_TRAX || CONFIG_ESP32S2_TRAX || CONFIG_ESP32S3_TRAX
esp_clk_init();
esp_perip_clk_init();
// Now that the clocks have been set-up, set the startup time from RTC
// and default RTC-backed system time provider.
g_startup_time = esp_rtc_get_time_us();
// Clear interrupt matrix for PRO CPU core
core_intr_matrix_clear();
#ifndef CONFIG_IDF_ENV_FPGA // TODO: on FPGA it should be possible to configure this, not currently working with APB_CLK_FREQ changed
#ifdef CONFIG_ESP_CONSOLE_UART
uint32_t clock_hz = esp_clk_apb_freq();
#if ESP_ROM_UART_CLK_IS_XTAL
clock_hz = esp_clk_xtal_freq(); // From esp32-s3 on, UART clock source is selected to XTAL in ROM
#endif
esp_rom_output_tx_wait_idle(CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM);
// In a single thread mode, the freertos is not started yet. So don't have to use a critical section.
int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused)); // To avoid build errors about spinlock's __DECLARE_RCC_ATOMIC_ENV
esp_rom_uart_set_clock_baudrate(CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM, clock_hz, CONFIG_ESP_CONSOLE_UART_BAUDRATE);
#endif
#endif
#if SOC_DEEP_SLEEP_SUPPORTED
// Need to unhold the IOs that were hold right before entering deep sleep, which are used as wakeup pins
if (rst_reas[0] == RESET_REASON_CORE_DEEP_SLEEP) {
esp_deep_sleep_wakeup_io_reset();
}
#endif //#if SOC_DEEP_SLEEP_SUPPORTED
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
esp_cache_err_int_init();
#endif
#if CONFIG_ESP_SYSTEM_MEMPROT_FEATURE && !CONFIG_ESP_SYSTEM_MEMPROT_TEST
// Memprot cannot be locked during OS startup as the lock-on prevents any PMS changes until a next reboot
// If such a situation appears, it is likely an malicious attempt to bypass the system safety setup -> print error & reset
#if CONFIG_IDF_TARGET_ESP32S2
if (esp_memprot_is_locked_any()) {
#else
bool is_locked = false;
if (esp_mprot_is_conf_locked_any(&is_locked) != ESP_OK || is_locked) {
#endif
ESP_EARLY_LOGE(TAG, "Memprot feature locked after the system reset! Potential safety corruption, rebooting.");
esp_restart_noos();
}
//default configuration of PMS Memprot
esp_err_t memp_err = ESP_OK;
#if CONFIG_IDF_TARGET_ESP32S2 //specific for ESP32S2 unless IDF-3024 is merged
#if CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK
memp_err = esp_memprot_set_prot(PANIC_HNDL_ON, MEMPROT_LOCK, NULL);
#else
memp_err = esp_memprot_set_prot(PANIC_HNDL_ON, MEMPROT_UNLOCK, NULL);
#endif
#else //CONFIG_IDF_TARGET_ESP32S2 specific end
esp_memp_config_t memp_cfg = ESP_MEMPROT_DEFAULT_CONFIG();
#if !CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK
memp_cfg.lock_feature = false;
#endif
memp_err = esp_mprot_set_prot(&memp_cfg);
#endif //other IDF_TARGETS end
if (memp_err != ESP_OK) {
ESP_EARLY_LOGE(TAG, "Failed to set Memprot feature (0x%08X: %s), rebooting.", memp_err, esp_err_to_name(memp_err));
esp_restart_noos();
}
#endif //CONFIG_ESP_SYSTEM_MEMPROT_FEATURE && !CONFIG_ESP_SYSTEM_MEMPROT_TEST
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
// External devices (including SPI0/1, cache) should be initialized
#if !CONFIG_APP_BUILD_TYPE_RAM
// Normal startup flow. We arrive here with the help of 1st, 2nd bootloader. There are valid headers (app/bootloader)
// Read the application binary image header. This will also decrypt the header if the image is encrypted.
__attribute__((unused)) esp_image_header_t fhdr = {0};
// We can access the image header through the cache by reading from the memory-mapped virtual DROM start offset
uint32_t fhdr_src_addr = (uint32_t)(&_rodata_reserved_start) - sizeof(esp_image_header_t) - sizeof(esp_image_segment_header_t);
hal_memcpy(&fhdr, (void *) fhdr_src_addr, sizeof(fhdr));
if (fhdr.magic != ESP_IMAGE_HEADER_MAGIC) {
ESP_EARLY_LOGE(TAG, "Invalid app image header");
abort();
}
#if CONFIG_IDF_TARGET_ESP32
#if !CONFIG_SPIRAM_BOOT_INIT
// If psram is uninitialized, we need to improve some flash configuration.
bootloader_flash_clock_config(&fhdr);
bootloader_flash_gpio_config(&fhdr);
bootloader_flash_dummy_config(&fhdr);
bootloader_flash_cs_timing_config();
#endif //!CONFIG_SPIRAM_BOOT_INIT
#endif //CONFIG_IDF_TARGET_ESP32
#if CONFIG_SPI_FLASH_SIZE_OVERRIDE
int app_flash_size = esp_image_get_flash_size(fhdr.spi_size);
if (app_flash_size < 1 * 1024 * 1024) {
ESP_EARLY_LOGE(TAG, "Invalid flash size in app image header.");
abort();
}
bootloader_flash_update_size(app_flash_size);
#endif //CONFIG_SPI_FLASH_SIZE_OVERRIDE
#else
// CONFIG_APP_BUILD_TYPE_RAM && !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
bootloader_flash_unlock();
#endif
#endif //!CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
#if !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
s_cpu_inited[0] = true;
volatile bool cpus_inited = false;
while (!cpus_inited) {
cpus_inited = true;
for (int i = 0; i < SOC_CPU_CORES_NUM; i++) {
cpus_inited &= s_cpu_inited[i];
}
esp_rom_delay_us(100);
}
#endif
SYS_STARTUP_FN();
}
call_start_cpu0函数是芯片上电后,由二级引导程序加载完毕并跳转执行的第一个应用程序入口点,负责完成最底层的硬件和运行环境初始化。可以参考下面这个简化的流程图,它清晰地展示了从二级引导程序到 call_start_cpu0的关键步骤:

下面详细解析它的每一部分工作:
call_start_cpu0函数位于 components/esp_system/port/cpu_start.c文件中,由二级引导程序加载后直接跳转执行,并且此函数从不返回 。它的核心使命是为高级语言(如C)的运行和操作系统的启动铺平道路。
基础C运行环境(CRT)初始化:
- 初始化内存: 清零 .bss段(存储未初始化全局变量),并从 Flash 复制 .data段(存储已初始化全局变量)到内部 SRAM 中,确保变量具有正确的初始值;
- 配置CPU异常: 替换芯片ROM中固化的简易错误处理程序,为应用程序设置更完善的异常和中断处理程序,以便在发生严重错误时能提供更详细的诊断信息。
关键硬件初始化:
- MMU缓存配置: 完成内存管理单元(MMU)和高速缓存的配置,这对于CPU高效访问外部Flash中的代码和数据至关重要;
- 时钟设置: 将CPU时钟设置为在 menuconfig中配置的频率(如80MHz, 160MHz, 240MHz),以满足应用对性能的需求;
- PSRAM初始化(若使能): 如果项目配置中使能了外部PSRAM(伪静态随机存储器),会在此阶段进行初始化,扩展可用内存空间;
- 看门狗控制: 根据配置项 CONFIG_BOOTLOADER_WDT_ENABLE决定是否使能RTC看门狗定时器。
启动APP_CPU(双核协同):
这是 call_start_cpu0的一个关键同步步骤。在ESP32的双核系统中:
- PRO_CPU(CPU0)是首先启动并执行所有初始化操作的核心,而APP_CPU(CPU1)在芯片复位后保持复位状态;
- call_start_cpu0函数会设置APP CPU的入口地址(指向 call_start_cpu1函数),然后解除APP CPU的复位状态;
- 随后,PRO CPU会等待APP CPU也完成其自身的端口初始化(在 call_start_cpu1中),并设置一个全局标志位,表明自己已准备就绪。这个握手机制确保了两核在后续启动过程中的同步。
重要特性与后续流程:
- 弱函数定义: 在ESP-IDF中,call_start_cpu0是一个弱函数。这意味着,如果你的应用程序有极其特殊的底层初始化需求(例如,需要在最早阶段修改默认的初始化顺序或添加自定义操作),你可以在自己的代码中重新实现一个同名的强函数来覆盖默认实现。但对于绝大多数应用场景,使用默认实现即可;
- 交接给系统初始化: 当 call_start_cpu0完成了所有上述底层"脏活累活"后,它会调用系统层的初始化函数 start_cpu0。从这一刻起,控制权便移交给了更上层的软件系统,后续的初始化工作(如初始化堆分配器、创建主任务、启动FreeRTOS调度器等)将由 start_cpu0及其相关函数链(如 start_cpu0_default)负责完成,并最终调用到用户熟悉的 app_main函数。
总而言之,call_start_cpu0是ESP32启动过程中承上启下的关键一环,它搭建了从硬件初始化到软件系统运行的桥梁。
更多推荐



所有评论(0)