EPS32的FreeRTOS任务概念

一、为什么要用 FreeRTOS 任务

1.裸机开发的问题
场景:同时处理 LED 闪烁、OPEDS 编码 / 解码等多任务(含延时、复杂逻辑)
痛点:需手动实现时间片调度,逻辑复杂、易出错;无成熟框架支撑,系统规模扩大后难以维护
2.FreeRTOS 的优势
系统初始化后直接创建多个独立任务,启动调度器即可
任务调度器是成熟的 “脚手架”,无需手动处理任务切换
每个任务可独立死循环,但必须包含让出 CPU 的延时操作(否则独占 CPU 导致系统异常)
在这里插入图片描述

二、查看系统创建的已有任务

idf.py menuconfig
# 路径:Component config → FreeRTOS → 启用以下选项
# 1. Enable FreeRTOS task stats
# 2. Enable FreeRTOS task CPU core usage stats
# 按S保存,回车确认,Q退出

在这里插入图片描述

void print_all_tasks() {
    char task_list_buffer[1024]; // 缓冲区大小建议 >= 512 字节
    vTaskList(task_list_buffer); // 获取任务信息
    printf("Task List:\n%s\n", task_list_buffer);
} 
extern "C" void app_main(void)
{  
    while (true)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));    
        print_all_tasks();    
    }
}

打印结果
在这里插入图片描述

1、这里打印的信息有 6 列
第 1 列:任务名称
第 2 列:任务状态
X (Runing): 正在运行
R (Ready): 就绪
B (Block): 阻塞
S (Suspended): 挂起
第 3 列:任务优先级数字越小优先级越高
第 4 列:剩余栈大小高水位,这个数字 * sizeof (StackType_t) 代表高水位剩余字节数
第 5 列:任务创建序号
第 6 列:任务运行的核心编号 (前面配置的使能 xCoreID in vTaskList)
0: pro_cpu
1: app_cpu
-1: 没有绑定
2、各任务作用:
IDLE0/IDLE1 (空闲任务) 功能:
1、喂狗 (定时做)
2、清理内存碎片 (简易的 java GC 机制)
3、栈溢出检查
ipc0/ipc1 (核间通讯任务)
Tmr Svc (定时器任务): 负责定时器函数回调
main (main_task 任务): 调用了我们的入口函数 app_main 函数

三、获取当前任务的栈高水位

1.核心概念
高水位:任务运行过程中剩余栈空间最少的一次值(即栈使用最深的时刻)
作用:验证任务栈分配是否合理,剩余过少易导致栈溢出、程序崩溃
单位差异:ESP32 FreeRTOS 栈按字节统计,STM32 FreeRTOS 按 4 字节为单位(内部乘 4)
2.代码

              
extern "C" void app_main(void)
{  
    while (true)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));    
        TaskHandle_t main_task = xTaskGetCurrentTaskHandle();
        UBaseType_t stack_remaining = uxTaskGetStackHighWaterMark(main_task); 
        // vTaskResume(xTaskGetHandle("ipc0"));
        std::printf("app_main 栈剩余空间: %u 字节 \n",  
               stack_remaining * sizeof(StackType_t)); 
    }
}

3.修改 main task 栈大小
操作:idf.py menuconfig → Component config → ESP System Settings → Main task stack size
示例:默认栈大小改到 4096 字节后,剩余高水位会明显增加
在这里插入图片描述
在这里插入图片描述
之前
在这里插入图片描述
在这里插入图片描述

使用C++创建FreeRTOS任务

一、文件目录修改

1.app.cpp更名为main.cpp
在这里插入图片描述
2.创建app.cpp和app.h
在这里插入图片描述
3.创建work_task.cpp和work_task.h
在这里插入图片描述
4.改MakeLists.txt

# idf_component_register(SRCS "main.cpp"
#                     INCLUDE_DIRS ".")


set(SOURCES "main.cpp"
    "app.cpp"
    "work_task.cpp"
    "drivers/storage/sd_card.cpp" 
    "drivers/audio/audio_es7210.cpp" 
    "drivers/audio/wav_recorder.cpp" 
)

