【ESP32+freeRTOS学习笔记之“ESP32环境下使用freeRTOS的特性分析(1-启动分析)”】

2023-05-16

目录

  • 1、ESP32启动过程
    • 1.1 一级引导程序
    • 1.2 二级引导程序
    • 1.3 应用程序启动阶段
      • 1.3.1 第一阶段,硬件和基本 C 语言运行环境的端口初始化。
      • 1.3.2 第二阶段,软件服务和 FreeRTOS 的系统初始化。
      • 1.3.3 第三阶段,运行主任务并调用 app_main。
    • 1.4 APP CPU 的内核启动流程
  • 2 代码跟踪与特殊性说明
  • 3、总结

乐鑫的ESP-IDF对FreeRTOS做了深度的适配,使ESP32-IDF的用户能更好的使用FreeRTOS,而无需再去移植FreeRTOS。但开发者在使用适配后的FreeRTOS过程中,会与使用标准的FreeRTOS有一些不同,这些不同不是根本上的区别,只是一些使用顺序及逻辑上的些许改变,但对于在ESP32平台上开发应用却还是很关键。因此要讲情这些区别,必须从ESP32的启动这程说起。

具体的启动过程,在ESP官方资料上有明确的说明,本文第一节是把官方资料搬过来,便于大家阅读,以及快速了解启动的流程。本文的第二节则通过代码跟踪的方式,详细分析了在ESP-IDF中使用FreeRTOS的特殊性。下面进入主题。

1、ESP32启动过程

ESP32的启动,一共分为三个步骤,分别是一级引导、二级引导、主程序运行这三步。一级是由固化在芯片上ROM上的程序进行加载因此是我们无法看到源码进行解读的,只能根据乐鑫提供的技术资料里的内容进行理解。二级引导程序是BootLoad部分是我们可以跟踪和分析的。因此,这部分内容是我们重点开始分析的部分。

1.1 一级引导程序

被固化在了 ESP32 内部的 ROM 中,它会从 flash 的 0x1000 偏移地址处加载二级引导程序至 RAM (IRAM & DRAM) 中。这里一级引导程序会出现三种模式,但因这些不是讨论的重点,因此这里不再赘述。

1.2 二级引导程序

当一级引导程序校验并加载完二级引导程序后,它会从二进制镜像的头部找到二级引导程序的入口点,并跳转过去运行。在 ESP-IDF 中,存放在 flash 的 0x1000 偏移地址处的二进制镜像就是二级引导程序。二级引导程序的源码可以在 ESP-IDF 的 components/bootloader 目录下找到。

二级引导程序作用:从 flash 中加载分区表和主程序镜像至内存中,主程序中包含了 RAM 段和通过 flash 高速缓存映射的只读段。
二级引导程序默认从 flash 的 0x8000 偏移地址处(可配置的值)读取分区表。请参考 分区表 获取详细信息。引导程序会寻找工厂分区和 OTA 应用程序分区。如果在分区表中找到了 OTA 应用程序分区,引导程序将查询 otadata 分区以确定应引导哪个分区。

以上一级引导程序和二级引导程序都是相对固定的内容,只涉及到ESP32的硬件初始化以及环境的设置。不涉及到使用FreeRTOS的特殊部分,因此如果需要详细了解的,可以去乐鑫提供的技术资料里查阅。

1.3 应用程序启动阶段

这时第二个 CPU 和 RTOS 的调度器启动。应用程序启动包含了从应用程序开始执行到 app_main 函数在主任务内部运行前的所有过程。可分为三个阶段:

1.3.1 第一阶段,硬件和基本 C 语言运行环境的端口初始化。

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

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

