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中修改打开下面👇设置
在这里插入图片描述
然后运行以下程序

/**
 * @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任务列表用表格表示的结果:

任务名称状态优先级剩余堆栈空间 (字节)任务编号功能描述
mainX120364应用主任务,通常用于启动其他任务和初始化系统。
IDLE1R08126空闲任务之一,通常在系统空闲时执行,一般进行喂狗。
IDLE0R010085空闲任务之二,通常在系统空闲时执行,一般进行喂狗。
esp_timerB2233523定时器服务任务,处理系统定时器事件。
ipc1S245242进程间通信任务之一,用于处理高优先级的IPC操作。
Tmr SvcB113207定时器服务任务,管理应用程序定时器回调函数的执行。
ipc0S245121进程间通信任务之二,用于处理高优先级的IPC操作。

各个字段含义:

  • 任务名称 (Task): 任务的名称。
  • 状态 (State): 任务的状态。
    • X: 未知状态
    • R: 运行中 (Running)
    • B: 阻塞中 (Blocked)
    • S: 挂起 (Suspended)
  • 优先级 (Prio): 任务的优先级,数值越大优先级越高。
  • 剩余堆栈空间 (Stack): 任务剩余的堆栈空间大小,以字节为单位。
  • 任务编号 (Num): 任务的编号。

带有WIFI功能时的任务

我们将WIFI扫描的功能加入到app_main

#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扫描后任务列表中增加了以下任务:

  1. tiT: 优先级18,状态为阻塞 (B),堆栈空间为2436字节,任务编号为8。
  2. sys_evt: 优先级20,状态为阻塞 (B),堆栈空间为1588字节,任务编号为9。
  3. wifi: 优先级23,状态为运行 ®,堆栈空间为4384字节,任务编号为10。

这些任务对应的就是,ESP-IDF编程文档中关于WIFI编程流程的图片,对应关系如下👇。

在这里插入图片描述
而对于App task任务在上图1.4步骤创建

创建App task方法如下

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任务

五个任务分别是什么

到现在五个任务都有了,它们分别执行的是什么功能呢

任务功能描述

  1. Main task::主任务通常负责系统初始化和启动其他任务。在Wi-Fi连接过程中的主要作用是初始化并启动Wi-Fi连

  2. App task:应用任务负责应用程序的主要逻辑处理,例如处理用户请求、数据处理等。在Wi-Fi连接过程中的主要作用是处理Wi-Fi连接状态的变化并相应地通知其他任务。

  3. Event task:事件任务负责处理系统和应用程序的事件,在Wi-Fi连接过程中主要负责处理Wi-Fi事件,作为中间媒介促使两个任务沟通,例如连接成功、断开连接、获取IP地址等。
    在这里插入图片描述

    例如,在启动阶段,“主任务”(Main task)启动了Wi-Fi的激活。一旦Wi-Fi任务成功启动,它会向"事件任务"(Event task)发送一条消息,指示WIFI_STA已启用。收到此信息后,事件任务会向应用程序任务(App task)提供反馈,通知它WIFI_STA已启用。

  4. LwIP task:LwIP任务负责TCP/IP协议栈的处理,包括网络数据包的发送和接收。在Wi-Fi连接过程中,LwIP任务负责网络数据的处理和传输。

  5. Wi-Fi task:Wi-Fi任务负责Wi-Fi的管理和控制,包括扫描可用网络、连接到指定网络、处理Wi-Fi事件等。

事件循环库

事件循环库使组件能够声明事件,允许其他组件注册处理程序(即在事件发生时执行的代码片段)。此时,无需直接涉及应用程序,松散耦合组件也能够在其他组件状态变化时附加所需的行为。此外,通过将代码执行序列化,在指定的任务中运行事件循环库,可以简化事件处理程序,实现更高效的事件处理。

那么如何在代码中捕获上图的1,2的信息呢

我们修改代码如下

#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_eventESP_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的打印操作。

/**
 * @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 开发入门 - 孤独的二进制出品

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