任务实用程序

[English]

在 FreeRTOS 中,任务实用程序用于获取任务状态、任务句柄、堆栈使用情况等信息,辅助进行任务调试与系统监测。

本文档仅涵盖任务实用程序中部分常用 API,更多内容请参考 FreeRTOS 官方文档

任务信息 API

任务信息类 API 主要用于获取任务的基本标识信息,如当前任务句柄或任务名称,便于在调试或日志中定位任务来源。

xTaskGetCurrentTaskHandle()

xTaskGetCurrentTaskHandle() 用于获取当前正在运行的任务的句柄,常用于需要引用自身任务的场景,例如任务自挂起、自删除等操作。

API 原型

TaskHandle_t xTaskGetCurrentTaskHandle( void );
返回值说明

返回值

说明

返回当前正在运行(即调用该 API)的任务的句柄

返回值在任务上下文中有效。

示例 1:获取自身任务句柄并修改优先级

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 任务 1 函数
void vTask1 (void *pvParameters)
{
    // 获取当前任务句柄
    TaskHandle_t xHandleTask1 = xTaskGetCurrentTaskHandle();

    while (1) {
        // 获取任务优先级
        UBaseType_t xPriorityTask1 = uxTaskPriorityGet(xHandleTask1);

        printf("Priority of Task 1 is %d\n", xPriorityTask1);

        // 提升任务 1 的优先级(如果小于任务 2)
        if (xPriorityTask1 < 2) {
            printf("Raising Task 1 priority to %d\n", xPriorityTask1+1);
            vTaskPrioritySet(xHandleTask1, xPriorityTask1+1);
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void)
{
    xTaskCreate(vTask1, "Task1", 2048, NULL, 1, NULL);
}

以上代码执行结果如下:

Priority of Task 1 is 1
Raising Task 1 priority to 2
Priority of Task 1 is 2
Priority of Task 1 is 2
Priority of Task 1 is 2
Priority of Task 1 is 2

本示例展示了获取自身任务句柄的基本方法及其在任务自管理中的应用。

任务启动后通过 xTaskGetCurrentTaskHandle() 获取自身句柄,并结合 uxTaskPriorityGet()vTaskPrioritySet() 实现对自身优先级的动态调整。

相较于直接传入 NULL 表示当前任务,使用任务句柄具备更强的通用性和灵活性,既可配合如 pcTaskGetName()eTaskGetState() 等仅接受句柄的 API 使用,也便于调试信息输出及跨模块传递任务控制信息。

示例 2:在任务内部向外传出自身句柄

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 定义全局变量保存任务句柄
TaskHandle_t xHandleTask = NULL;

// 任务 1 函数
void vTask1 (void *pvParameters)
{
    while (1) {
        if (xHandleTask != NULL){
            vTaskSuspend(xHandleTask);
            printf("Task 2 is suspended\n");
            xHandleTask = NULL;
        }
        else {
            printf("Task 1 is running\n");
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void vTask2 (void *pvParameters)
{
    // 获取当前任务句柄
    xHandleTask = xTaskGetCurrentTaskHandle();

    while (1) {
        printf("Task 2 is running\n");
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void)
{
    xTaskCreate(vTask1, "Task1", 2048, NULL, 1, NULL);
    xTaskCreate(vTask2, "Task2", 2048, NULL, 1, NULL);
}

以上代码执行结果如下:

Task 1 is running
Task 2 is running
Task 2 is suspended
Task 1 is running
Task 1 is running
Task 1 is running
Task 1 is running

本示例演示了任务在运行过程中通过 xTaskGetCurrentTaskHandle() 主动获取自身任务句柄,并将其保存至全局变量,供其他任务使用,实现了任务间的直接控制。

相比在任务创建时由外部传入并保存句柄的方式,该方法由任务自身管理句柄,因而具有更高的解耦性。 此方法特别适用于任务内部初始化、自注册或启动后动态绑定的场景,有助于构建结构清晰、灵活可控的任务系统。

备注

解耦性是指模块或组件之间相互依赖程度低,彼此可以独立开发、使用和修改,不会因为一个模块的变化而影响另一个模块的运行。

pcTaskGetName()

pcTaskGetName() 用于获取指定任务的名称字符串,常用于调试、日志输出或任务识别。

API 原型

char * pcTaskGetName( TaskHandle_t xTaskToQuery );
参数解释

传入参数

参数功能

参数说明

xTaskToQuery

需要查询名称的任务句柄。

若传入非空句柄,查询对应任务的名称;若传入 NULL,则查询当前任务自身的名称。

返回值说明

返回值

说明

任务名称字符串指针

返回所查询任务名称的指针,是一个以 NULL 结尾的标准字符串。

备注

该返回值指向的字符串是静态分配的,不应在用户代码中修改或释放。可以直接传给标准字符串函数使用,适用于打印日志、调试任务状态等场景。

示例 1:查询任务名称并打印

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 定义全局变量保存任务句柄
TaskHandle_t xHandleTask = NULL;

void vTask1 (void *pvParameters)
{
    // 获取任务名字
    const char *NameTask2 = NULL;
    const char *NameTask1 = pcTaskGetName(NULL);

    while (1) {
        if (xHandleTask != NULL){
            NameTask2 = pcTaskGetName(xHandleTask);
            printf("Task 2 name: %s\n", NameTask2);
        }
        printf("Task 1 name: %s\n", NameTask1);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void vTask2 (void *pvParameters)
{
    // 获取当前任务句柄
    xHandleTask = xTaskGetCurrentTaskHandle();

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void)
{
    xTaskCreate(vTask1, "Task1", 2048, NULL, 1, NULL);
    xTaskCreate(vTask2, "Task2", 2048, NULL, 1, NULL);
}

以上代码执行结果如下:

Task 1 name: Task1
Task 2 name: Task2
Task 1 name: Task1
Task 2 name: Task2
Task 1 name: Task1
Task 2 name: Task2

本示例展示了如何使用 pcTaskGetName() 获取指定任务的名称,并在运行时周期性打印。该示例通过传入 NULL 获取当前任务的名称,同时也传入其他任务的有效句柄以查询对应任务的名称。

尽管任务名称通常由开发者在创建任务时指定,但在跨模块访问、日志输出、动态任务创建,或与其他仅接受任务句柄的 API ,例如 eTaskGetState() 联合使用时,pcTaskGetName() 仍具备实际价值。 它提供了一种统一的信息获取方式,避免手动维护任务名字符串,提高了代码的解耦性和可维护性。

状态与调度 API

状态与调度类 API 用于查询任务当前状态或系统调度器的运行状态,便于判断任务行为和系统调度情况。

eTaskGetState()

eTaskGetState() 用于获取指定任务的当前状态,如运行、就绪、阻塞、挂起或已删除,常用于任务状态监测与调试。

API 原型

eTaskState eTaskGetState( TaskHandle_t xTask );
参数解释

传入参数

参数功能

参数说明

xTask

需要获取状态的任务句柄。

若传入非空句柄,查询对应任务的名称;若传入 NULL,则查询当前任务自身的名称。

返回值说明

返回值

对应状态

状态说明

eReady

就绪态

任务已就绪,等待被调度执行。

eRunning

运行态

任务当前正在执行(仅适用于当前任务)。

eBlocked

阻塞态

任务正在等待某事件或超时。

eSuspended

挂起态

任务被挂起,不会被调度。

eDeleted

删除态

任务已被删除,等待资源释放。

备注

返回值为 eTaskState 类型的枚举,返回任务当前的状态。

示例 1:获取任务状态

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

TaskHandle_t task1Handle = NULL;

void vtask1(void *pvParameters) {
    while (1) {
        printf("Task 1 is running\n");
        vTaskDelay(pdMS_TO_TICKS(2000)); // 进入阻塞态
    }
}

void vtask2(void *pvParameters) {
    xTaskCreate(vtask1, "Task1", 2048, NULL, 2, &task1Handle);

    eTaskState state;
    int counter = 0;

    while (1) {
        if (eTaskGetState(xTaskGetCurrentTaskHandle()) == eRunning) {
            printf("Task 2 state: RUNNING\n");
        }

        counter++;

        if (task1Handle != NULL) {
            state = eTaskGetState(task1Handle);

            switch (state) {
                case eRunning:   printf("Task 1 state: RUNNING\n"); break;
                case eReady:     printf("Task 1 state: READY\n"); break;
                case eBlocked:   printf("Task 1 state: BLOCKED\n"); break;
                case eSuspended: printf("Task 1 state: SUSPENDED\n"); break;
                case eDeleted:   printf("Task 1 state: DELETED\n"); break;
                default:         printf("Task 1 state: UNKNOWN\n"); break;
            }

            // 控制 Task 1 状态变化
            if (counter == 2) {
                vTaskSuspend(task1Handle); // Task 1 挂起
                printf("Task 1 suspended\n");
            }

            if (counter == 4) {
                vTaskDelete(task1Handle); // Task 1 删除
                printf("Task 1 deleted\n");
            }
        }

        vTaskDelay(pdMS_TO_TICKS(1000)); // 主任务周期运行
    }
}

void app_main() {
    xTaskCreate(vtask2, "Task2", 2048, NULL, 3, NULL); // 优先级较高,便于获取 Task 1 状态
}

以上代码执行结果如下:

Task 2 state: RUNNING
Task 1 is running
Task 1 state: READY
Task 2 state: RUNNING
Task 1 state: BLOCKED
Task 1 suspended
Task 2 state: RUNNING
Task 1 state: SUSPENDED
Task 2 state: RUNNING
Task 1 state: SUSPENDED
Task 1 deleted
Task 2 state: RUNNING
Task 1 state: DELETED

本示例演示了使用 eTaskGetState() 获取并观察其他任务的状态变化过程。

系统中创建了两个任务,其中 Task 1 优先级较低,运行中打印信息并定期延时进入阻塞态;主控任务 Task 2 优先级较高,在启动后立即创建 Task 1,并周期性执行以下操作:

  • 打印自身状态(验证当前任务为运行态)

  • 获取 Task 1 状态并通过 switch 打印描述

  • 在特定周期对 Task 1 执行挂起和删除操作,模拟状态切换

  • 每次循环延时 1 秒

根据示例输出,任务 Task 1 和 Task 2 的状态和执行如下:

循环序号

执行结果

Task 1

Task 2

1

Task 2 state: RUNNING

Task 1 已被创建,但尚未运行。

Task 2 正在执行中,通过 eTaskGetState(xTaskGetCurrentTaskHandle()) 判断当前状态为 eRunning,并打印。

1

Task 1 is running

Task 1 开始执行 printf(),此时其状态为 eRunning

Task 2 在执行 printf() 后让出 CPU,调度器将执行权切换至 Task 1。

1

Task 1 state: READY

Task 1 可能已执行完 printf(),但尚未进入 vTaskDelay(),此时其状态短暂处于 eReady (等待再次被调度)。也可能是 Task 1 已进入阻塞态,但任务控制块尚未更新,导致读取到 eReady

调度器再次切换至 Task 2,Task 2 使用 eTaskGetState(task1Handle) 查询 Task 1 状态。

2

Task 2 state: RUNNING

Task 1 处于阻塞态。

再次通过 eTaskGetState() 判断当前任务状态,确认 Task 2 正在运行。

2

Task 1 state: BLOCKED

Task 1 已执行完 printf() 并调用 vTaskDelay(),进入阻塞态,调度器不会分配 CPU 给它。

Task 2 查询 Task 1 状态并打印为 eBlocked

2

Task 1 suspended

Task 1 被挂起,任务进入 eSuspended 状态。

此时 counter == 2,Task 2 调用 vTaskSuspend() 将 Task 1 挂起。

3

Task 2 state: RUNNING

Task 1 处于挂起态。

再次通过 eTaskGetState() 判断当前任务状态,确认 Task 2 正在运行。

3

Task 1 state: SUSPENDED

Task 1 处于挂起态。

Task 2 查询 Task 1 状态,确认其处于挂起状态。

4

Task 2 state: RUNNING

Task 1 处于挂起态。

再次通过 eTaskGetState() 判断当前任务状态,确认 Task 2 正在运行。

4

Task 1 state: SUSPENDED

Task 1 处于挂起态。

Task 2 查询 Task 1 状态,确认其处于挂起状态。

4

Task 1 deleted

删除操作立即生效,Task 1 被标记为删除。

此时 counter == 4,Task 2 调用 vTaskDelete() 删除 Task 1。

5

Task 2 state: RUNNING

Task 1 处于删除态。

再次通过 eTaskGetState() 判断当前任务状态,确认 Task 2 正在运行。

5

Task 1 state: DELETED

Task 1 处于删除态。

查询 Task 1 状态,系统返回 eDeleted,表示任务已被删除,不会再被调度。

任务状态的获取虽然提供了任务调试的有效方式,但由于状态切换过程非常迅速且依赖调度器行为,使用时需注意以下事项:

  • 任务创建后立即处于就绪态,但实际是否立刻运行取决于调度器调度决策(包括优先级和当前任务是否让出 CPU)。

  • 阻塞态的常见触发源包括:调用 vTaskDelay()、等待信号量、队列或事件组。

  • 任务挂起后将不再被调度器调度,除非显式调用 vTaskResume() 恢复。

  • 被删除的任务状态为 eDeleted,但该状态仅在任务删除后短暂保留,之后其任务句柄可能变为无效,访问将导致不可预期行为。为避免访问无效句柄导致不可预期行为,建议在删除任务后将对应任务句柄置为 NULL

  • printf() 是阻塞操作,可能引起任务切换,影响状态观测的时序。

  • 任务状态(特别是 eReadyeBlocked)切换极其短暂,在实际观测中容易遇到边界状态,可能造成误读。

  • 状态信息由调度器维护并在适当时机更新,因此通过 eTaskGetState() 获取的状态不能完全等同于任务的瞬时执行情况,仅作参考。

理解任务状态机制与其局限性,有助于在调试和任务调度设计中做出合理判断,避免对状态值的误用。

xTaskGetSchedulerState()

xTaskGetSchedulerState() 用于获取调度器当前的状态,判断调度器是运行中、已挂起还是未启动,帮助了解系统调度是否正常。

API 原型

BaseType_t xTaskGetSchedulerState( void );
返回值说明

返回值

说明

taskSCHEDULER_NOT_STARTED

调度器还未启动,此时所有任务都不会被调度执行,即使任务已经创建。

taskSCHEDULER_RUNNING

调度器已启动并正常运行,任务按优先级被调度。

taskSCHEDULER_SUSPENDED

调度器被挂起,任务调度暂时停止,所有任务停止切换,但任务状态不变,等待调度器恢复后继续调度。

示例 1:打印调度器状态

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"

void exampleTask(void *param)
{
    int counter = 0;
    bool print_resume = false;
    BaseType_t state;
    while (1) {
        if (counter == 2) {
            printf(">>> Suspended scheduler from task\n");

            vTaskSuspendAll();

            // 安静忙等待 3 秒,无法进行 printf 或 delay
            int64_t start = esp_timer_get_time();
            while (esp_timer_get_time() - start < 3000000) {}
            state = xTaskGetSchedulerState(); // 保存当前调度器状态

            xTaskResumeAll();  // 恢复调度器
            print_resume = true;
        }

        if (print_resume) {
            if (state == taskSCHEDULER_SUSPENDED) {
                printf("Scheduler state: SUSPENDED\n");
            }
            printf(">>> Resumed scheduler from task\n");
            print_resume = false;
        }

        state = xTaskGetSchedulerState();
        if (state == taskSCHEDULER_RUNNING) {
            printf("Scheduler state: RUNNING\n");
        }

        counter++;
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    xTaskCreate(exampleTask, "exampleTask", 2048, NULL, 5, NULL);
}

以上代码执行结果如下:

Scheduler state: RUNNING
Scheduler state: RUNNING
>>> Suspended scheduler from task
Scheduler state: SUSPENDED
>>> Resumed scheduler from task
Scheduler state: RUNNING
Scheduler state: RUNNING

本示例演示了如何使用 xTaskGetSchedulerState() 在任务中打印调度器状态,并在运行过程中挂起与恢复调度器的过程。 任务周期性输出调度器当前状态,展示了调度器挂起后仍然允许当前任务继续执行,以及通过基于 esp_timer_get_time() 的忙等待模拟挂起期间延时的方式。 恢复调度器后,任务继续正常打印运行状态。

需要特别注意的是:

  • 调度器挂起仅阻止任务切换,不会暂停当前任务执行。

  • 调度器挂起期间不能调用任何阻塞函数,如 vTaskDelay() 或内部使用互斥锁的 printf(),否则会导致系统断言失败。

  • ESP-IDF 环境中,调度器启动由系统自动完成,无法观察到 taskSCHEDULER_NOT_STARTED 状态。

挂起和恢复调度器的操作必须成对使用,以保证系统的稳定运行。

其中 vTaskSuspendAll() 用于暂时挂起 FreeRTOS 的任务调度器,防止任务切换;xTaskResumeAll() 则恢复任务调度,允许任务切换继续进行。 二者配合使用,可实现一段代码中临时禁止任务切换,保证执行的连续性和一致性。

备注

调用 xTaskResumeAll() 仅恢复通过 vTaskSuspendAll() 挂起的调度器,但不会恢复通过 vTaskSuspend() 挂起的任务。

在原生 FreeRTOS 环境下,vTaskSuspendAll() 只暂停任务调度器,不会禁用中断,因此中断仍可正常执行,适合临界区较大且允许中断响应的场景。 调用 xTaskResumeAll() 后,调度器恢复运行,并可能触发一次任务切换。

在 ESP-IDF 环境下,两个函数的原理相似,但实现更复杂,调度暂停一般只影响调用核本身,不会暂停其他核心的调度。 由于多核特点,保护共享资源时通常还需要结合硬件特性和多核同步机制(例如自旋锁)灵活应用。

有关ESP-IDF 环境下调度器挂起和恢复的更进一步的相关信息请查阅 ESP-IDF 官方文档中关于 FreeRTOS 调度器挂起 的章节。

运行时间 API

运行时间类 API 用于获取系统运行的时间信息和任务的运行时间统计,辅助性能分析与系统监控。

xTaskGetTickCount()

xTaskGetTickCount() 用于获取系统自启动以来的节拍计数(tick 数),常用于实现延时或计算时间间隔。

API 原型

TickType_t xTaskGetTickCount( void );
返回值说明

返回值

说明

返回系统节拍数(ticks)

xTaskGetTickCount() 被调用,即调度器启动,以来经过的系统节拍数(ticks)。

示例 1:循环读取当前 tick

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 任务函数
void vDelayTask (void *pvParameters)
{
    while (1) {
        TickType_t now = xTaskGetTickCount();  // 读取当前 tick
        printf("Tick: %lu \n", now);

        vTaskDelay(10); // 延时 10  tick
    }
}

void app_main(void)
{
    xTaskCreate(vDelayTask, "Task1", 2048, NULL, 1, NULL);
}

以上代码执行结果如下:

Tick: 2
Tick: 12
Tick: 22
Tick: 32
Tick: 42
Tick: 52

该示例创建了一个周期性任务,在循环中调用 xTaskGetTickCount() 获取当前系统节拍计数,并将其打印输出。 通过 vTaskDelay(10) 实现任务每隔 10 个 tick 运行一次,形成固定节奏。

从输出结果可见,tick 计数自调度器启动后开始递增,任务按设定周期稳定输出 tick 值。

该 API 还常用于计算任务启动后的运行时间差。 可以在任务开始时调用 xTaskGetTickCount() 记录初始 tick 值,在后续某一时刻再次调用并求差,计算任务执行经过的节拍数。适用于运行耗时统计或逻辑间隔判断。

此外,在使用 vTaskDelayUntil() 实现周期任务时,需要在任务中维护一个 tick 基准时间,该时基由 xTaskGetTickCount() 初始化,用于记录周期起点,从而确保每次延时对齐节拍,避免因任务执行时间波动而引起周期漂移。

vTaskGetRunTimeStats()

vTaskGetRunTimeStats() 用于获取各任务的运行时间占比信息,以字符串形式输出,常用于性能分析和任务执行时间监控(需启用相关配置)。

API 原型

void vTaskGetRunTimeStats( char *pcWriteBuffer );
参数解释

传入参数

参数功能

参数说明

pcWriteBuffer

一个由用户分配的字符数组指针。

用于接收各任务的运行时间统计信息(ASCII 文本格式)。

备注

该缓冲区不会进行越界检查,用户需确保其容量足够大。推荐每个任务预留约 40 字节,以防止输出内容被截断。

API 配置

调用该 API 前需启用相关配置:

API 配置

目标宏

作用

configGENERATE_RUN_TIME_STATS

生成运行时间统计。启用后可收集每个任务的 CPU 时间占用信息。

configUSE_STATS_FORMATTING_FUNCTIONS

启用 vTaskList()vTaskGetRunTimeStats() 等格式化输出函数。

configSUPPORT_DYNAMIC_ALLOCATION

支持动态内存分配。使能动态创建任务/队列/计时器等 API。

portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()

配置运行时间统计计时器。用户需提供实现,用于初始化硬件定时器以收集时间信息。

portGET_RUN_TIME_COUNTER_VALUE()

获取当前运行时间,供统计函数采样使用。

原生 FreeRTOS 配置步骤如下:

  1. 打开 FreeRTOS 项目中的 FreeRTOSConfig.h 文件。

  2. 查找表格中的宏是否被定义(可能被注释或设为 0)。

  3. 如果设为 0,则将其更改为 1。如果没有,手动添加如下内容:

#define configGENERATE_RUN_TIME_STATS            1
#define configUSE_STATS_FORMATTING_FUNCTIONS     1
#define configSUPPORT_DYNAMIC_ALLOCATION         1

// 可更改为其他自行实现的定时器读取函数,返回一个递增的时间基准(单位可为微秒、毫秒等)。
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()     // 如果 SysTick 已由 FreeRTOS 初始化,则留空
#define portGET_RUN_TIME_COUNTER_VALUE()             (xTaskGetTickCount() * portTICK_PERIOD_MS)
  1. 保存文件,重新编译。

基于 ESP-IDF 环境配置步骤如下:

  1. 打开 ESP-IDF 终端。

  2. 输入以下指令,打开 menuconfig 配置界面:

idf.py menuconfig
  1. 按照路径:Component config > FreeRTOS > Kernel,后勾选 configGENERATE_RUN_TIME_STATS, configUSE_TRACE_FACILITYconfigUSE_STATS_FORMATTING_FUNCTIONS

  2. 如该路径下没有这两个宏,可以按下 / 后输入宏名进行查找。

  3. 按下 s 保存修改。

  4. 在项目代码中定义宏:

//  esp_timer 为例
#include "esp_timer.h"

#ifndef portCONFIGURE_TIMER_FOR_RUN_TIME_STATS
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()   // esp_timer 自动初始化,无需实现
#endif

#ifndef portGET_RUN_TIME_COUNTER_VALUE
#define portGET_RUN_TIME_COUNTER_VALUE()   (esp_timer_get_time())
#endif

备注

在 ESP-IDF 中,FreeRTOSConfig.h 由系统组件自动管理,用户直接修改会被编译时覆盖,因此不建议直接修改该文件。正确做法是通过 menuconfig 配置相关选项,并在项目代码中定义必要的宏,实现自定义功能。

示例 1:获取任务的运行时间占比信息

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"

// 绑定 esp_timer 作为运行时间统计计时器
#ifndef portCONFIGURE_TIMER_FOR_RUN_TIME_STATS
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()   // esp_timer 已自动初始化,无需实现
#endif

#ifndef portGET_RUN_TIME_COUNTER_VALUE
#define portGET_RUN_TIME_COUNTER_VALUE() (esp_timer_get_time())  // 返回微秒
#endif

void vExampleTask(void *pvParameters)
{
    while (1) {
        printf("Example Task running...\n");
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void app_main(void)
{
    // 创建示例任务
    xTaskCreate(vExampleTask, "ExampleTask", 2048, NULL, 5, NULL);

    // 分配缓冲区存储运行时间统计字符串
    char *stats_buffer = pvPortMalloc(512);
    if (stats_buffer == NULL) {
        printf("无法分配内存\n");
        return;
    }

    // 启用运行时间统计功能后,持续打印任务运行时间信息
    while (1) {
        vTaskGetRunTimeStats(stats_buffer);
        printf("\nTask Run Time Stats (microseconds):\n%s\n", stats_buffer);
        vTaskDelay(pdMS_TO_TICKS(2000));
    }

    // 此处永远不会到达
    vPortFree(stats_buffer);
}

以上代码执行结果如下:

Example Task running...

Task Run Time Stats (microseconds):
main            20518           38%
IDLE1           19828           37%
IDLE0           0               <1%
ExampleTask     111             <1%
ipc1            32304           60%
ipc0            32149           60%

Example Task running...

Task Run Time Stats (microseconds):
main            32851           1%
IDLE1           2019828         98%
IDLE0           1991926         96%
ExampleTask     208             <1%
ipc0            32149           1%
ipc1            32304           1%

该示例展示了如何在 ESP-IDF 中使用 vTaskGetRunTimeStats() 获取任务的 CPU 占用时间信息。

示例通过创建一个简单任务,结合 esp_timer_get_time() 实现运行时间统计,周期性打印系统中所有任务的累计运行时间及百分比。 统计结果包括用户任务、默认 main 任务和系统任务(如 IDLEIPC 任务),用于观察各任务的运行占比。

该方法适用于系统性能分析和任务行为监控,使用前需在配置中启用运行时间统计功能,并提供字符串缓冲区存放输出内容。

堆栈与统计 API

堆栈与统计类 API 用于获取任务堆栈使用情况和系统任务数量及状态列表,帮助检测堆栈溢出风险和系统任务管理。

uxTaskGetStackHighWaterMark()

uxTaskGetStackHighWaterMark() 用于获取任务运行以来的最小剩余堆栈空间,用于评估堆栈使用峰值,辅助判断是否存在堆栈溢出风险。

API 原型

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
参数解释

传入参数

参数功能

参数说明

xTask

需要查询的任务句柄。

若传入非空句柄,查询对应任务的堆栈空间;若传入 NULL,则查询当前任务自身的堆栈空间。

返回值说明

返回值

说明

返回以字(word)为单位的堆栈高水位标记。

用于监测任务堆栈的安全余量,帮助及时调整堆栈大小避免溢出。

备注

例如,在 32 位系统中:

  • 若返回值为 1,说明还有 4 个字节的堆栈未被使用。

  • 若返回值接近 0,说明任务堆栈已接近溢出风险。

  • 若返回值为 0,说明任务堆栈可能已发生溢出。

示例 1:查询任务的剩余堆栈空间

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 定义全局变量保存任务 2 句柄
TaskHandle_t xHandleTask2 = NULL;

void vTask1(void *pvParameters)
{
    while (1) {
        // 模拟一些操作
        vTaskDelay(pdMS_TO_TICKS(1000));

        // 获取任务最小剩余堆栈空间(单位:word)
        UBaseType_t uxHighWaterMarkForTask1 = uxTaskGetStackHighWaterMark(NULL);    // 当前任务
        UBaseType_t uxHighWaterMarkForTask2 = uxTaskGetStackHighWaterMark(xHandleTask2);    // 任务 2

        printf("任务 1 最低剩余堆栈:%u \n", uxHighWaterMarkForTask1);
        printf("任务 2 最低剩余堆栈:%u \n", uxHighWaterMarkForTask2);
    }
}

void vTask2(void *pvParameters)
{
    // 获取当前任务句柄
    xHandleTask2 = xTaskGetCurrentTaskHandle();
    while (1) {
        // 模拟一些操作
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    xTaskCreate(vTask1, "Task 1", 2048, NULL, 2, NULL);
    xTaskCreate(vTask2, "Task 2", 2048, NULL, 2, NULL);
}

以上代码执行结果如下:

任务 1 最低剩余堆栈:1344
任务 2 最低剩余堆栈:1352
任务 1 最低剩余堆栈:128
任务 2 最低剩余堆栈:1352
任务 1 最低剩余堆栈:128
任务 2 最低剩余堆栈:1352

该示例演示了 uxTaskGetStackHighWaterMark() 的典型用法,通过传入不同任务句柄,获取任务运行以来的最小剩余堆栈空间。

该 API 返回的是历史最低剩余值,而非当前剩余堆栈,用于评估任务在执行期间的堆栈使用峰值,判断是否存在接近溢出的风险。 返回值单位为 word,可根据实际需求转换为字节显示。

通过定期打印该值,可在调试阶段动态监测任务堆栈使用情况,确保其最小剩余堆栈始终维持在合理范围内,从而优化堆栈配置,提升系统的稳定性和可靠性。

uxTaskGetNumberOfTasks()

uxTaskGetNumberOfTasks() 用于获取当前系统中正在运行的任务总数,便于监控任务数量和系统负载情况。

API 原型

UBaseType_t uxTaskGetNumberOfTasks( void );
返回值说明

返回值

说明

返回 RTOS 内核当前正在管理的任务数。

包括所有就绪态、阻塞态和挂起态的任务。已删除但尚未被空闲任务释放的任务也将包含在计数中。

示例 1:查询当前任务数量

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

TaskHandle_t xHandleTask2 = NULL;

void vTask1(void *pvParameters)
{
    int count = 0;
    while (1) {
        UBaseType_t taskCount = uxTaskGetNumberOfTasks();
        printf("当前系统任务数:%u\n", taskCount);

        if (count == 2) {
            printf(">>>>> Task 2 SUSPENDED\n");
            vTaskSuspend(xHandleTask2);
        }
        else if (count == 4) {
            printf(">>>>> Task 2 DELETED\n");
            vTaskDelete(xHandleTask2);
        }
        count++;
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vTask2(void *pvParameters)
{
    // 获取当前任务句柄
    xHandleTask2 = xTaskGetCurrentTaskHandle();

    while (1)
    {
        printf(">>>>> Task 2 is RUNNING\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    xTaskCreate(vTask1, "Task1", 2048, NULL, 5, NULL);
    xTaskCreate(vTask2, "Task2", 2048, NULL, 5, NULL);
}

以上代码执行结果如下:

当前系统任务数:6
>>>>> Task 2 is RUNNING
当前系统任务数:6
>>>>> Task 2 is RUNNING
当前系统任务数:6
>>>>> Task 2 SUSPENDED
当前系统任务数:6
当前系统任务数:6
>>>>> Task 2 DELETED
当前系统任务数:5
当前系统任务数:5

该示例通过调用 uxTaskGetNumberOfTasks() 实时获取当前系统中任务的总数量,验证了该 API 会统计包括就绪态、阻塞态、挂起态以及已删除但尚未由空闲任务释放的任务。

任务 1 定期打印任务数量,并在运行过程中挂起和删除任务 2,输出结果显示挂起状态下任务数不变,任务被删除并释放后任务数减少,说明该 API 能准确反映系统中有效任务的实际数量。

该函数常用于系统调试、任务生命周期监测以及资源管理场景,便于开发者判断任务是否异常增长、是否被正确释放,从而提升系统的可控性与稳定性。

vTaskList()

vTaskList() 用于以文本形式输出系统中所有任务的状态信息,包括任务名、状态、优先级、堆栈剩余和任务序号,常用于任务监控和调试(需启用相关配置)。

API 原型

void vTaskList( char *pcWriteBuffer );
参数解释

传入参数

参数功能

参数说明

pcWriteBuffer

一个由用户分配的字符数组指针。

用于接收各任务的运行时间统计信息(ASCII 文本格式)。

在 ASCII 表中,以下字母用于表示任务的状态:

  • X - 正在运行

  • B - 已阻塞

  • R - 准备就绪

  • D - 已删除(等待清理)

  • S - 已挂起或已阻塞,没有超时

API 配置

调用该 API 前需启用相关配置:

API 配置

目标宏

作用

configUSE_TRACE_FACILITY

启用任务状态跟踪功能,必须为 1 才能使用任务统计相关函数。

configUSE_STATS_FORMATTING_FUNCTIONS

启用 vTaskList()vTaskGetRunTimeStats() 等格式化输出函数。

原生 FreeRTOS 配置步骤如下:

  1. 打开 FreeRTOS 项目中的 FreeRTOSConfig.h 文件。

  2. 查找是否已有这两个宏的定义(可能被注释或设为 0)。

  3. 如果设为 0,则将其更改为 1。如果没有,手动添加如下内容:

#define configUSE_TRACE_FACILITY                1
#define configUSE_STATS_FORMATTING_FUNCTIONS    1
  1. 保存文件,重新编译。

基于 ESP-IDF 环境配置步骤如下:

  1. 打开 ESP-IDF 终端。

  2. 输入以下指令,打开 menuconfig 配置界面:

idf.py menuconfig
  1. 按照路径:Component config > FreeRTOS > Kernel,后勾选 configUSE_TRACE_FACILITYconfigUSE_STATS_FORMATTING_FUNCTIONS

  2. 如该路径下没有这两个宏,可以按下 / 后输入宏名进行查找。

  3. 按下 s 保存修改。

备注

在 ESP-IDF 中,FreeRTOSConfig.h 由系统组件自动管理,用户直接修改会被编译时覆盖,因此不建议直接修改该文件。正确做法是通过 menuconfig 配置相关选项,并在项目代码中定义必要的宏,实现自定义功能。

示例 1:查询任务列表

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

TaskHandle_t xHandleTask2 = NULL;

void vPrintTaskList(void)
{
    // 分配足够的缓冲区存储任务列表信息
    // 注意堆栈大小和任务数量不同,缓冲区大小需适当调整
    char *taskListBuffer = pvPortMalloc(512);
    if (taskListBuffer != NULL) {
        vTaskList(taskListBuffer);
        printf("任务列表:\n%s\n", taskListBuffer);
        vPortFree(taskListBuffer);
    } else {
        printf("内存不足,无法打印任务列表\n");
    }
}

void vTask1(void *pvParameters)
{
    int count = 0;

    while (1) {
        UBaseType_t taskCount = uxTaskGetNumberOfTasks();
        printf("当前系统任务数:%u\n", taskCount);

        vPrintTaskList();

        if (count == 2) {
            printf(">>>>> Task 2 SUSPENDED\n");
            vTaskSuspend(xHandleTask2);
        }
        else if (count == 3) {
            printf(">>>>> Task 2 DELETED\n");
            vTaskDelete(xHandleTask2);
        }
        count++;
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vTask2(void *pvParameters)
{
    xHandleTask2 = xTaskGetCurrentTaskHandle();
    while (1)
    {
        printf(">>>>> Task 2 is RUNNING\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    xTaskCreate(vTask1, "Task1", 4096, NULL, 2, NULL);
    vTaskDelay(pdMS_TO_TICKS(1000));
    xTaskCreate(vTask2, "Task2", 4096, NULL, 2, NULL);
}

以上代码执行结果如下:

当前系统任务数:6
任务列表:
Task1           X       2       2176    6
main            R       1       1920    3
IDLE1           R       0       800     5
IDLE0           R       0       1016    4
ipc1            S       24      536     2
ipc0            S       24      528     1

当前系统任务数:6
任务列表:
Task1           X       2       2144    6
main            R       1       1920    3
IDLE1           R       0       800     5
IDLE0           R       0       808     4
ipc0            S       24      528     1
ipc1            S       24      536     2

>>>>> Task 2 is RUNNING
当前系统任务数:6
任务列表:
Task1           X       2       2144    6
IDLE1           R       0       800     5
IDLE0           R       0       808     4
Task2           B       2       952     7
ipc1            S       24      536     2
ipc0            S       24      528     1

>>>>> Task 2 SUSPENDED
当前系统任务数:6
任务列表:
Task1           X       2       2144    6
IDLE1           R       0       800     5
IDLE0           R       0       808     4
Task2           S       2       952     7
ipc0            S       24      528     1
ipc1            S       24      536     2

>>>>> Task 2 DELETED
当前系统任务数:5
任务列表:
Task1           X       2       2144    6
IDLE1           R       0       800     5
IDLE0           R       0       808     4
ipc1            S       24      536     2
ipc0            S       24      528     1

当前系统任务数:5
任务列表:
Task1           X       2       2144    6
IDLE1           R       0       800     5
IDLE0           R       0       808     4
ipc0            S       24      528     1
ipc1            S       24      536     2

在先前的 uxTaskGetNumberOfTasks() 示例中,虽然仅显式创建了两个任务,但实际打印出的任务数量为 6,说明系统中还存在其他默认任务。

本示例在此基础上进行了扩展,引入 vTaskList(),用于输出当前系统中所有任务的详细信息,包括任务名、状态、优先级、剩余堆栈空间以及任务序号。

从运行结果可以看出,除了用户创建的 Task1 和 Task2 外,系统还包含多个内部任务,如空闲任务( IDLE0IDLE1 )、初始的 main 任务(用于运行 app_main() )以及内核调度相关任务( ipc0ipc1)。 因此,在 Task1 创建后,系统任务总数为 6;在创建 Task2 后,虽然增加了一个任务,但 main 任务在 app_main() 执行结束后自行退出并删除自身,故任务总数仍为 6,直到 Task2 被删除后才减少为 5。

使用 vTaskList() 时需注意以下事项:

  • 在创建第二个任务前适当延时(如调用 vTaskDelay() ),避免因任务创建过快导致堆栈溢出。

  • 每个任务所需堆栈空间应根据功能分配,任务信息缓冲区大小(即传入 vTaskList()buffer)也需根据任务数合理分配,防止输出截断或内存不足。

  • 在 ESP-IDF 中,执行 app_main()main 任务会在函数结束后自动删除,任务数随之减少。但在原生 FreeRTOS 中,main 不作为任务存在,不会出现在任务列表中。

  • 任务编号为系统内部自增标识,不因任务删除而复用,因此编号可能存在跳跃。