一、概念介绍

1.1什么是单片机的启动流程

单片机的启动流程指的是单片机从上电或复位后到开始执行用户代码的一系列初始化步骤。不同的单片机的启动流程有其特定的细节,但大多数单片机的启动流程都遵循一个通用的模式。
通用启动流程:

  1. 上电或复位:当单片机接通电源或被复位时,启动流程开始。复位可以是软件触发的,也可以是通过复位引脚进行的硬件复位。
  2. 执行内置启动代码:单片机首先执行存储在内部 ROM 中的固件,这通常是由单片机制造商预先编程的。这段代码不可更改,称为启动加载程序或自举加载程序(Bootloader)。
  3. 硬件初始化:启动加载程序进行硬件初始化,包括设置时钟(例如晶振或内部RC振荡器)、配置电源管理、初始化内存控制器等。
  4. 检查启动模式:单片机可能会检查某些引脚或配置寄存器来确定启动模式,比如是否进入编程模式、正常启动还是其他特殊模式。
  5. 加载用户程序:在正常启动模式下,启动加载程序会从预设的内存位置(通常是内置或外部的非易失性存储器,如 Flash)加载用户程序的初始代码到 RAM 中。
  6. 跳转到主程序:一旦用户程序被加载,控制权被交给用户程序的入口点,通常是 main() 函数。在这一点上,用户程序开始执行。
  7. 用户程序执行:用户程序会根据设计执行初始化代码,比如设定外设、初始化变量、设置中断服务例程等,然后进入主循环或开始任务调度(如果使用了操作系统)。

    二、ESP32启动流程概述

    ESP32-DevKitC开发板:

    ESP32-DevKitC引脚图:

    ESP32功能框图

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

2.1 一级引导程序 (First Stage Bootloader)

  • 存储位置:一级引导程序被烧录在 ESP32 的内部只读存储器(ROM)中,所以它是不可更改的。
  • 主要职责:初始化硬件,比如配置时钟系统、电源管理和内存。

    详解:

    ESP32上电复位后的启动流程
  1. 首先是PRO CPU 激活:(ESP32 是一款双核的微控制器,分为 PRO CPU(通常为主 CPU)和 APP CPU)
    在复位之后,PRO CPU 首先被激活,开始执行位于 ESP32 芯片掩膜 ROM 中的复位向量代码(复位向量代码是指在微控制器或其他类型的处理器中,在复位事件发生后首先执行的一段代码)
    这个地址是 0x40000400,不可修改。
    APP CPU 在这个阶段仍处于复位状态,不参与初始化过程。

  2. 执行复位向量代码:
    复位向量代码首先会检查 GPIO_STRAP_REG 寄存器的值,该寄存器记录了复位时不同引脚的状态,用于确定 ESP32 的工作模式。

  3. 工作模式判定:
    从深度睡眠模式复位:
    系统检查 RTC_CNTL_STORE6_REG 和 RTC_CNTL_STORE7_REG 寄存器的值。
    如果 RTC_CNTL_STORE6_REG 的值非零,并且 RTC_CNTL_STORE7_REG 的 CRC 校验有效,则跳转到 RTC_CNTL_STORE6_REG 指定的地址执行代码。
    如果校验无效或 RTC_CNTL_STORE6_REG 为零,或者指定地址的代码执行完毕返回,则执行上电复位相关操作。

  4. 上电复位、软件 SoC 复位、看门狗 SoC 复位:
    系统会检查是否有 UART 或 SDIO 的下载模式请求。
    如果有,则配置 UART 或 SDIO 接口等待下载代码。
    如果没有,则继续执行软件 CPU 复位的相关操作。

  5. 加载和执行代码:
    对于软件 CPU 复位和看门狗 CPU 复位,系统会配置 SPI Flash,并尝试从 Flash 加载代码至执行内存。
    如果加载失败,会将 BASIC 解析器解压缩到 RAM 并尝试启动。
    需要注意的是,此时 RTC 看门狗可能仍然启用,如果在设定时间内没有输入事件,看门狗将会复位 SoC,重启整个流程。
    如果 UART 接收到输入,程序将关闭看门狗计时器。

  6. APP CPU 启动:
    在 call_start_cpu0 函数中,APP CPU 的复位状态被解除,它开始执行程序,加入到系统的任务处理中。

2.2 二级引导程序 (Second Stage Bootloader)

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

