本章主要分析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加载到它们指定的内存地址:

        ​​跳转至应用程序​​:一旦处理完所有段(即加载了代码并设置了 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启动过程中承上启下的关键一环,它搭建了从硬件初始化到软件系统运行的桥梁。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