1、为应用程序重新配置 CPU 异常(允许应用程序中断处理程序运行,并使用为应用程序配置的选项来处理 严重错误,而不是使用 ROM 提供的简易版错误处理程序处理。
2、如果没有设置选项 CONFIG_BOOTLOADER_WDT_ENABLE,则不使能 RTC 看门狗定时器。
3、初始化内部存储器(数据和 bss)。
4、完成 MMU 高速缓存配置。
5、如果配置了 PSRAM,则使能 PSRAM。
6、将 CPU 时钟设置为项目配置的频率。
7、根据应用程序头部设置重新配置主 SPI flash,这是为了与 ESP-IDF V4.0 之前的引导程序版本兼容,请参考 引导加载程序兼容性。
8、如果应用程序被配置为在多个内核上运行,则启动另一个内核并等待其初始化(在类似的“端口层”初始化函数 call_start_cpu1 内)。
call_start_cpu0 完成运行后,将调用在 components/esp_system/startup.c 中找到的“系统层”初始化函数 start_cpu0。其他内核也将完成端口层的初始化,并调用同一文件中的 start_other_cores。

1.3.2 第二阶段,软件服务和 FreeRTOS 的系统初始化。

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

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

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

1.3.3 第三阶段,运行主任务并调用 app_main。

在所有其他组件都初始化后,主任务会被创建,FreeRTOS 调度器开始运行。

做完一些初始化任务后(需要启动调度器),主任务在固件中运行应用程序提供的函数 app_main。

运行 app_main 的主任务有一个固定的 RTOS 优先级(比最小值高)和一个 可配置的堆栈大小。

主任务的内核亲和性也是可以配置的,请参考 CONFIG_ESP_MAIN_TASK_AFFINITY。

与普通的 FreeRTOS 任务(或嵌入式 C 的 main 函数)不同,app_main 任务可以返回。如果app_main 函数返回,那么主任务将会被删除。系统将继续运行其他的 RTOS 任务。因此可以将 app_main 实现为一个创建其他应用任务然后返回的函数,或主应用任务本身。

1.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 代码跟踪与特殊性说明

上面应用程序启动的三个阶段是描述是官方说明文档,但在代码跟踪下来后, 我认为从FreeRTOS的角度来看,实际是分为两个大阶段更为合适,一个是ESP32的启动部分,一个是FreeRTOS的启动部分。代码的跟踪流程如下所示:

->【components/esp_system/port/cpu_start.c中运行call_start_cpu0函数,并在最后调用了这个宏SYS_STARTUP_FN();】
->【components/esp_system/include/esp_private/startup_internal.h中的宏SYS_STARTUP_FN()实为调用g_startup_fn函数指针数组】
->【components/esp_system/startup.c实现数组g_startup_fn[0]=start_cpu0如果是双核则g_starup_fn[1]=start_cpu_other_cores】
->【而start_cpu0()函数又是start_cpu0_default()函数的别名,在最后又调用了esp_startup_start_app()】

这一步是重要的分水岭,在这之前是 ESP32的传统启动部分,但在这之后,就是FreeRTOS的启动部分了,具体见下面第二阶段的描述
->【components/freertos/port/xtensa/port.c中的void esp_startup_start_app(void)函数中调用esp_startup_start_app_common(void)】
->【components/freertos/port/port_common.c中void esp_startup_start_app_common(void)函数中创建了main_task()任务】
->【components/freertos/port/port_common.c 中的main_task()中调用app_main()】

以上内容是代码链接节点的说明,可能简单了一些,有兴趣可以打开程序自已根据上面的流程也跟踪一下,这里不再另外详述,这里重要的是理解FreeRTOS部分的代码,这里的启动方式和标准的FreeRTOS方式有不同。标准的方式是由开发者在main()程序里主动去调用vTaskStartScheduler()。而在ESP32里,在系统的启动阶段的最后会自动启动FreeRTOS的调度器。代码如下:

在这里插入图片描述
esp_startup_start_app_common(void)函数中创建了main_task()任务,该任务的优先级为1,在main_task()任务里,先是启动了esp32的任务软件狗并同时看护在两个内核中运行的FreeRTOS空闲任务Idle0与Idle1.并同时定义了空闲任务的钩子函数(回调函数)用于空闲任务每运行一次喂一次狗。然后启动了app_main()任务。该任务正是开发者可以自定义所有工作的入口。具体如下:

static void main_task(void* args)
{
/*。。。。。。此处省略部分代码。。。。。。*/

    //Initialize task wdt if configured to do so  初始化任务看门狗
#ifdef CONFIG_ESP_TASK_WDT_PANIC
    ESP_ERROR_CHECK(esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, true));
#elif CONFIG_ESP_TASK_WDT
    ESP_ERROR_CHECK(esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false));
