在基于 FreeRTOS 开发嵌入式项目时,随着业务逻辑变复杂,系统内会同时运行多个任务。我们经常会遇到这些问题:任务有没有正常创建?当前处于运行、阻塞还是就绪状态?任务栈空间是否充足、有没有栈溢出风险?各个任务的优先级配置是否符合预期?
如果单纯依靠 LED 翻转、串口打印简单信息来判断,不仅效率低,还很难定位隐性问题。其实 FreeRTOS 官方提供了一整套任务信息查询 API,可以一站式获取系统内所有任务的名称、优先级、运行状态、栈剩余空间、任务编号等核心数据,是 RTOS 开发中不可或缺的调试工具。
本文结合实际工程代码,从零搭建任务查询测试工程,详细讲解每一个查询 API 的用法、返回含义、使用场景以及配置注意事项,代码经过真机测试可直接运行,新手也能跟着上手。
一、整体工程架构说明
本次测试工程基于 STM32 + FreeRTOS 搭建,整体采用 FreeRTOS 经典的「开始任务」架构,分工清晰:
- 主函数:完成底层硬件初始化、中断分组配置,创建启动任务并开启任务调度器;
- 开始任务:统一创建所有业务任务与查询任务,任务创建完成后自行删除,仅承担 "启动" 作用;
- 普通业务任务 task1_task :基础测试任务,通过
vTaskDelay主动进入阻塞态,用来验证任务状态查询功能; - 查询任务 query_task:本文核心任务,集中调用 FreeRTOS 各类任务查询 API,通过串口打印所有任务信息,实现系统状态监控。
工程依赖底层驱动:系统时钟、延时函数、串口、LED 驱动,是 STM32 标准工程配置,无需额外修改。
二、完整可运行源码
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"
//===================== 开始任务 配置 =====================
#define START_TASK_SIZE 128 // 任务栈大小
#define START_TASK_PRIO 1 // 任务优先级
TaskHandle_t StartTask_Hander; // 任务句柄
void start_task( void * pvParameters );
//===================== 业务任务1 配置(LED闪烁) =====================
#define TASK1_TASK_SIZE 50
#define TASK1_TASK_PRIO 2
TaskHandle_t TASK1Task_Hander;
void task1_task( void * pvParameters );
//===================== 任务查询任务 配置(核心调试任务) =====================
#define QUERY_TASK_SIZE 200
#define QUERY_TASK_PRIO 3
TaskHandle_t QUERYTask_Hander;
void query_task( void * pvParameters );
// 全局缓冲区:用于存放 vTaskList 格式化任务列表字符串
char Infobuffer[1000];
// 主函数:硬件初始化 + 创建启动任务 + 开启调度
int main(void)
{
// 设置中断优先级分组为组4
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
delay_init(); // 系统延时初始化
uart_init(115200); // 串口初始化,波特率115200
LED_Init(); // LED硬件初始化
// 创建开始任务
xTaskCreate( (TaskFunction_t) start_task,
(char *) "start_task",
(uint16_t) START_TASK_SIZE,
(void *) NULL,
(UBaseType_t) START_TASK_PRIO,
(TaskHandle_t *) &StartTask_Hander );
vTaskStartScheduler(); // 开启FreeRTOS任务调度器,永不返回
}
/**
* @brief 开始任务:统一创建所有子任务,完成后自删除
* @param pvParameters: 任务入口参数,此处未使用
* @retval 无
*/
void start_task( void * pvParameters )
{
// 创建LED闪烁任务
xTaskCreate( (TaskFunction_t) task1_task,
(char *) "task1_task",
(uint16_t) TASK1_TASK_SIZE,
(void *) NULL,
(UBaseType_t) TASK1_TASK_PRIO,
(TaskHandle_t *) &TASK1Task_Hander );
// 创建任务查询调试任务
xTaskCreate( (TaskFunction_t) query_task,
(char *) "query_task",
(uint16_t) QUERY_TASK_SIZE,
(void *) NULL,
(UBaseType_t) QUERY_TASK_PRIO,
(TaskHandle_t *) &QUERYTask_Hander );
// 开始任务使命完成,自我删除,释放系统资源
vTaskDelete(StartTask_Hander);
}
/**
* @brief 业务任务1:LED周期翻转,带延时(模拟阻塞态)
* @param pvParameters: 任务入口参数,此处未使用
* @retval 无
*/
void task1_task( void * pvParameters )
{
while(1)
{
LED0 = ~LED0; // LED电平翻转
vTaskDelay(1000); // 延时1000个系统节拍,任务进入阻塞态
}
}
/**
* @brief 任务查询核心任务:调用各类FreeRTOS查询API,串口打印任务信息
* @param pvParameters: 任务入口参数,此处未使用
* @retval 无
*/
void query_task( void * pvParameters )
{
UBaseType_t Priority;
TaskStatus_t *TaskStatusArray;
UBaseType_t ArraySize,i;
uint32_t TotalRunTime;
TaskHandle_t TaskHandle;
UBaseType_t StackMin;
eTaskState TaskState;
// 1. 获取当前查询任务自身的优先级
Priority = uxTaskPriorityGet(QUERYTask_Hander);
printf("TASK Pri = %lu \r\n",Priority);
// 2. 获取系统当前所有任务总数
ArraySize = uxTaskGetNumberOfTasks();
// 根据任务总数,动态申请内存,用于存放所有任务状态结构体
TaskStatusArray = pvPortMalloc(ArraySize * sizeof(TaskStatus_t));
if(TaskStatusArray != NULL)
{
// 3. 获取系统内全部任务的详细状态信息
ArraySize = uxTaskGetSystemState(TaskStatusArray, ArraySize, &TotalRunTime);
// 打印表头,格式化对齐
printf("%-20s %-10s %-10s\r\n", "TaskName", "TaskPri", "Number");
printf("----------------------------------------\r\n");
// 遍历所有任务,打印 任务名、基础优先级、任务编号
for(i = 0; i < ArraySize; i++)
{
printf("%-20s %-10lu %-10lu\r\n",
TaskStatusArray[i].pcTaskName,
TaskStatusArray[i].uxBasePriority,
TaskStatusArray[i].xTaskNumber);
}
}
// 释放动态申请的内存,避免内存泄漏
if(TaskStatusArray != NULL)
{
vPortFree(TaskStatusArray);
}
// 4. 根据任务名获取任务句柄
TaskHandle = xTaskGetHandle("query_task");
printf("Task handle is %#x \r\n",TaskHandle);
printf("Task handle is %#x \r\n",QUERYTask_Hander);
// 5. 获取任务栈历史最小剩余空间(栈水位,检测栈溢出)
StackMin = uxTaskGetStackHighWaterMark(QUERYTask_Hander);
printf("StackMin is %lu \r\n",StackMin);
// 6. 获取指定任务(task1_task)的当前运行状态
TaskState = eTaskGetState(TASK1Task_Hander);
printf("TaskState is %d \r\n",TaskState);
// 7. 调用官方API:生成标准格式化任务列表
vTaskList(Infobuffer);
// 自定义表头,与官方列表格式对齐
printf("%-15s %-7s %-7s %-7s %-4s\r\n",
"TaskName", "State", "Pri", "Stack", "Num");
printf("----------------------------------------------------\r\n");
// 打印官方格式化任务信息
printf("%s \r\n",Infobuffer);
// 任务循环,常驻后台作为调试任务
while(1)
{
}
}
三、关键宏配置(必须开启,否则 API 失效)
FreeRTOS 为了裁剪代码体积,很多拓展功能 API 默认是关闭的,必须在 FreeRTOSConfig.h 中手动开启对应宏,否则查询函数会运行异常、返回错误值。请确保工程内存在以下配置:
// 允许获取任务状态 eTaskGetState
#define INCLUDE_eTaskGetState 1
// 允许获取任务栈水位 uxTaskGetStackHighWaterMark
#define INCLUDE_uxTaskGetStackHighWaterMark 1
// 开启追踪功能,支持任务状态、列表统计
#define configUSE_TRACE_FACILITY 1
// 开启格式化统计函数,支持 vTaskList
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
如果缺少以上宏,栈查询、任务状态查询、vTaskList 都会无法正常使用。
四、代码分段解析 & API 详解
下面按照代码执行顺序,逐一讲解每一部分代码、API 功能、返回值含义以及实际使用场景。
4.1 任务宏与句柄定义
#define START_TASK_SIZE 128
#define START_TASK_PRIO 1
TaskHandle_t StartTask_Hander;
每一个 FreeRTOS 任务都需要定义栈大小、优先级、任务句柄:
- 栈大小:单位为
字(STM32 下 1 字 = 4 字节),根据任务逻辑复杂度设置,简单任务 50~128 即可; - 优先级:数值越大优先级越高,IDLE 空闲任务优先级固定为 0;
- 任务句柄:任务的唯一标识,后续所有针对任务的操作(查询、挂起、删除)都需要依靠句柄。
全局数组 char Infobuffer[1000] 作为缓冲区,存放 vTaskList 生成的字符串,缓冲区大小建议不低于 500 字节,任务数量多时可适当加大。
4.2 主函数与任务创建流程
主函数遵循 STM32 + FreeRTOS 标准初始化流程:先配置底层硬件、中断分组,再调用 xTaskCreate 创建开始任务,最后调用 vTaskStartScheduler 开启调度器。
调度器开启后,系统会按照优先级自动调度所有任务,主函数不再执行后续代码。
开始任务 start_task 作为 "过渡任务",统一创建 task1_task 和 query_task,创建完成后调用 vTaskDelete 自我删除。这种写法是工程常用规范,避免在主函数中堆砌大量任务创建代码,结构更清晰。
4.3 普通业务任务 task1_task
void task1_task( void * pvParameters )
{
while(1)
{
LED0 = ~LED0;
vTaskDelay(1000);
}
}
这是一个典型的循环任务,核心作用是模拟常规业务逻辑。重点在于 vTaskDelay(1000): 当任务执行到延时函数时,会主动放弃 CPU 使用权,进入阻塞态;延时 1000 个系统节拍结束后,任务重新进入就绪态,等待调度器调度。 我们正是利用这个特性,来测试「任务状态查询 API」。
4.4 核心查询任务 query_task(重点解析)
该任务集中使用了 FreeRTOS 全部主流任务查询 API,下面逐个拆解:
4.4.1 uxTaskPriorityGet:获取任务优先级
Priority = uxTaskPriorityGet(QUERYTask_Hander);
printf("TASK Pri = %lu \r\n",Priority);
- 函数功能:根据传入的任务句柄,获取任务当前的运行优先级;
- 传入参数:目标任务句柄;
- 返回值:
UBaseType_t类型,即任务优先级数值; - 使用场景:调试时验证任务优先级配置是否和预期一致,排查优先级抢占异常问题。
4.4.2 uxTaskGetNumberOfTasks + pvPortMalloc:获取任务总数并分配内存
ArraySize = uxTaskGetNumberOfTasks();
TaskStatusArray = pvPortMalloc(ArraySize * sizeof(TaskStatus_t));
uxTaskGetNumberOfTasks:统计系统内当前所有有效任务总数,包含系统自带的 IDLE 空闲任务、定时器服务任务;pvPortMalloc:FreeRTOS 专用动态内存分配函数,等同于标准库malloc。 因为任务数量不固定,我们先统计总数,再动态申请对应大小的内存,用来存储所有任务的状态结构体TaskStatus_t,最大化利用内存。
注意:动态申请的内存使用完毕后,必须用
vPortFree释放,长期不释放会造成内存泄漏,最终导致系统崩溃。
4.4.3 uxTaskGetSystemState:批量获取所有任务详细信息
ArraySize = uxTaskGetSystemState(TaskStatusArray, ArraySize, &TotalRunTime);
这是 FreeRTOS 功能最强大的批量查询 API,一次性收集系统内所有任务的完整信息,存入 TaskStatus_t 结构体数组中。
TaskStatus_t 结构体包含的核心字段:
pcTaskName:任务名称;uxBasePriority:任务基础优先级(初始配置的优先级);xTaskNumber:系统分配的任务唯一编号;xHandle:任务句柄;- 其余字段还包含当前优先级、运行时长等,可根据需求扩展打印。
后续通过 for 循环遍历结构体数组,配合格式化 printf,整齐打印每一个任务的名称、优先级、编号。代码中使用 %-20s、%-10lu 格式控制符,实现串口输出对齐,阅读更直观。
4.4.4 xTaskGetHandle:根据任务名获取任务句柄
TaskHandle = xTaskGetHandle("query_task");
printf("Task handle is %#x \r\n",TaskHandle);
printf("Task handle is %#x \r\n",QUERYTask_Hander);
- 函数功能:通过任务名字符串反向查找对应的任务句柄;
- 补充说明:代码中同时打印了 "通过名称获取的句柄" 和 "手动定义的全局句柄",二者地址完全一致,验证句柄有效性;
- 使用场景:工程中如果不方便传递全局句柄,可以通过任务名来操作对应任务。
4.4.5 uxTaskGetStackHighWaterMark:栈水位查询(栈溢出检测核心)
StackMin = uxTaskGetStackHighWaterMark(QUERYTask_Hander);
printf("StackMin is %lu \r\n",StackMin);
这是嵌入式开发中排查栈溢出问题的神器。
- 函数功能:获取任务栈的历史最小剩余空间(栈水位);
- 单位:字(STM32 下 1 字 = 4 字节);
- 解读规则:
- 返回值越大:栈剩余空间越多,任务运行越安全;
- 返回值持续变小:说明任务局部变量、函数嵌套过多,栈在不断被占用;
- 返回值接近 0:栈即将溢出,会出现程序跑飞、死机、硬件异常等问题,必须调大任务栈大小。
4.4.6 eTaskGetState:查询任务运行状态
TaskState = eTaskGetState(TASK1Task_Hander);
printf("TaskState is %d \r\n",TaskState);
根据任务句柄,获取任务当前运行状态,返回值为枚举类型,对应关系如下:
0 = eRunning 运行态:当前任务正在占用CPU运行
1 = eReady 就绪态:任务已准备就绪,等待调度
2 = eBlocked 阻塞态:任务被延时、信号量、队列等阻塞(本文 task1 常态)
3 = eSuspended 挂起态:任务被手动挂起,不再参与调度
4 = eDeleted 已删除态:任务已被删除
本文中 task1_task 执行了 vTaskDelay,所以正常打印结果为 2(阻塞态),符合预期。
4.4.7 vTaskList:官方标准任务列表
vTaskList(Infobuffer);
printf("%-15s %-7s %-7s %-7s %-4s\r\n", "TaskName", "State", "Pri", "Stack", "Num");
printf("%s \r\n",Infobuffer);
vTaskList 是 FreeRTOS 官方封装好的一体化任务列表函数,会自动将所有任务的名称、状态缩写、优先级、剩余栈、任务编号拼接成字符串,存入传入的缓冲区。
- 状态缩写说明:R = 运行态、B = 阻塞态、S = 挂起态;
- 优势:无需手动解析结构体,一行代码即可生成标准调试列表,是项目调试最常用的方式。 代码中自定义了表头,和官方列表格式对齐,进一步提升可读性。
五、真机运行结果解读
将代码下载到开发板,打开串口助手(波特率 115200),会看到如下格式的输出:

