提示:本博客作为学习笔记,有错误的地方希望指正
一、ESP32 启动流程介绍
??参考资料:ESP IDF编程手册V4.4 ??我这里主要从系统代码层来分析esp32的启动过程。esp32 demo中默认使用的是freertos操作系统,我们最开始创建工程的时候或者demo中一定有void app_main(void)这个函数,这个是官方规定的,我们就从这个函数开始分析,分析函数的调用过程,一层一层向剥茧一样的剥开整个启动流程。 ??其中启动流程在两个层中,一个为系统层freeRTOS中,另外一个在系统层中,esp_system中,我就从这两个层开始分析。
二、freeRTOS层
??最开始在创建时候就存在app_main函数,分析是谁在调用它,我们可以使用vscode打开安装包中的components下面freertos文件夹,搜索app_main。
/用户安装位置/esp-idf/components/freertos/port/port_common.c
?? 在port_common.c .c文件中存在下面这两个函数
void esp_startup_start_app_common(void)
{
#if CONFIG_ESP_INT_WDT
esp_int_wdt_init();
esp_int_wdt_cpu_init();
#endif
esp_crosscore_int_init();
#ifdef CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME
esp_gdbstub_init();
#endif
portBASE_TYPE 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 !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)
{
#if !CONFIG_FREERTOS_UNICORE
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
heap_caps_enable_nonos_stack_heaps();
#if CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL
if (g_spiram_ok) {
esp_err_t r = esp_spiram_reserve_dma_pool(CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL);
if (r != ESP_OK) {
ESP_EARLY_LOGE(TAG, "Could not reserve internal/DMA pool (error 0x%x)", r);
abort();
}
}
#endif
#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
#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
#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);
}
??在main_task主要的执行逻辑是一些空闲钩子函数和看门狗的东西。然后就是调用app_main,最后删除任务。 ??在esp_startup_start_app_common也是关于看门狗的配置和为该CPU初始化系统核中断系统,最后创建main_task的任务。
/用户安装位置/esp-idf/components/freertos/port/xtensa/port.c的文件中
??在上面的路径中port.c .c文件中esp_startup_start_app 中调用了esp_startup_start_app_common函数,执行esp_startup_start_app_common函数之后就开启任务调度。 ??上面就是整个freertos中系统启动的流程。
三、esp_system层
??在上述的freertos启动中我们继续分析到esp_startup_start_app,继续看看这个函数怎么被调用的。在components文件中esp_system中可以找到该函数被调用。
/用户安装位置/esp-idf/components/esp_system/startup.c
??start_cpu0_default函数中调用esp_startup_start_app函数,具体的代码如下。
static void start_cpu0_default(void)
{
ESP_EARLY_LOGI(TAG, "Pro cpu start user code");
int cpu_freq = esp_clk_cpu_freq();
ESP_EARLY_LOGI(TAG, "cpu freq: %d", cpu_freq);
if (LOG_LOCAL_LEVEL >= ESP_LOG_INFO) {
const esp_app_desc_t *app_desc = esp_ota_get_app_description();
ESP_EARLY_LOGI(TAG, "Application information:");
#ifndef CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR
ESP_EARLY_LOGI(TAG, "Project name: %s", app_desc->project_name);
#endif
#ifndef CONFIG_APP_EXCLUDE_PROJECT_VER_VAR
ESP_EARLY_LOGI(TAG, "App version: %s", app_desc->version);
#endif
#ifdef CONFIG_BOOTLOADER_APP_SECURE_VERSION
ESP_EARLY_LOGI(TAG, "Secure version: %d", app_desc->secure_version);
#endif
#ifdef CONFIG_APP_COMPILE_TIME_DATE
ESP_EARLY_LOGI(TAG, "Compile time: %s %s", app_desc->date, app_desc->time);
#endif
char buf[17];
esp_ota_get_app_elf_sha256(buf, sizeof(buf));
ESP_EARLY_LOGI(TAG, "ELF file SHA256: %s...", buf);
ESP_EARLY_LOGI(TAG, "ESP-IDF: %s", app_desc->idf_ver);
}
do_core_init();
do_global_ctors();
do_secondary_init();
#ifndef CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE
wdt_hal_context_t rtc_wdt_ctx = {.inst = WDT_RWDT, .rwdt_dev = &RTCCNTL};
wdt_hal_write_protect_disable(&rtc_wdt_ctx);
wdt_hal_disable(&rtc_wdt_ctx);
wdt_hal_write_protect_enable(&rtc_wdt_ctx);
#endif
#if SOC_CPU_CORES_NUM > 1 && !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE
s_system_full_inited = true;
#endif
esp_startup_start_app();
while (1);
}
??在这个函数中主要打印一些esp32的系统信息,如:CPU频率、分区、工程名、APP版本、编译时间、用ELF文件的SHA256填充所提供的缓冲区等,之后就是调用esp_startup_start_app,最后是进入死循环中。 ??其中start_cpu0_default使用弱连接。 其中关于__attribute__的详细介绍可以参考这篇博客。C之attribute用法、C语言__attribute__的使用、attribute_ 之weak,alias属性
void start_cpu0(void) __attribute__((weak, alias("start_cpu0_default"))) __attribute__((noreturn));
??由于attribute的作用我们继续搜索start_cpu0,在源码startup.c中我们找到下面g_startup_fn这个数组。
const sys_startup_fn_t g_startup_fn[SOC_CPU_CORES_NUM] = { [0] = start_cpu0,
#if SOC_CPU_CORES_NUM > 1
[1 ... SOC_CPU_CORES_NUM - 1] = start_cpu_other_cores
#endif
};
??而这个数组的数据类型是sys_startup_fn_t类型的,我们顺便看看这个类型是啥类型。在头文件start_internal.h文件中我们可以看到sys_startup_fn_t的原定义。
typedef void (*sys_startup_fn_t)(void);
??这里主要由typedef定义的一个函数指针,即g_startup_fn就是一个函数指针数组。这里有一篇关于typedef的函数指针的声明的文章:typedef定义的函数指针 ??其中关于函数指针和指针函数的区分的话我是按照中文语法和C语言括号优先级来理解记忆更好,不然容易混淆。 ??普通函数:函数作为主语,这个谁我们经常使用的也是最容易理解的。函数有返回数据类型、函数名、函数参数组成。 ??普通函数定义如下:
<返回数据类型> <函数名> (<参数>) {
代码;
}
??指针函数:函数作为主语,被指针修饰,所以主体是一个函数,是函数自然有返回类型和函数的参数。 ??指针函数定义如下:
<返回数据类型> *<函数名>( <参数>) {
代码;
}
??函数指针:指针作为主语,被函数修饰,所以主体是一个指针,是指针自然有它是什么数据类型。
<函数返回数据类型> (* <函数名>) (<参数>) {
代码;
}
??下面就是普通函数、指针函数、函数指针的定义示例,指针函数和普通函数的区别在于函数名前面多了一个“*”号。
void fun(int x, int y);
void *fun(int x, int y);
void (*fun)(int x, int y);
??继续在头文件start_internal.h文件中我们看到一个#define的宏定义
#define SYS_STARTUP_FN() ((*g_startup_fn[(cpu_hal_get_core_id())])())
??然后我们看下在哪个地方调用SYS_STARTUP_FN(),在/安装路径/esp-idf/components/esp_system/port/cpu_start.c 中 调用宏定义SYS_STARTUP_FN() 在call_start_cpu0、和call_start_cpu1的函数中都有调用SYS_STARTUP_FN(),ESP32一些系列是双核,根据不同系列的芯片去适配调用不同的call_start_cpu_x。 在最后就是ENTRY(call_start_cpu0); ??到此ESP32的启动流程就分析结束了,就像剥茧一样的一层一层的剥开整个框架,这里主要分为两个层,一个是freeRTOS层,另外一个层就是对于esp32的整个系统层esp_system。了解其中的核心启动流程方便对于整个系统框架的理解和后面可能会出现的问题可以有一个排查的方向。 ??整体的启动文件比较多,使用了许多的条件编译以适应不同的cpu,里面也有需要C语言的高级用法,可以顺便学习学习,可能有些分析不正确的,希望大家指正。
|