ESP32-S3-WIFI-FreeRTOS 任务
介绍
FreeRTOS
FreeRTOS是一个开源的实时操作系统(RTOS)内核,以模块化的方式与ESP-IDF集成。这意味着所有的ESP-IDF应用程序和各种ESP-IDF组件都建立在FreeRTOS框架之上。FreeRTOS内核已经被移植到ESP芯片的所有CPU架构上,包括Xtensa和RISC-V。
在ESP32中,WIFI的操作与FreeRTOS的多任务紧密相连,并且彼此之间存在相互依赖的关系。实际上,FreeRTOS作为一种用于实时操作系统的开源软件,它提供了任务调度和管理的功能。而ESP32作为一款集成了WIFI功能的芯片,充分利用了FreeRTOS的多任务处理机制来实现同时处理多个WIFI连接和数据传输的能力。因此,了解和熟悉FreeRTOS的多任务编程模型对于有效地操作和管理ESP32的WIFI功能至关重要。通过合理的任务分配和调度,能够充分利用ESP32的资源,提高WIFI的性能和稳定性。同时,合理处理WIFI任务与其他任务的并行执行,能够确保系统的整体响应性和吞吐量。
显示当前所有的任务
在开始前让我们先看看不进行任何操作,ESP3都有哪些任务
首先在 menuconfig中修改打开下面👇设置
然后运行以下程序
c
/**
* @brief 显示当前所有的任务
*
*/
void task_list(){
char ptrTaskList[250];
vTaskList(ptrTaskList);
printf("********************************************\n");
printf("Task State Prio Stack Num\n");
printf("********************************************\n");
printf(ptrTaskList);
printf("********************************************\n");
}
void app_main(void){
task_list();
}
效果如下
以下是将FreeRTOS任务列表用表格表示的结果:
任务名称 | 状态 | 优先级 | 剩余堆栈空间 (字节) | 任务编号 | 功能描述 |
---|---|---|---|---|---|
main | X | 1 | 2036 | 4 | 应用主任务,通常用于启动其他任务和初始化系统。 |
IDLE1 | R | 0 | 812 | 6 | 空闲任务之一,通常在系统空闲时执行,一般进行喂狗。 |
IDLE0 | R | 0 | 1008 | 5 | 空闲任务之二,通常在系统空闲时执行,一般进行喂狗。 |
esp_timer | B | 22 | 3352 | 3 | 定时器服务任务,处理系统定时器事件。 |
ipc1 | S | 24 | 524 | 2 | 进程间通信任务之一,用于处理高优先级的IPC操作。 |
Tmr Svc | B | 1 | 1320 | 7 | 定时器服务任务,管理应用程序定时器回调函数的执行。 |
ipc0 | S | 24 | 512 | 1 | 进程间通信任务之二,用于处理高优先级的IPC操作。 |
各个字段含义:
- 任务名称 (Task): 任务的名称。
- 状态 (State) : 任务的状态。
X
: 未知状态R
: 运行中 (Running)B
: 阻塞中 (Blocked)S
: 挂起 (Suspended)
- 优先级 (Prio): 任务的优先级,数值越大优先级越高。
- 剩余堆栈空间 (Stack): 任务剩余的堆栈空间大小,以字节为单位。
- 任务编号 (Num): 任务的编号。
带有WIFI功能时的任务
我们将WIFI扫描的功能加入到app_main
中
c
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/**
* @brief WIFI扫描
*
*/
void wifi_scan(){
ESP_LOGI("WIFI", "0. 初始化NVS存储");
ESP_ERROR_CHECK(nvs_flash_init()); // 对NVS默认的区域进行初始化
ESP_LOGI("WIFI", "1. WIFI 初始化阶段");
esp_netif_init(); // 1.1 创建一个 LwIP 核心任务
esp_event_loop_create_default(); // 1.2 创建一个系统事件任务
esp_netif_create_default_wifi_sta(); // 1.3.1 创建有 TCP/IP 堆栈的默认网络接口实例
wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_config); // 1.3.2 创建 Wi-Fi 驱动程序任务
ESP_LOGI("WIFI", "2. WIFI 配置阶段");
esp_wifi_set_mode(WIFI_MODE_STA); // 2 将 Wi-Fi 模式配置为 station
ESP_LOGI("WIFI", "3. WIFI 启动阶段");
esp_wifi_start(); // 3.1 启动 Wi-Fi 驱动程序
ESP_LOGI("WIFI", "4. WIFI 扫描");
//在所有信道中扫描全部 AP(前端)
wifi_country_t country_config = {
.cc = "CN",
.schan = 1,
.nchan = 13,
};
esp_wifi_set_country(&country_config); // 4.1 扫描配置国家代码
wifi_scan_config_t scan_config = {
.show_hidden = true // 显示隐藏
};
esp_wifi_scan_start(&scan_config,true); // 4.2 配置扫描信息 true表示当这个任务执行的时候,回进入阻塞状态等待扫描
//得到扫描的AP数量
uint16_t ap_num =0;
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_num));
ESP_LOGI("WIFI","AP Count : %d",ap_num);
//获取具体的AP信息
uint16_t max_aps=20;
wifi_ap_record_t ap_records[max_aps];
memset(ap_records,0,sizeof(ap_records));
uint16_t aps_count =max_aps;
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&aps_count,ap_records));
//打印信息
ESP_LOGI("WIFI","AP Count: %d",aps_count);
printf("%30s %3s %3s %3s\n","SSID","频道","强度","MAC地址");
for(int i=0;i<aps_count;i++){
printf("%30s %4d %4d %02X-%02X-%02X-%02X-%02X-%02X\n",
ap_records[i].ssid,
ap_records[i].primary,
ap_records[i].rssi,
ap_records[i].bssid[0],
ap_records[i].bssid[1],
ap_records[i].bssid[2],
ap_records[i].bssid[3],
ap_records[i].bssid[4],
ap_records[i].bssid[5]);
}
}
/**
* @brief 显示当前所有的任务
*
*/
void task_list(){
char ptrTaskList[250];
vTaskList(ptrTaskList);
printf("********************************************\n");
printf("Task State Prio Stack Num\n");
printf("********************************************\n");
printf(ptrTaskList);
printf("********************************************\n");
}
void app_main(void)
{
wifi_scan();
task_list();
for(;;){
vTaskDelay(1000/portTICK_PERIOD_MS);
}
}
可见怎加了WIFI扫描后任务列表中增加了以下任务:
- tiT: 优先级18,状态为阻塞 (B),堆栈空间为2436字节,任务编号为8。
- sys_evt: 优先级20,状态为阻塞 (B),堆栈空间为1588字节,任务编号为9。
- wifi: 优先级23,状态为运行 ®,堆栈空间为4384字节,任务编号为10。
这些任务对应的就是,ESP-IDF编程文档中关于WIFI编程流程的图片,对应关系如下👇。
而对于App task任务在上图1.4步骤创建
创建App task方法如下
c
void app_task(void* pt){
ESP_LOGI("app_task","App Task创建成功");
vTaskDelete(NULL);
}
xTaskCreate(app_task,"App Task",1024*12,NULL,1,NULL); //1.4 创建app_task任务
五个任务分别是什么
到现在五个任务都有了,它们分别执行的是什么功能呢
任务功能描述
-
Main task::主任务通常负责系统初始化和启动其他任务。在Wi-Fi连接过程中的主要作用是初始化并启动Wi-Fi连
-
App task:应用任务负责应用程序的主要逻辑处理,例如处理用户请求、数据处理等。在Wi-Fi连接过程中的主要作用是处理Wi-Fi连接状态的变化并相应地通知其他任务。
-
Event task :事件任务负责处理系统和应用程序的事件,在Wi-Fi连接过程中主要负责处理Wi-Fi事件,作为中间媒介促使两个任务沟通,例如连接成功、断开连接、获取IP地址等。
例如,在启动阶段,"主任务"(Main task)启动了Wi-Fi的激活。一旦Wi-Fi任务成功启动,它会向"事件任务"(Event task)发送一条消息,指示WIFI_STA已启用。收到此信息后,事件任务会向应用程序任务(App task)提供反馈,通知它WIFI_STA已启用。
-
LwIP task:LwIP任务负责TCP/IP协议栈的处理,包括网络数据包的发送和接收。在Wi-Fi连接过程中,LwIP任务负责网络数据的处理和传输。
-
Wi-Fi task:Wi-Fi任务负责Wi-Fi的管理和控制,包括扫描可用网络、连接到指定网络、处理Wi-Fi事件等。
事件循环库
事件循环库使组件能够声明事件,允许其他组件注册处理程序(即在事件发生时执行的代码片段)。此时,无需直接涉及应用程序,松散耦合组件也能够在其他组件状态变化时附加所需的行为。此外,通过将代码执行序列化,在指定的任务中运行事件循环库,可以简化事件处理程序,实现更高效的事件处理。
那么如何在代码中捕获上图的1,2的信息呢
我们修改代码如下
cpp
#include "esp_event.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// 定义事件处理程序
void run_on_event(void* handler_arg, esp_event_base_t base, int32_t id, void* event_data)
{
// 事件处理程序逻辑
ESP_LOGE("event_handle", "事件处理程序 base: %s, id: %d", base, id);
}
// 应用任务
void app_task(void* pt) {
ESP_LOGI("app_task", "App Task创建成功");
// 注册事件处理程序
esp_err_t err = esp_event_handler_register(ESP_EVENT_ANY_BASE, ESP_EVENT_ANY_ID, run_on_event, NULL);
if (err != ESP_OK) {
ESP_LOGE("app_task", "事件处理程序注册失败: %s", esp_err_to_name(err));
} else {
ESP_LOGI("app_task", "事件处理程序注册成功");
}
// 删除任务
ESP_LOGI("app_task", "删除App Task");
vTaskDelete(NULL);
}
// 主函数
void app_main(void) {
// 创建应用任务
xTaskCreate(app_task, "app_task", 2048, NULL, 5, NULL);
}
我们定义了一个事件处理程序 run_on_event
和一个任务 app_task
,该任务中使用 esp_event_handler_register
注册和处理各种系统和应用程序事件,从而实现事件驱动的编程模型,在事件处理程序 run_on_event
中接收所有事件并打印事件的基类型 (base) 和事件ID (id)。简单来说app_task任务中的esp_event_handler_register
指的是要听Event task任务中的信息(Event task就像一个传话筒,作任务间信息传递的媒介),下面是各参数的含义
-
ESP_EVENT_ANY_BASE 表示我要听WIFI_Event(注册处理所有事件基类型的事件),
-
ESP_EVENT_ANY_ID:表示注册处理所有事件ID的事件。
-
run_on_event:这是事件处理程序函数,当注册的事件发生时,将调用该函数。
-
NULL:这是传递给事件处理程序的参数,在本例中未使用。
esp_event_handler_register
函数,代码将 run_on_event
注册为一个通用事件处理程序,使其能够处理所有类型和所有ID的事件。这意味着,无论何种事件发生,run_on_event
都会被调用并处理该事件。
🚨需要注意的事情是指重要的事件,比如Wi-Fi成功连接到接入点。当引用事件时,应该使用由两部分组成的标识符。事件循环是连接事件和事件处理程序之间的桥梁,事件源可以通过使用事件循环库提供的API将事件发布到事件循环中 而esp_event_handler_register
函数就是默认事件循环 API 。注册到事件循环的事件处理程序会对特定类型的事件做出响应。
可能说到这里还是很晕,我们结合流程图来看,刚刚我们在App task任务中使用esp_event_handler_register
,相当于告诉Event task(Event loop),我要听谁的信息,哪些信息,我们要听的是Wi-Fi task(Wi-Fi_Event或者叫base)的所有信息,而在启动阶段我们可以听到下图1️⃣这个信息,而WIFI_EVENT_STA_START 这个信息会传给run_on_event 这个函数,用int32_t id
这个参数来接收。那么在上面代码run_on_event 中 ESP_LOGE("event_handle", "事件处理程序 base: %s, id: %d", base, id);
这行代码就会打印WIFI_EVENT_STA_START 所对应的ID
🚨注意在事件处理程序中尽量避免执行大量程序,只做简短的信息操作处理。
WIFI的初始化,配置,启动之后,就要开始进行WIFI扫描流程如下
扫描操作会在WIFI_task任务中完成,完成后会在Event_task发送已经完成的信息,我们修改程序如下,在esp_event_handler_register
捕捉到对应信息时,进行对应的处理,例如我们在捕捉到WIFI_EVENT_STA_START 信息时表明WIFI已经启动,此时进行WIFI的扫描操作,在捕捉到WIFI_EVENT_SCAN_DONE信息时表明扫描完成,此时进行AP的打印操作。
cpp
/**
* @brief WIFI 扫描
*
*/
void wifi_scan_task(void* pt){
ESP_LOGI("WIFI", "4. WIFI 扫描");
//在所有信道中扫描全部 AP(前端)
wifi_country_t country_config = {
.cc = "CN",
.schan = 1,
.nchan = 13,
};
esp_wifi_set_country(&country_config); // 4.1 扫描配置国家代码
wifi_scan_config_t scan_config = {
.show_hidden = true // 显示隐藏
};
esp_wifi_scan_start(&scan_config,true); // 4.2 配置扫描信息 true表示当这个任务执行的时候,回进入阻塞状态等待扫描
vTaskDelete(NULL);
}
/**
* @brief 显示扫描的ap信息
*
* @param pd
*/
void wifi_show_task(void* pd){
//得到扫描的AP数量
uint16_t ap_num =0;
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_num));
ESP_LOGI("WIFI","AP Count : %d",ap_num);
//获取具体的AP信息
uint16_t max_aps=20;
wifi_ap_record_t ap_records[max_aps];
memset(ap_records,0,sizeof(ap_records));
uint16_t aps_count =max_aps;
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&aps_count,ap_records));
//打印信息
ESP_LOGI("WIFI","AP Count: %d",aps_count);
printf("%30s %3s %3s %3s\n","SSID","频道","强度","MAC地址");
for(int i=0;i<aps_count;i++){
printf("%30s %4d %4d %02X-%02X-%02X-%02X-%02X-%02X\n",
ap_records[i].ssid,
ap_records[i].primary,
ap_records[i].rssi,
ap_records[i].bssid[0],
ap_records[i].bssid[1],
ap_records[i].bssid[2],
ap_records[i].bssid[3],
ap_records[i].bssid[4],
ap_records[i].bssid[5]);
}
vTaskDelete(NULL);
}
// 定义事件处理程序
void run_on_event(void* handler_arg, esp_event_base_t base, int32_t id, void* event_data)
{
// 事件处理程序逻辑
//ESP_LOGE("event_handle","事件处理程序 base: %s , id: %d",base,id);
switch(id){
case WIFI_EVENT_STA_START:
ESP_LOGE("EVENT_HANDLE","WIFI_EVENT_STA_START");
xTaskCreate(wifi_scan_task,"WIFI_scan Task",1024*12,NULL,1,NULL);
break;
case WIFI_EVENT_SCAN_DONE:
ESP_LOGE("EVENT_HANDLE","WIFI_EVENT_SCAN_DONE");
xTaskCreate(wifi_show_task,"WIFI_show Task",1024*12,NULL,1,NULL);
default:
}
}
/**
* @brief 用户自定义app_task任务用于捕捉wifi_task的消息
*
* @param pt
*/
void app_task(void* pt){
ESP_LOGI("app_task","App Task创建成功");
esp_event_handler_register(ESP_EVENT_ANY_BASE,ESP_EVENT_ANY_ID,run_on_event,NULL);
vTaskDelete(NULL);
}
总结
参考资料
ESP-IDF 编程指南 事件循环库
ESP-IDF 编程指南 Wi-Fi 驱动程序
FreeRTOS 任务 - 乐鑫 ESP32 物联网开发框架 ESP-IDF 开发入门 - 孤独的二进制出品