set(INCLUDE_DIRS "." 
            "drivers"
            "drivers/storage"
            "drivers/audio/"
)


idf_component_register(SRCS ${SOURCES} 
                INCLUDE_DIRS ${INCLUDE_DIRS} 
                )

二、实现单例模式的App

简单说,单例模式是一种编程设计思路,目的是让某个类在整个程序里只能创建出「唯一的一个实例」,不管你在哪调用,拿到的都是同一个对象。
为什么要用单例模式?
比如你这个App类是整个程序的核心控制层,如果允许创建多个App实例:
会重复初始化后台任务(比如多次new WorkTask,导致系统里出现多个重复的后台任务);
全局配置、资源会冲突(比如多个实例同时操作同一个硬件)。

app.h

#pragma once
#include <cstdio>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "work_task.h"
class App
{
private:
    App();
    ~App(); 
    WorkTask* work_task=nullptr;

public:
    static App& GetInstance() {
        static App instance;
        return instance;
    }
    // 删除拷贝构造函数和赋值运算符
    App(const App&) = delete;
    App& operator=(const App&) = delete; 
    void run();

    void print_all_tasks();
};

这段代码通过C++ 单例模式封装 ESP32 应用核心类App,核心目的是保证App全局仅一个实例,避免重复初始化资源:
私有化构造 / 析构函数、删除拷贝 / 赋值运算符,从语法上杜绝多实例创建;
提供GetInstance()静态方法获取唯一实例,实例化时初始化后台任务(WorkTask);
声明run()(应用主循环)和print_all_tasks()(打印 FreeRTOS 任务列表)接口,支撑应用核心逻辑运行与调试。

app.cpp



#include "app.h"



void App::print_all_tasks() {
    char task_list_buffer[1024]; // 缓冲区大小建议 >= 512 字节
    vTaskList(task_list_buffer); // 获取任务信息
    printf("Task List:\n%s\n", task_list_buffer);
} 


App::App(){
    work_task = new WorkTask(4096*2);


}

App::~App(){

}

void App::run(){ 

    while (true)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));    
        print_all_tasks();     
    }
}

work_task.h

#pragma once
#include <cstdio>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
class WorkTask
{
private:
    /* data */
public:
    WorkTask(uint32_t stack_size);
    ~WorkTask();

    void work_task_loop();

};

这段代码是 ESP32 FreeRTOS 后台任务的封装类WorkTask,核心作用是:在类的构造函数中自动创建一个 FreeRTOS 后台任务,任务内部运行死循环,每秒打印一次日志,专门处理耗时 / 后台逻辑

work_task.cpp

#include "work_task.h"

void WorkTask::work_task_loop()
{  
    while (1)
    { 
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        printf("work task loop\n");
    }
}

WorkTask::WorkTask(uint32_t stack_size)
{
    // FreeRTOS任务创建函数:创建一个独立的后台任务
    xTaskCreate(
        // 1. 任务执行函数:空捕获列表的Lambda表达式(适配FreeRTOS的C风格函数指针)
        [](void *pvParameters) {
            // 将void*类型的参数安全转换为WorkTask类指针(static_cast编译期类型检查)
            WorkTask *work_task = static_cast<WorkTask*>(pvParameters);
            // 调用类的成员方法,执行后台任务的核心业务逻辑
            work_task->work_task_loop();
            // 删除当前任务(实际不会执行,因为work_task_loop是死循环)
            // 若后续修改loop为可退出逻辑,此句可释放任务资源
            vTaskDelete(NULL); 
        },
        "work_task",        // 2. 任务名称:用于vTaskList打印任务列表时识别
        stack_size,         // 3. 任务栈大小:使用外部传入的数值(字节)
        this,               // 4. 任务参数:传递当前WorkTask实例的指针给任务函数
        3,                  // 5. 任务优先级:3(高于main任务的1,保证后台任务优先执行)
        NULL                // 6. 任务句柄:无需后续操作(删除/挂起/修改优先级),传NULL
    );
}

