> 文章列表 > ESP32学习五-启动流程

ESP32学习五-启动流程

ESP32学习五-启动流程

一、简介

        在ESP32的开发中,通常我们会从app_main函数中开始我们的代码开发。但是为什么是app_main呢?app_main又是从哪里被调用的app_main之前又做了什么操作呢?今天我们就来详细分析一下。

        官方参考文档:应用程序的启动流程 - ESP32 - — ESP-IDF 编程指南 release-v5.0 文档

二、app_main

        在我们的开发中,最先接触的就是app_main。esp-idf提供的所有例程,也都是从app_main开始,就像下图这样的。

        开发者自己添加的代码,也都是从app_main函数这里开始。那我们app_main之前是什么呢?

         我们直接打开esp-idf/components/esp32/cpu_start.c。在这个文件里边,我们找到了app_main函数被调用的地方。

         这里可以看到app_main被main_task调用。显然main_task是一个任务。那再看看main_task在哪里被调用。

        

         这里追溯到main_task是一个被创建的任务。而这个任务是在start_cpu0_default中被调用的。同时也可以看到,在创建了mai_task之后,才启动vTaskStartScheduler。那就可以确定,start_cpu0_default就是程序最初开始执行的地方了。

        这里再看一下main_task这个任务,创建时只有任务栈大小优先级

         可以看到,优先级也是很低的,只比空闲任务高了一级

         而任务栈的大小,可以看到给到了4096Byte

        那我们再返回看一下main_task。

         可以看到,main_task任务在调用完app_main之后,就删除任务结束了

        而start_cpu0_default是在哪里被调用呢?我们在cpu_start.c文件中只找到了这句话。 

         这句代码什么意思呢?我们继续往下看。

三、引导程序

        根据官方文档介绍,ESP32从上电到运行app_main函数中间所经历的步骤,大致上可以分为如下3个步骤:

  1. 一级引导程序。该程序被固化在了ESP32内部的ROM中,它会从Flash的0x1000偏移地址处引导二级程序至RAM(IRAM&DRAM)中。
  2. 二级引导程序。该程序从Flash中加载分区表主程序镜像至内存中,主程序中包含了RAM段和通过flash高速缓存映射的只读段。
  3. 应用程序启动阶段。该阶段,第二个CPU和RTOS的调度器会被启动。

        一级引导程序

        SoC复位后,PRO CPU会立即开始运行,执行复位向量代码,而APP CPU仍然保持复位状态。在启动过程中,PRO CPU会执行所有的初始化操作。APP CPU的复位状态会在应用程序启动代码的call_start_cpu0函数中失效。复位向量代码位于ESP32芯片掩膜ROM处,且不能被修改

        复位向量调用的启动代码会根据GPIO_STRAP_REG寄存器的值来确定ESP32的启动模式,该寄存器保存着复位后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技术规格书了解SOC启动模式以及具体执行过程。
  3. 软件CPU复位、看门狗CPU复位:根据EFUSE中配置SPI FLASH,然后尝试从Flash中加载代码,这部分将会在后面详细介绍。

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

        二级引导程序二进制镜像会从FLASH的0x1000偏移地址处加载。如果正在使用Secure Boot,则flash的第一个4K扇区用于存储安全启动IV以及引导程序镜像的摘要,否则不使用该扇区。

        二级引导程序

        在ESP-IDF中,存放在flash的0x1000偏移地址处的二进制镜像就是二级引导程序。二级引导程序的源码可以在ESP-IDF的esp-idf/components/bootloader/subproject/main/bootloader_start.c中找到。ESP-IDF使用二级引导程序可以增加FLASH分区的灵活性(使用分区表),并且方便实现FLASH加密,安全引导和空中升级(OTA)等功能。

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

        二级引导程序默认从flash的0x8000偏移地址处(可配置的值)读取分区表。可参考上一篇文章(ESP32学习四-自定义分区表_t_guest的博客-CSDN博客)。引导程序会寻找工厂分区和OTA应用程序分区。如果在分区表中找到了OTA应用程序分区,引导程序将查询otadata分区以确定应引导哪个分区。

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

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

  • 对于在内部IRAM(指令RAM)或DRAM(数据RAM)中具有加载地址的段,将把数据从flash复制到它们的加载地址处。
  • 对于一些加载地址位于DROM(数据存储在FLASH中)或IROM(代码从flash中运行)区域的段,通过配置flash MMU,可为从flash到加载地址提供正确的映射。

        注:二级引导程序同时为PRO CPU和APP CPU配置flash MMU,但仅使能PRO CPU的flash MMU。原因是二级引导程序代码已加载到APP CPU的高速缓存使用的内存区域中。因此使能APP CPU高速缓存的任务就交给了应用程序。

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

        总结

        简单,总结一下:

  • 一级引导程序是固化在ROM中的。从flash的0x1000偏移地址处加载boot程序
  • 二级引导程序,就是bootloader程序,从flash的0x8000处加载分区表。根据分区表运行应用程序
  • bootloader程序在esp-idf/components/bootloader/subproject/main/bootloader_start.c文件中

四、应用程序

        端口初始化

        ESP-IDF应用程序的入口是esp-idf/components/esp_32/cpu_star.c文件中的call_start_cpu0函数。这个函数由二级引导加载程序执行,并且从不返回

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

  • 为应用程序重新配置CPU异常(运行应用程序中断处理程序运行,并使用为应用程序配置的选项来处理严重错误,并不是使用ROM提供的简易版错误处理程序处理)。
  • 如果没有设置选型CONFIG_BOOTLOADER_WDT_ENABLE,则不使能RTC看门狗定时器
  • 初始化内部存储器(数据和bss)
  • 完成MMU高速缓存配置
  • 如果配置了PSRM,则使能PSRAM
  • 将CPU时钟设置为项目配置的频率
  • 根据应用程序头部设置重新配置主SPI FLASH,这是为了与ESP-IDF V4.0之前的引导程序版本兼容。
  • 如果应用程序被配置为在多个内核上运行,则启动另一个内核并等待其初始化(在类似的“端口层”初始化函数call_start_cpu1)

        call_start_cpu0完成运行后,将调用在同一文件下的初始化函数start_cpu0

         系统初始化

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

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

  • 如果默认的日志级别允许,则记录该应用程序的相关信息(项目名称、应用程序版本)等
  • 初始化堆分配器(在这之前,所有分配必须是静态的或在堆栈上)
  • 初始化newlib组件的系统个调用和时间函数
  • 配置断电检测器
  • 根据串行控制台配置设置libc stdin、stdout和stderr
  • 执行与安全相关的检查,包括为该配置烧录efuse(包括禁用ESP32 V3的ROM下载模式、CONFIG_ESP32_DISABLE_BASIC_ROM_CONSOLE).
  • 初始化SPI flash API支持
  • 调用全局C++构造函数和任何标有__attribute__((constructor))的C函数。

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

        

五、总结

        1.一级引导从flash的0x1000处加载bootloader程序

        2.bootlader程序在esp-idf/components/bootloader/bootloader_start.c中。运行call_start_cpu0函数。并打印相关信息

         3.在Bootloader中,从flash的0x8000中获取分区表,并根据分区表,找到应用程序地址,加载应用程序

        4.在esp-idf/components/esp32/cpu_start.c中调用call_start_cpu0函数,开始应用程序的初始化,并打印相关信息。结尾调用start_cpu0函数

         5.因为start_cpu0和start_cpu0_default为弱链接,所以接下来调用start_cpu0_default函数,任务中创建main_task任务,并且启动任务调度器

         6.在main_task中,调用app_main函数,开始执行用户的应用程序代码。