详解

  1. 二级引导程序的功能和位置

    • 存储位置:在 flash 内存的 0x1000 偏移处存放有二级引导程序的二进制镜像。
    • 源码位置:源码位于 ESP-IDF 的 components/bootloader 目录下,允许开发者查看和修改源码以适应特定的应用需求。
    • 增加灵活性:二级引导程序使得 flash 分区表的使用变得灵活,有助于系统资源的配置和管理。
    • 安全功能:提供了 flash 加密、安全启动和空中升级(OTA)等高级功能的实现基础。
  2. 启动过程

    • 加载过程:一级引导程序负责校验并加载二级引导程序到 RAM。它从二进制镜像的头部找到入口点,然后跳转到该地址,开始执行二级引导程序。
    • 读取分区表:二级引导程序默认从 0x8000 偏移地址读取分区表,该地址是可以配置的。分区表包含了如何在 flash 中组织不同数据(如应用程序、文件系统等)的信息。
    • 分区表和 OTA:引导程序会寻找工厂分区和 OTA 应用程序分区。如果找到 OTA 应用程序分区,引导程序会检查 otadata 分区以决定哪个分区应当被启动。(OTA更新是指通过网络(如 Wi-Fi 或蜂窝网络)直接在设备上安装或升级其操作系统或应用程序的过程)
    • 配置选项:ESP-IDF 提供了丰富的配置选项,我们可以根据需要调整引导加载程序的行为。
  3. 读取和映射程序段

    • 内部 RAM 段:对于需要加载到内部 IRAM 或 DRAM 的段,二级引导程序将数据从 flash 复制到相应的加载地址。
    • DROM/IROM 段:对于加载地址位于 DROM 或 IROM 区域的段,二级引导程序配置 flash MMU 映射,以便从 flash 直接执行。
    • Flash MMU 配置:二级引导程序为 PRO CPU 和 APP CPU 配置 flash MMU,但仅使能 PRO CPU 的 MMU。APP CPU 的 MMU 使能将留给应用程序负责,因为二级引导程序已经被加载到了 APP CPU 将要使用的内存区域中。
  4. 应用程序的加载和执行

    • 完整性校验:在所有段处理完毕后,二级引导程序会验证应用程序的完整性,确保代码未被篡改。
    • 跳转执行:它从二进制镜像文件的头部寻找应用程序的入口地址,然后跳转到该地址,正式开始执行用户的应用程序。

2.3应用程序启动阶段

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

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

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

  3. 运行主任务并调用 app_main。

详解

端口初始化

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

call_start_cpu0 函数

  • 位置:在 components/esp_system/port/cpu_start.c 中。
  • 功能:作为 ESP-IDF 应用程序的入口点,由二级引导程序调用以启动 PRO CPU(第一个 CPU 核心)。
  • 特点:此函数执行一系列初始化任务,且从不返回,因为它最终会跳转到应用程序的主函数。

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

初始化功能

  1. C 运行环境初始化

    • 设置基本的 C 运行时环境,如初始化堆栈、数据段、BSS段等。
    • 这为 C 语言编写的程序代码运行提供必要的环境。
  2. SoC 硬件配置

    • 对 ESP32 芯片的内部硬件进行初始设置,包括时钟、GPIO、中断等。
  3. CPU 异常配置

    • 重新配置 CPU 异常处理器,允许应用程序自己处理中断和严重错误,而不是依赖于 ROM 中的基本错误处理程序。
  4. RTC 看门狗定时器

    • 如果没有启用 CONFIG_BOOTLOADER_WDT_ENABLE 配置,则不使能 RTC 看门狗。
    • 看门狗定时器用于监控系统运行状态,防止系统挂起。
  5. 内存初始化

    • 初始化内部存储器,清零数据段和未初始化的数据段(BSS)。
  6. MMU 和缓存配置

    • 完成内存管理单元(MMU)的设置,配置 CPU 的缓存行为。
  7. PSRAM 配置

    • 如果配置了外部 PSRAM(Pseudo-static RAM),则进行初始化并使能。
  8. CPU 时钟配置

    • 根据项目配置设置 CPU 运行的时钟频率。
  9. 主 SPI Flash 配置

    • 根据应用程序头部设置,重新配置主 SPI Flash,确保与 ESP-IDF V4.0 之前版本的引导程序兼容性。
  10. 启动其他内核

    • 如果应用程序配置为多核运行,则启动 APP CPU,并在 call_start_cpu1 函数中完成类似的初始化。

系统层初始化

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

  • start_cpu0 函数

    • call_start_cpu0 执行完毕后,接下来调用 startup.c 文件中的 start_cpu0 函数。
    • 这个函数继续进行系统级别的初始化,如 FreeRTOS 的启动、系统服务和守护进程的初始化。
  • start_other_cores 函数

    • 对于多核心配置,每个内核在完成端口层的初始化后,将调用 start_other_cores 函数,以保证所有内核同步完成系统层的初始化。

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

  1. 记录应用程序信息:

    • 如果系统设置的日志级别允许,初始化阶段会记录应用程序的重要信息,如项目名称和版本。这对于调试和问题追踪非常有用,因为它可以快速告诉开发者或维护者当前运行的程序详细信息。
  2. 初始化堆分配器:

    • 在这个阶段之前,所有的内存分配都需要是静态的,也就是说,在编译时就已经确定,或者是分配在栈上的,即临时的局部变量。初始化堆分配器后,程序就能动态地在堆上分配内存,这对于大多数运行时内存管理任务来说是必须的。
  3. 初始化newlib组件:

    • newlib是为嵌入式系统设计的C标准库实现。在这个阶段,系统调用(syscalls)和时间函数被初始化,这样应用程序就可以使用标准的C库函数,如文件操作和时间获取。
  4. 配置断电检测器:

    • 这保证了系统在电源不足时能够检测并采取适当的措施,可能是通过保存状态或者优雅地关闭,避免数据损坏或不一致性。
  5. 设置libc的标准输入输出:

    • 标准输入输出(stdin、stdout、stderr)被配置为通过串行控制台进行通信,这对于实时调试信息的输出和错误消息的报告至关重要。
  6. 执行安全相关的检查:

    • 在这一阶段,系统会执行一系列的安全检查,包括烧录efuse,这是一种只能写入一次的存储器,用于存储加密密钥或配置选项等。例如,这可能包括禁用ESP32 V3的ROM下载模式,这样就不允许通过ROM引导程序的简单控制台来重新编程设备,从而提高安全性。
  7. 初始化SPI flash API支持:

    • SPI flash是ESP32设备常用的存储介质。初始化SPI flash API支持意味着设置了与存储设备交互的软件接口,允许应用程序读写flash。
  8. 调用全局C++构造函数和带有特殊属性的C函数:

    • 在C++中,全局对象需要在main函数执行之前构造。此阶段确保所有全局对象的构造函数都被调用。对于C语言,任何标有__attribute__((constructor))属性的函数也会在这个时候被执行。这些函数通常用于初始化工作。
  9. 二级系统初始化:

    • 在这个阶段,系统允许单独的组件进行初始化。如果组件声明了一个用ESP_SYSTEM_INIT_FN宏注释的初始化函数,它会在这里被调用。这允许更细粒度的控制,组件可以独立地进行它们所需的任何启动配置。

运行主任务

在所有其他组件都初始化后,主任务会被创建,FreeRTOS 调度器开始运行。
在完成所有其他组件的初始化之后,系统会创建一个主要任务,并启动FreeRTOS的任务调度器。之后,这个主任务会执行固件内由程序员提供的app_main函数。
app_main在执行时,主任务都会有一个预定的RTOS优先级,这个优先级高于最低可能的设置值,并且它的堆栈大小是可以根据需要进行配置的。另外,可以通过CONFIG_ESP_MAIN_TASK_AFFINITY设置,来指定主任务运行在特定的CPU核心上。
不同于一般的FreeRTOS任务或者嵌入式C程序中的main函数,app_main函数执行完毕后可以选择返回。一旦app_main返回,主要任务就会结束并从系统中删除,但FreeRTOS将继续运行系统中的其它任务。这样,app_main可以设计为仅仅用来启动其他任务然后退出,或者作为主应用程序任务一直运行。
解释一下上面提到的部分名词:

  1. 创建主任务:

    • 在FreeRTOS中,所有的代码都是在任务中运行的。初始化过程完成后,系统会创建一个新的任务,这个任务用于运行 app_main 函数。
  2. 启动RTOS调度器:

    • FreeRTOS调度器是负责任务管理的核心组件。它决定哪个任务应该在何时运行。在主任务被创建后,调度器开始运行,管理app_main 以及系统中的其他任务。
  3. 运行 app_main 函数:

    • app_main 是用户定义的入口点,它在主任务中执行,完成应用程序特定的初始化任务。这可能包括设置硬件外设、初始化全局资源、创建额外的任务等。
  4. 主任务的属性:

    • 主任务运行 app_main 时,它具有一个固定的RTOS优先级,该优先级高于FreeRTOS的最小优先级。这保证了 app_main 能够在系统启动时相对较快地执行。
    • 主任务的堆栈大小是可配置的,以确保有足够的内存空间执行 app_main 中的代码。
    • 通过 CONFIG_ESP_MAIN_TASK_AFFINITY 配置项,可以设置主任务的内核亲和性,即它应该在哪个CPU核心上运行,这对于多核处理器来说很重要。
  5. 任务的生命周期:

    • 与普通的FreeRTOS任务不同,app_main 在执行完毕后可以返回,而不是无限循环。当 app_main 返回后,主任务会被删除。
    • 即使主任务被删除,FreeRTOS调度器仍会继续运行,调度其他可能已创建的任务。
  6. 系统的持续运行:

    • app_main 可以被实现为仅初始化和创建其它任务,然后返回。在这种情况下,主要的应用逻辑会在 app_main 创建的其他任务中执行。
    • 如果 app_main 返回,FreeRTOS和ESP-IDF会确保系统继续运行其他任务。这使得 app_main 可以根据需要灵活地设计,要么作为主要的应用程序任务,要么作为启动其他任务的初始化例程。