【AI小智硬件程序(五)】
AI小智硬件程序(五)
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.任务执行逻辑:消费者取出队列所有任务后解锁(尽快释放锁),再遍历执行,兼顾效率与线程安全。
更多推荐




所有评论(0)