WorkTask::~WorkTask()
{
}

后台任务逻辑及代码实现

核心目标:把 CPU 密集型任务(如 OPUS 编码 / 解码)从主任务剥离到独立的后台任务中,避免耗时计算阻塞主任务(比如主任务的打印、硬件响应等),核心原因:
1.OPUS 编码是 “CPU 密集型” 操作(计算量大、耗时久);
2.若放在主任务中执行,会导致主任务卡顿、看门狗超时重启;
3.后台任务独立运行,专门处理这类耗时逻辑,主任务只需 “提交任务” 和 “接收结果”,无需等待计算完成。
在这里插入图片描述

一.代码改写

type_def.h

#pragma once

#include <list>
#include <functional>
#include <mutex>

using ListFunction =  std::list<std::function<void()>>;

using FuncVoid = std::function<void()>;

using MutexUniqueLock =  std::unique_lock<std::mutex>;


using  MutexLockGuard =  std::lock_guard<std::mutex>;

这个type_def.h是统一的类型别名文件,核心就 2 件事:
1.把后台任务中频繁用的、长串的 C++ 标准类型(如std::list<std::function<void()>>),简化成短别名(如ListFunction),减少代码书写量、提升可读性;
2.统一项目类型命名(比如锁、任务函数、任务队列的类型),适配 “生产者 - 消费者” 后台任务的逻辑(存函数、加锁执行)

work_task.h

#pragma once
#include <cstdio>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
class WorkTask
{
private:
    /* data */
public:
    WorkTask(uint32_t stack_size);
    ~WorkTask();

    void work_task_loop();

};

work_task.cpp

#include "work_task.h" 


void WorkTask::add_task(FuncVoid task)
{
    MutexLockGuard lock(mutex_);  
    task_count++; 
    task_list.emplace_back([call = std::move(task),this](){
        call();
        {
            MutexLockGuard lock(mutex_);
            task_count--;
            if (task_count==0&&task_list.empty())
            {
                condition_variable_.notify_all();
            } 
        }  
    }); 
    condition_variable_.notify_all();

}

void WorkTask::work_task_loop()
{   
    while (1)
    {  
        MutexUniqueLock lock(mutex_); 
        condition_variable_.wait(lock,[this](){return !task_list.empty();});

        ListFunction func_list = std::move(task_list);
        lock.unlock();
        for (auto& func : func_list)
        {
            func();
        } 
    } 
}

WorkTask::WorkTask(uint32_t stack_size)
{
    xTaskCreate([](void *pvParameters){
        WorkTask *work_task = static_cast<WorkTask*>(pvParameters);
        work_task->work_task_loop();
        vTaskDelete(NULL); 
    }, "work_task", stack_size, this, 3, NULL);
}

WorkTask::~WorkTask()
{
}

二.WorkTask 新旧版本核心差异对比

对比维度 旧版 WorkTask 新版 WorkTask
核心能力 仅固定执行 “每秒打印日志” 的硬编码逻辑 支持动态提交任意无参无返回的任务函数,通用化调度
核心机制 单纯死循环 + 延时 基于「队列 + 互斥锁 + 条件变量」的生产者 - 消费者模型
任务灵活性 逻辑写死,无法扩展 可动态添加 OPUS 编码 / 解码等任意耗时任务

1.新增add_task方法:生产者调用该方法提交任务(如 OPUS 编码函数),提交时加锁保证线程安全,同时更新任务计数、唤醒消费者;
2.任务队列(task_list):用链表存储待执行的任务函数,替代旧版固定逻辑;
3.互斥锁 + 条件变量:
互斥锁(mutex_):保证多线程提交 / 取任务时队列读写安全;
条件变量(condition_variable_):消费者无任务时阻塞等待,有任务时被唤醒,避免空循环浪费 CPU;
4.任务计数(task_count):跟踪未完成任务数,全部执行完且队列为空时触发通知;
5.任务执行逻辑:消费者取出队列所有任务后解锁(尽快释放锁),再遍历执行,兼顾效率与线程安全。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