您的位置:首页 > 移动开发

ESP32/ESP8266中的应用程序接口user_init()、app_main()到底是什么?

2020-08-02 17:00 169 查看

1.引言

ESP32/ESP8266是目前众多嵌入式开发者、物联网开发者中使用比较热门的开发平台,这不仅得益于其高性价比硬件设计,其软件平台的强大支持以及开源的精神也让该系列的芯片、开发板增色不少。官方提供的软件开发包(SDK)中,给用户提供了编写自己代码的函数接口,即user_init()或app_main(),其中user_init()是ESP8266 RTOS SDK v3.0之前的版本中提供给用户的应用程序接口,之后版本的应用程序接口改为app_main(),而ESP32的SDK中提供的函数接口一直是app_main()。总之,无论是user_init()还是app_main(),它们只是名字不一样,作用完全一样,我们写的代码要在这个函数中完成初始化,它是整个应用程序的入口。

今天就来聊一聊这个该应用程序接口到底是个啥?以及向里面写代码的时候需要注意什么?

2.从一个小实验说起:

写了两段代码,并烧录到ESP8266开发板中跑了一下,代码与实验结果如下:(SDK版本为v3.0)

代码1:

[code]#include <stdio.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"

#define SPIN_ITER 5000 //Actual CPU cycles used will depend on compiler optimization
#define STATS_TICKS pdMS_TO_TICKS(1000)

#define delay_TASK_PRIO 5
TaskHandle_t delayedTask_handler;
#define delay_TASK_PRIO 5

/**
* @brief Function to print the CPU usage of tasks over a given duration.
*just for test
*/

static void delayed_task(void *arg)
{
printf("In delayed_task\n");
while (1) {
//Consume CPU cycles
for (int i = 0; i < SPIN_ITER; i++) {
__asm__ __volatile__("NOP");
}
printf("delay begin\n");
printf("......\n");
printf("delay task end\n");

vTaskDelay(pdMS_TO_TICKS(10000));
}
}

