(1)如果有嵌入式企业需要招聘校园大使,湖南区域的日常实习,任何区域的暑假Linux驱动实习岗位,可C站直接私聊,或者邮件:zhangyixu02@gmail.com,此消息至2025年1月1日前均有效
(2)因为实习,需要使用ESP32S3进行开发,而乐鑫官方的esp32_s3_box.c并不是完全符合我们的要求,所以我被安排了解一下esp32的启动流程,然后根据乐鑫的esp32_s3_box.c文件编写一个适配我们自己产品的板级支持包。接下来我根据乐鑫官方文档以及结合自己的理解,梳理一下启动流程。
(3)乐鑫的启动文件个人认为还是有点意思的,因为在我的认知里面,启动文件一般都是采用的汇编代码编写的。但是乐鑫的这个启动文件却是采用的C语言编写(还是有一小部分是用的汇编),理解起来还是方便许多。
(1)ESP32的启动流程分为三段:一级引导程序,二级引导程序,应用程序启动阶段。
(1)一级引导程序:这一部分程序直接存储进入ESP32的ROM中了,所以普通开发者无法直接查看,主要是做一些前期的准备工作。
(2)个人认为,唯一需要了解的是,在这一阶段会判断是否请求自定义启动模式,如 UART 下载模式。这也一定程度说明了,为什么进入下载模式,需要先按住BOOT,然后再按下松开复位键,最后松开BOOT的原因了。
(3)因为怕有人无法理解上面加粗部分,我再详细介绍一下。因为芯片内部的操作是非常迅速的,如果你先按下复位键,再按下BOOT,这个时候一级启动引导程序早就完成了。就无法进入你想要的下载模式。
(4)关于芯片启动的控制介绍如下,原文在ESP32 技术规格书的2.6.1章节:
(1)二级引导程序:这个程序是可查看并且可被修改的,在安装ESP-IDF 的时候,指定的
Enter_ESP-IDF_container_directory
路径下esp-idf\components\bootloader
路径中找到源代码,这一部分应该主要是用于修改bootloader
的能做如下配置:
<1>内部模块的最小化初始配置;
<2>如果配置了 flash 加密 和/或 Secure,则对其进行初始化。
<3>根据分区表和 ota_data(如果存在)选择需要引导的应用程序 (app) 分区;
<4>将此应用程序镜像加载到 RAM(IRAM 和 DRAM)中,最后把控制权转交给此应用程序。
(2)利用对bootloader
的修改配置,能够实现OTA空中升级程序。
<1>OTA 升级机制可以让设备在固件正常运行时根据接收数据(如通过 Wi-Fi 或蓝牙)进行自我更新。
<2>在嵌入式开发过程中,对于绝大多数人来说,单片机的程序烧录都是直接利用一根数据线进行更新程序的。如果是大型的企业开发,SOC一般都是集成在了大型设备里面,我们如果要更新程序,总不可能把每次更新都将整个设备拆除,拿个数据线插上去更新程序,再重新装上吧。因此,有了OTA空中升级机制之后,我们就可以像手机那样,连接上网络,下载更新固件程序,然后就可以更新了。
(1)ESP32的应用程序启动阶段又分为三个阶段:
<1>硬件和基本 C 语言运行环境的端口初始化。
<2>软件服务和 FreeRTOS 的系统初始化。
<3>运行主任务并调用 app_main
(1)在执行完二级引导程序之后,他会跳转到指定的
Enter_ESP-IDF_container_directory
路径下esp-idf\components\esp_system\cpu_start.c
中。
(2)在cpu_start.c
文件中找到call_start_cpu0 ()
函数。这个函数由二级引导加载程序执行,并且从不返回。因此你看不到是哪个函数调用了他,他是从汇编的最底层直接调用的。
(4)在这个函数中会初始化基本的 C 运行环境 (“CRT”),并对 SoC 的内部硬件进行了初始配置。
(5)执行完call_start_cpu0 ()
就会找到Enter_ESP-IDF_container_directory
路径下esp-idf\components\esp_system\startup.c
文件,找到的“系统层”初始化函数start_cpu0()
。其他内核也将完成端口层的初始化,并调用同一文件中的start_other_cores()
。
<1>不过这里的start_cpu0()
函数可能是以start_cpu0_default()
形式体现出来,因为在startup.c
的第110行有如下代码,表示弱关联。
<2>start_other_cores()
这个函数,我也没有找到,个人猜测可能是do_secondary_init()
。因为从注释和函数名上来看,do_secondary_init()
就是对第二个内核的初始化。
void start_cpu0(void) __attribute__((weak, alias("start_cpu0_default"))) __attribute__((noreturn));
(1)主要的系统初始化函数是
start_cpu0()
。默认情况下,这个函数与start_cpu0_default()
函数弱链接。这意味着可以覆盖这个函数,增加一些额外的初始化步骤。
void start_cpu0(void) __attribute__((weak, alias("start_cpu0_default"))) __attribute__((noreturn));
(2)做完一些初始化步骤之后,
start_cpu0_default()
函数就会调用esp_startup_start_app()
准备进入主任务了。
(1)
esp_startup_start_app()
先会检查看门狗的配置环境,CPU0的初始化。最终调用xTaskCreatePinnedToCore()
这个宏在FreeRTOS上创建一个主任务main_task()
任务,名字叫做main()
,无传入参数,任务优先级为1。这个任务不会停止。
(2)main_task()
这个函数实现如下,其实对app_main()
函数进行了再一次的封装,然后执行vTaskDelete(NULL)
进行任务自杀。(这个是FreeRTOS的知识点)
(3)这很好的揭示了,为什么用户使用ESP32是在app_main()
函数里面进行编程。
static void main_task(void* args)
{
app_main();
vTaskDelete(NULL);
}
(4)如果你阅读了ESP32BOX的源代码,你就会发现,
app_main()
这个程序没有死循环。我们都知道,一个嵌入式程序是不能停止的,一定要进行死循环,否则会出现一些问题。
(5)如果你有这样的疑惑,那么就可以看一下系统初始化部分所讲解的start_cpu0_default()
函数。在这个函数里面的最后一样,执行完esp_startup_start_app()
之后,就是执行while (1)
进入死循环了。最后根据你的操作,进入FreeRTOS的任务调度环节。
(1)乐鑫官方文档—应用程序的启动流程;
(2)ESP32 技术规格书;
(3)乐鑫官方文档—引导加载程序 (Bootloader);