FreeRTOS——任务信息查询API

在基于 FreeRTOS 开发嵌入式项目时,随着业务逻辑变复杂,系统内会同时运行多个任务。我们经常会遇到这些问题:任务有没有正常创建?当前处于运行、阻塞还是就绪状态?任务栈空间是否充足、有没有栈溢出风险?各个任务的优先级配置是否符合预期?

如果单纯依靠 LED 翻转、串口打印简单信息来判断,不仅效率低,还很难定位隐性问题。其实 FreeRTOS 官方提供了一整套任务信息查询 API,可以一站式获取系统内所有任务的名称、优先级、运行状态、栈剩余空间、任务编号等核心数据,是 RTOS 开发中不可或缺的调试工具。

本文结合实际工程代码,从零搭建任务查询测试工程,详细讲解每一个查询 API 的用法、返回含义、使用场景以及配置注意事项,代码经过真机测试可直接运行,新手也能跟着上手。

一、整体工程架构说明

本次测试工程基于 STM32 + FreeRTOS 搭建,整体采用 FreeRTOS 经典的「开始任务」架构,分工清晰:

  1. 主函数:完成底层硬件初始化、中断分组配置,创建启动任务并开启任务调度器;
  2. 开始任务:统一创建所有业务任务与查询任务,任务创建完成后自行删除,仅承担 "启动" 作用;
  3. 普通业务任务 task1_task :基础测试任务,通过 vTaskDelay 主动进入阻塞态,用来验证任务状态查询功能;
  4. 查询任务 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_taskquery_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));
  1. uxTaskGetNumberOfTasks:统计系统内当前所有有效任务总数,包含系统自带的 IDLE 空闲任务、定时器服务任务;
  2. 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 字节);
  • 解读规则:
    1. 返回值越大:栈剩余空间越多,任务运行越安全;
    2. 返回值持续变小:说明任务局部变量、函数嵌套过多,栈在不断被占用;
    3. 返回值接近 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),会看到如下格式的输出:

逐行解读:

  1. TASK Pri = 3:查询任务自身优先级为 3,配置正常;
  2. 任务清单列表:展示系统所有任务名、基础优先级、编号,能直观看到所有已创建任务;
  3. 任务句柄:两种方式获取的句柄地址一致,句柄有效;
  4. StackMin is 142:查询任务栈最小剩余 142 字,栈空间充足;
  5. TaskState is 2:LED 任务处于阻塞态,是 vTaskDelay 导致,运行正常;
  6. 官方 vTaskList 列表:整合状态、优先级、栈空间,信息最全,适合长期调试使用。

六、工程使用注意事项总结

  1. 宏配置优先 :使用任务查询类 API 前,务必在 FreeRTOSConfig.h 开启对应功能宏,否则函数功能失效;
  2. 内存管理pvPortMalloc 动态分配的内存,必须搭配 vPortFree 释放,杜绝内存泄漏;
  3. 栈水位判断:栈水位值建议保留 20 字以上余量,数值过低及时增大任务栈大小,避免栈溢出;
  4. 缓冲区大小vTaskList 使用的字符串缓冲区,根据任务数量合理设置,防止缓冲区溢出;
  5. 任务状态理解vTaskDelay、等待信号量 / 队列都会让任务进入阻塞态,这是正常现象,无需误认为任务卡死。

七、拓展与应用场景

这套任务查询代码可以直接作为通用调试模板应用在所有 FreeRTOS 项目中:

  1. 项目开发阶段:常驻查询任务,实时监控所有任务状态,快速定位任务卡死、优先级抢占异常;
  2. 性能优化阶段:通过栈水位数据,合理裁剪任务栈大小,节省 MCU 内存资源;
  3. 问题排查阶段:设备现场出现死机、功能异常时,通过串口打印任务列表,判断异常任务。

FreeRTOS 的任务查询 API 看似简单,却是 RTOS 开发调试的核心工具。熟练掌握这些函数,能帮我们从 "盲调" 变成 "可视化调试",大幅提升开发和排错效率。

相关推荐
踏着七彩祥云的小丑2 小时前
嵌入式测试学习第 24 天:串口通信详细流程、收发数据原理
单片机·嵌入式硬件
周周记笔记2 小时前
【元器件专题】MOS管内部结构
嵌入式硬件
周周记笔记2 小时前
【元器件专题】MOS管的设计应用
单片机·嵌入式硬件
一路往蓝-Anbo3 小时前
第九章:OTA 与 Flash 驱动 —— 如何用TDD验证固件升级逻辑的鲁棒性
stm32·单片机·嵌入式硬件·软件工程·tdd·ota·嵌入式测试驱动开发
zlinear数据采集卡3 小时前
电源纹波无处遁形!工业采集卡电源去耦与滤波电路深度解析
c语言·嵌入式硬件·fpga开发·自动化·硬件架构
一路往蓝-Anbo3 小时前
第十章:TDD部署 —— Ceedling 环境的深度集成
stm32·单片机·嵌入式硬件·单元测试·测试驱动开发·tdd
QiLinkOS3 小时前
合肥气链科技有限公司创办与未来技术应用
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
一只肥瘫瘫4 小时前
STM32 程序升级学习笔记:Bootloader、IAP 与串口升级流程
笔记·stm32·学习
国科安芯4 小时前
ASM232S电气特性与TIA/EIA-232-F及ITU V.28标准符合性深度分析
单片机·嵌入式硬件·算法·安全·架构