#endif

    //Add IDLE 0 to task wdt 把IDLE0加入任务看门狗的监控
#ifdef CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0
    TaskHandle_t idle_0 = xTaskGetIdleTaskHandleForCPU(0);
    if(idle_0 != NULL){
        ESP_ERROR_CHECK(esp_task_wdt_add(idle_0));
    }
#endif
    //Add IDLE 1 to task wdt 把IDLE1加入任务看门狗的监控
#ifdef CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1
    TaskHandle_t idle_1 = xTaskGetIdleTaskHandleForCPU(1);
    if(idle_1 != NULL){
        ESP_ERROR_CHECK(esp_task_wdt_add(idle_1));
    }
#endif

    app_main();  //此处调用用户自定义的主任务。
    vTaskDelete(NULL);
}

从上面的代码可以看出,ESP32在适配FreeRTOS为了程序的安全性,是建议开发者打开任务看门狗对空闲任务进行监控的。因为空闲任务的优先级是0,是最低等级的,所以在开发时要特别注意所有自定义的,优先级高于0级的所有任务都要注意运行时间的管理,在必要时要有阻塞动作,及时让出CPU时间片给空闲任务。否则超出喂狗时间。
在这里插入图片描述
idle_hook_cb()是真正的喂狗动作,该函数存在idle_cb[]数组里,并最后在空闲函数的钩子函数(或叫回调函数)vApplicationIdleHook()内部调用了。这个喂狗动作藏得好深,我跟踪了好久。具体过程是这样的,FreeRTOS的空闲钩子函数对外的接口名称为vApplicationIdleHook()这个函数,而这个函数又在components/freertos/port/xtensa/include/freertos/portmacro.h里定义了一个原型叫esp_vApplicationIdleHook,因此 ,ESP32利用了这个原型,在components/esp_system/freertos_hooks.c的里定义了这个原型函数esp_vApplicationIdleHook()具体如下:

在这里插入图片描述

3、总结

上面这些原码分析,清楚地看到FreeRTOS在ESP32平台中的这些特殊的地方:
1、不需要我们再调用vTaskStartScheduler()函数来启动调度器,因此系统已经帮我们调用了。
2、主函数app_main()本身也是个任务,这个任务的优先级是1,而且该任务只要一返回,就被删除。所以我们在这个主函数中总是把该建的其它任务都建好,然后尽量返回,让系统回收了这个任务。
3、一般情况下ESP32会建立两个任务看门狗(这是可以配置的),用于监控空闲任务的运行时间,当然主要目的是为了防止某些任务进入死循环而导致系统崩溃。这点在调试时要记得,有时候系统不是任务逻辑有问题,可能只是运行时间久了导致空闲任务无法喂狗。
4、在写用户的应用时,要调整好各个任务的优先级,以免出现某任务长时间占用系统时间而导致FreeRTOS的任务调度平衡被打破。这时可以在相关任务里放入一些阻塞相关的函数,来使调度器正常调度。

以上这些内容与FreeRTOS官网里的内容不太一样。这是ESP32与FreeRTOS适配中的特别情况,需要开发者了然于胸。通过代码的追踪能使开发者知其所以然,更有信心的面对程序健壮性的挑战。

关联资料:
esp32应用程序启动流程
ESP32引导加载程序 (Bootloader)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【ESP32+freeRTOS学习笔记之“ESP32环境下使用freeRTOS的特性分析(1-启动分析)”】 的相关文章

随机推荐