逐行解读:
TASK Pri = 3:查询任务自身优先级为 3,配置正常;- 任务清单列表:展示系统所有任务名、基础优先级、编号,能直观看到所有已创建任务;
- 任务句柄:两种方式获取的句柄地址一致,句柄有效;
StackMin is 142:查询任务栈最小剩余 142 字,栈空间充足;TaskState is 2:LED 任务处于阻塞态,是vTaskDelay导致,运行正常;- 官方
vTaskList列表:整合状态、优先级、栈空间,信息最全,适合长期调试使用。
六、工程使用注意事项总结
- 宏配置优先 :使用任务查询类 API 前,务必在
FreeRTOSConfig.h开启对应功能宏,否则函数功能失效; - 内存管理 :
pvPortMalloc动态分配的内存,必须搭配vPortFree释放,杜绝内存泄漏; - 栈水位判断:栈水位值建议保留 20 字以上余量,数值过低及时增大任务栈大小,避免栈溢出;
- 缓冲区大小 :
vTaskList使用的字符串缓冲区,根据任务数量合理设置,防止缓冲区溢出; - 任务状态理解 :
vTaskDelay、等待信号量 / 队列都会让任务进入阻塞态,这是正常现象,无需误认为任务卡死。
七、拓展与应用场景
这套任务查询代码可以直接作为通用调试模板应用在所有 FreeRTOS 项目中:
- 项目开发阶段:常驻查询任务,实时监控所有任务状态,快速定位任务卡死、优先级抢占异常;
- 性能优化阶段:通过栈水位数据,合理裁剪任务栈大小,节省 MCU 内存资源;
- 问题排查阶段:设备现场出现死机、功能异常时,通过串口打印任务列表,判断异常任务。
FreeRTOS 的任务查询 API 看似简单,却是 RTOS 开发调试的核心工具。熟练掌握这些函数,能帮我们从 "盲调" 变成 "可视化调试",大幅提升开发和排错效率。