void app_main()
{

vTaskDelay(pdMS_TO_TICKS(100));

for (int i = 3; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
xTaskCreate(delayed_task, "delayed_task", 4096, NULL, delay_TASK_PRIO,    &delayedTask_handler);
}

代码1输出结果如下:

代码2:(其余代码与代码1相同,这里仅贴出app_main()中与之不同的地方)

[code]void app_main()
{

vTaskDelay(pdMS_TO_TICKS(100));

xTaskCreate(delayed_task, "delayed_task", 4096, NULL, delay_TASK_PRIO,    &delayedTask_handler);
for (int i = 3; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}

}

代码2的输出结果如下:

发现了没有?两次输出的结果有哪些不同呢?

代码1的输出结果显示,先执行完app_main()的代码,然后再执行创建的新任务delayed_task()的代码。

代码2的输出结构显示,app_main()的代码仅执行到printf("Restarting in %d seconds...\n", i);,就被新建的任务打断,待新任务执行完毕进入延时后,才回过来继续执行app_main的代码。

也就是说,系统启动后并不是总是执行完app_main()中的所有代码,才去执行新建任务的代码;新建的任务,有可能打断app_main()函数中代码的执行;而app_main()中的代码有可能在别的任务延时或者挂起时,再次接着执行。

在实际的工程代码中,代码2造成后果可能是严重的,主要原因如下:

(1)我们总是希望在系统真的准备好了,再执行真正的功能代码。假设delayed_task()中需要点亮LED灯,如果将创建delayed_task任务的代码放在LED_init()之前,那么delayed_task就不能及时打开LED灯(因为LED灯还没被初始化,它还没准备好),如果这是一个交通灯类的程序的话,将会是严重的系统出错。

[code]void app_main()
{

vTaskDelay(pdMS_TO_TICKS(100));

xTaskCreate(delayed_task, "delayed_task", 4096, NULL, delay_TASK_PRIO,    &delayedTask_handler);

for (int i = 3; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
LED_init();//初始化LED

}

(2)功能代码delayed_task在执行后,不希望看到app_main()中继续输出“垃圾”,即功能代码执行后,不希望再执行app_main()中的printf语句剩余的内容。如果delayed_task任务是向屏幕打印一篇美丽的诗歌,你绝不会想看到在诗歌后面打印出一系列app_main()中的printf内容。

因此,我们总是通过向app_main()中填写代码来实现我们的应用程序,那么app_main()到底是什么呢?

3.app_main()到底是什么?为什么将代码写到这个函数里面就能执行起来呢?

众所周知,CPU启动后将运行第一个“任务”,在无操作系统的环境中,将直接运行我们写的main()函数中的代码,而在有操作系统的环境中,操作系统启动后将创建并运行一个初始化任务。我们使用的SDK是含操作系统的,因此这里仅讨论有操作系统的情况。那么app_main()是我们上面所言的第一个“任务”吗?如果是,它的优先级是怎么分配的呢?

让我们来看一下ESP8266的启动程序,扒一扒app_main()到底在哪调用的:

[code]文件路径:ESP8266_RTOS_SDK/components/esp8266/source/startup.c

static void user_init_entry(void *param)
{

extern void app_main(void);
app_main();

vTaskDelete(NULL);

}
void call_start_cpu(size_t start_addr)
{
......

assert(xTaskCreate(user_init_entry, "uiT", ESP_TASK_MAIN_STACK, NULL, ESP_TASK_MAIN_PRIO, NULL) == pdPASS);

vTaskStartScheduler();

}

上面的代码中,我只截出了部分与本文讨论内容有关的部分。从代码中,我们可以看到,app_main()在user_init_entry()函数中被调用了,调用该函数后,调用了vTaskDelete(NULL)函数。call_start_cpu()函数调用了user_init_entry()函数。call_start_cpu()函数就是CPU启动后,运行的代码。值得注意的是,在call_start_cpu()函数中调用xTaskCreate创建的函数体为user_init_entry的任务并不会立即运行,因为此时调度器还未开始工作,只有执行到vTaskStartScheduler();语句才启动任务调度,开始执行app_main()中的语句。(可以认为此时的任务在挂起就绪列表,调度开启,则进入就绪列表,开始占用CPU执行)

其执行逻辑如上图所示。如此,我们对app_main()的认识便清晰了,它并不是一个任务(因为你并没有看见有xTaskCreat()函数创建与之对应的任务),它只是系统第一个执行的任务user_init_entry中的一个普通函数。此外,由于user_init_entry()在调用app_main()函数后,调用了vTaskDelete(),因此如果app_main()执行完毕(即返回),就会执行这条vTaskDelete(NULL);语句,也即删除该第一个任务user_init_entry。

有了上述对app_main()的认识,更进一步地,我们来看看调用app_main()的系统第一个执行的任务user_init_entry的优先级是如何设置的。其优先级设置如下:

[code]ESP8266_RTOS_SDK/components/esp_common/include/esp_task.h
#define ESP_TASK_PRIO_MIN (0)
#define ESP_TASK_MAIN_PRIO            (ESP_TASK_PRIO_MIN + 1)

即,系统第一个运行的任务的优先级为1。(优先级真的很低,最低了)。

因此,我们对app_main()的认识更清晰了,它在一个优先级为1的、系统运行的第一个任务user_init_entry中被调用,并且,只有app_main()执行完毕,才能执行user_init_entry中的vTaskDelete()语句删除第一个任务(防止再次运行)。

结合第二小节的实验结果,就能知道为什么代码2会出现app_main()的代码仅执行到printf("Restarting in %d seconds...\n", i);,就被新建的任务打断,待新任务执行完毕进入延时后,才回过来继续执行app_main的代码。因为在新建第一个任务后,就启动了调度器,新建的任务delayed_task的优先级为5,高于系统第一个执行的任务user_init_entry的优先级,因此,创建完delayed_task任务后,当app_main()执行vTaskdelay(),新建的任务delayed_task就会被调度器选中,并开始执行。等待delayed_task执行结束,开始延时时,app_main()才再次被调度器选中并开始接着执行。

(注,上述代码选取的是ESP8266的部分相关代码,ESP32与之类似,原理一模一样,感兴趣的可以查看ESP32的cpu_start.c和task.h文件)

4.总结与建议:

(1)app_main()是被系统创建的第一个任务调用的功能函数,该任务的优先级很低(容易被新建的高优先级的任务打断)。(本质:第一个运行的、只运行一次的、低优先级的任务中的一个函数。)

(2)一旦开始执行app_main()中的语句,意味着FreeRTOS任务调度已开启

(3)只有app_main()执行完毕,系统执行的第一个任务user_init_entry中的vTaskDelete()语句才能被执行,完全删除第一个任务(防止再次运行)。

(4)在实际的工程代码中,尽量保证app_main()中的代码的“干净整洁”,仅在其中完成一些不会产生垃圾、不影响后续代码执行的功能代码。因为,写出如上述代码2的代码,将会造成系统还未完全初始化硬件就执行其他任务或者app_main()被打断后接着执行,进而产生垃圾,干扰其他任务执行的恶劣情况。(具体影响,请回顾第2小节的分析)

Init_main()/app_main()中适合做什么?

  1. 设置硬件参数
  2. 调用初始化硬件的函数接口,初始化硬件
  3. 创建要使用的对象(包括任务、队列、信号量等)

Init_main()/app_main()中不适合做什么?

1.尽量不要在创建新的对象之后,在该函数内调用延时或阻塞函数,如不要调用挂起函数,慎重使用延时函数。总之,尽可能地让它快速地、干净地执行完是最好的选择。

(谢谢点赞或收藏)

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: