任务管理

[English]

在 FreeRTOS 中,任务管理是核心功能之一,其中任务延时、优先级控制和状态切换等机制为任务调度与执行提供了灵活性和可控性。

任务延时 API

FreeRTOS 提供了两种常用的任务延时 API,用于控制任务的执行节奏。它们都能让当前任务在指定的 tick 时间内进入阻塞状态,暂时让出 CPU 使用权。

vTaskDelay()

vTaskDelay() 用于将当前任务从调用时刻起延迟指定数量的系统时钟节拍(tick),延时期间任务进入阻塞态,释放 CPU 使用权。

该 API 实现的是相对延时,即延迟时间从 API 调用的当前 tick 时刻开始计算,而非基于固定时间基准。 因此,该 API 适用于实现简单的延时行为,但不适合用于需要严格周期控制的任务,因为其执行周期可能受任务执行时间波动或调度机制影响而产生偏移。

API 原型

void vTaskDelay( const TickType_t xTicksToDelay );
参数解释

传入参数

参数功能

参数说明

xTicksToDelay

设置任务延时的时长。

以系统时钟节拍(tick)为单位。延时期间任务处于阻塞状态,不会被调度执行。

备注

系统时钟节拍 (tick)是 FreeRTOS 中的基本时间单位,由系统定时器周期性地产生。每个 tick 所对应的实际时间由宏 portTICK_PERIOD_MS 表示,单位为毫秒。该值是根据宏 configTICK_RATE_HZ 自动计算得出,计算公式为: portTICK_PERIOD_MS = 1000 / configTICK_RATE_HZ。 其中, configTICK_RATE_HZ 表示每秒产生多少个 tick,即系统时钟节拍频率。以上两个宏均定义在系统配置文件(如 FreeRTOSConfig.h )中,可根据应用需求进行调整。

需要注意的是,应仅修改 configTICK_RATE_HZ 来改变系统 tick 的时间间隔, portTICK_PERIOD_MS 会自动根据其数值更新。修改 tick 频率后,应确保所有依赖时间计算的功能(如延时、定时器、超时控制等)仍能按预期工作,避免因单位变化导致调度异常或时序错误。

备注

建议在实际开发中使用 pdMS_TO_TICKS() 宏将延时时间从毫秒转换为 tick 数。 由于 vTaskDelay() 接收的参数是 tick 数,而非毫秒,直接填写毫秒值可能导致理解偏差或计算错误。 通过使用该宏,可按常见的时间单位(毫秒)表达期望的延时时长,程序会根据系统 tick 配置自动换算为对应的 tick 数。 这不仅使代码更易读、易于理解,也有助于在更改 configTICK_RATE_HZ 或移植到不同平台时保持时间控制逻辑的一致性,减少维护成本。

示例 1:简单延时循环任务

#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 - Task running\n", now);

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

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

以上代码执行结果如下:

Tick: 2 - Task running
Tick: 102 - Task running
Tick: 202 - Task running
Tick: 302 - Task running
Tick: 402 - Task running
Tick: 502 - Task running

该示例展示了 vTaskDelay() 的基本用法:任务每次执行后延时 100 个 tick,并在唤醒时打印当前 tick 值。运行结果表明,任务每隔 100 个 tick 输出一次,表现出其固定的执行周期。

示例 2:任务执行时间不固定引发周期漂移

// 主函数部分与示例 1 相同,此处不再重复展示
void vDelayTask (void *pvParameters)
{
    while (1) {
        TickType_t now = xTaskGetTickCount();
        printf("Tick: %lu - Task running\n", now);

        vTaskDelay(rand() % 50); // 模拟任务处理过程的不固定耗时

        vTaskDelay(100); // 延时 100 个 tick
    }
}

以上代码执行结果如下:

Tick: 2 - Task running
Tick: 135 - Task running
Tick: 278 - Task running
Tick: 390 - Task running
Tick: 519 - Task running
Tick: 619 - Task running

该示例在任务延时前加入一段随机长度的处理过程,用于模拟实际任务中执行时间的不确定性。由于任务每次执行所耗时间不同,调用 vTaskDelay(100) 的时刻也随之变化,进而使任务输出 tick 值的间隔逐渐偏移,周期表现出不稳定。

这体现了 vTaskDelay() 的相对延时特性,即延时从函数调用的时刻开始计算,而非基于固定的时间参考点。由于每次延时的起点取决于前一次任务完成的时间,一旦任务执行时间发生变化,后续延时也会相应偏移,进而导致周期逐渐漂移。因此,该方法不适用于对周期精度要求较高的任务。

备注

“周期漂移”是指由于任务每次执行所耗时间不一致,导致任务逐渐偏离原本设定的周期节奏。

示例 3:延时精度限制

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"  // 获取微秒计数器

// 主函数部分与示例 1 相同,此处不再重复展示
void vDelayTask (void *pvParameters)
{
    while (1) {
        int64_t start_us = esp_timer_get_time();    // 获取当前时间(延时前),单位:微秒
        vTaskDelay(1); // 延时 1 个 tick
        int64_t end_us = esp_timer_get_time();  // 获取当前时间(延时后),单位:微秒
        float actual_delay_ms = (end_us - start_us)/1000.0f;    // 将单位转换为毫秒

        printf("理论延时:%lu ms,实际延时:%.2f ms\n", portTICK_PERIOD_MS, actual_delay_ms);   // 对比理论延时时间和实际延时时间
    }
}

以上代码执行结果如下:

理论延时:10 ms,实际延时:4.45 ms
理论延时:10 ms,实际延时:9.54 ms
理论延时:10 ms,实际延时:9.80 ms
理论延时:10 ms,实际延时:9.84 ms
理论延时:10 ms,实际延时:9.83 ms
理论延时:10 ms,实际延时:9.84 ms
理论延时:10 ms,实际延时:9.83 ms
理论延时:10 ms,实际延时:9.84 ms
理论延时:10 ms,实际延时:9.83 ms

该示例用于对比 vTaskDelay() 的理论延时与实际延时之间的差异,验证基于 tick 的延时存在一定精度误差。

为了更精确地评估基于 tick 的延时精度,本示例引入 esp_timer_get_time() 以获取微秒级系统时间,便于测量 vTaskDelay() 实际延时与理论值之间的偏差。 任务每次执行时,先记录当前时间,调用 vTaskDelay(1) 进行 1 个 tick 的延时后,再次读取当前时间,并计算前后时间差作为实际延时时长。 最终将该值与理论延时 portTICK_PERIOD_MS (当前配置为 10 ms)进行对比,以观察二者之间的差异。

从运行结果可见,首次延时显著短于理论值,仅约 4.45 ms,这通常是由于任务在初次调用 vTaskDelay() 时正好接近下一个 tick 中断,导致实际等待时间被缩短。 随后多次延时趋于稳定,约为 9.8 ms,虽接近设定的 10 ms,但仍略有偏差。 这说明通过设置 tick 数进行延时时,实际等待时间可能略短或略长,导致任务早于或晚于预期时间开始执行。

需要注意的是,即使延时时间加长,单次调用的误差也不会随之放大,最多偏离一个 tick 周期,但在对时间精度敏感的应用中,仍可能影响任务节奏或累积偏差。

xTaskDelayUntil()

xTaskDelayUntil() 是为解决 vTaskDelay() 在周期控制上的不稳定问题而设计的,提供一种“节拍精确”的延时方式,使任务以固定周期运行。 相比相对延时机制,它能够保持稳定的周期节奏,有效避免因任务执行时间不一致而导致的周期漂移问题。

该函数适用于对周期精度要求较高的场景,如传感器数据采集、定时控制、周期性通信等任务。

备注

可能会见到 vTaskDelayUntil() 的写法。 该函数与 xTaskDelayUntil() 功能一致,唯一区别在于是否返回延时结果:

  • xTaskDelayUntil() 会返回任务是否按预期节拍被唤醒。

  • vTaskDelayUntil() 不提供返回值。

建议优先使用 xTaskDelayUntil(),以便在需要时监测调度偏差。

API 原型

BaseType_t xTaskDelayUntil( TickType_t *pxPreviousWakeTime,
                            const TickType_t xTimeIncrement );
参数解释

传入参数

参数功能

参数说明

pxPreviousWakeTime

指向上一次任务被唤醒时间的变量指针。

第一次调用前需使用 xTaskGetTickCount() 将该变量初始化为当前 tick 值。每次函数调用后,系统会自动更新该变量为 *pxPreviousWakeTime + xTimeIncrement 对应的 tick 时间,用于实现周期基准保持一致。

xTimeIncrement

周期时间长度。

以系统节拍(tick)为单位。函数会使任务阻塞至指定周期起点 *pxPreviousWakeTime + xTimeIncrement 对应的 tick 时间,从而实现节拍对齐的周期执行。

返回值说明

返回值

说明

pdTRUE

任务成功延时。

pdFALSE

任务未被延时。

备注

xTaskDelayUntil() 主要用于实现周期性任务。 如果调用时间晚于预期唤醒时间(例如执行周期变长),函数将立即返回 pdFALSE,表示任务运行滞后,可能需要检查任务执行时间、系统负载或任务优先级。

示例 1:简单延时循环任务

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

// 任务函数
void vDelayTask (void *pvParameters)
{
    const TickType_t xDelay = pdMS_TO_TICKS(1000);   // 期望延时周期:1000 ms,即 100 tick
    TickType_t xLastWakeTime = xTaskGetTickCount(); // 初始化为当前 tick

    while (1) {
        printf("Tick: %lu - Task running\n", xTaskGetTickCount());
        vTaskDelay(rand() % 50); // 模拟任务处理过程的不固定耗时
        vTaskDelayUntil(&xLastWakeTime, xDelay);
    }
}

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

以上代码执行结果如下:

Tick: 2 - Task running
Tick: 102 - Task running
Tick: 202 - Task running
Tick: 302 - Task running
Tick: 402 - Task running
Tick: 502 - Task running

本示例展示了如何使用 vTaskDelayUntil() 实现周期稳定的任务调度。 任务运行周期设置为 1000 ms(即 100 tick),与前文 vTaskDelay() 示例 2 处保持一致,方便对比两者在调度行为上的差异。 通过在每轮循环前加入一段随机长度的处理过程,模拟任务处理时间的不确定性,以此验证即使任务执行耗时存在波动,使用 vTaskDelayUntil() 仍能确保任务以固定周期运行,有助于实现精确定时控制。

执行结果显示每次任务运行时间点稳定间隔 100 tick,说明周期得到了有效控制。

vTaskDelay() 相比,vTaskDelayUntil() 能避免因任务执行耗时不同所导致的周期漂移问题。其机制基于“上次预期唤醒时间”进行延时计算,而非从调用时刻开始计时,因此能够保持周期恒定。 例如,即使任务本轮执行较快或较慢,下一轮依然会按原计划时间点触发,除非任务执行时间超过整个周期。

备注

vTaskDelayUntil() 仅在任务实际执行时间小于周期间隔时才能发挥精确控制的效果。 若任务耗时超过设定周期,周期性执行将无法保持准确,并会产生延时积压。为保证调度稳定,建议提前评估任务执行时间并合理设置周期参数。

任务优先级管理 API

FreeRTOS 提供了两种常用的任务优先级管理 API ,用于获取或修改任务的运行优先级,以控制任务在调度器中的执行顺序和响应速度。

uxTaskPriorityGet()

uxTaskPriorityGet() 用于获取指定任务当前的优先级。

API 原型

UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
参数解释

传入参数

参数功能

参数说明

xTask

指定要查询的任务句柄。

若传入非空句柄,返回对应任务的当前优先级;若传入 NULL,则返回调用该函数的任务自身的优先级。

返回值说明

返回值

说明

返回所查询任务的当前优先级。

类型为 UBaseType_t

示例 1:查询任务优先级

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

TaskHandle_t xHandleTask1 = NULL;

// 任务 1 函数
void vTask1 (void *pvParameters)
{
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 任务 2 函数
void vTask2 (void *pvParameters)
{
    // 在任务 2 中创建任务 1,确保创建后立即获取其句柄,便于后续查询其优先级
    xTaskCreate(vTask1, "Task1", 2048, NULL, 1, &xHandleTask1);

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

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

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void)
{
    xTaskCreate(vTask2, "Task2", 2048, NULL, 2, NULL);
}

以上代码执行结果如下:

Priority of Task 1 is 2
Priority of Task 2 is 1
Priority of Task 1 is 2
Priority of Task 2 is 1
Priority of Task 1 is 2
Priority of Task 2 is 1

该示例展示如何使用 uxTaskPriorityGet() 获取当前任务和其他任务的优先级。

任务 1 以优先级 2 创建,任务 2 以优先级 1 创建。任务 1 中通过传入 NULL 获取自身优先级,通过任务句柄 xHandleTask2 获取任务 2 的优先级。 为确保任务句柄有效,任务 1 在自身执行中创建任务 2,并在成功获取其句柄后再进入主循环,以保证后续操作的有效性。

运行结果显示,两个任务的优先级与创建时设定一致,说明 uxTaskPriorityGet() 能正确获取指定任务的当前优先级。

备注

传入的任务句柄必须已成功创建并可被调度。若传入的句柄尚未初始化(例如尚未从 xTaskCreate() 返回或全局变量尚未赋值),可能导致结果异常或系统错误。

vTaskPrioritySet()

vTaskPrioritySet() 用于修改指定任务的优先级,支持在系统运行期间动态调整任务调度策略。

API 原型

void vTaskPrioritySet( TaskHandle_t xTask,
                       UBaseType_t uxNewPriority );
参数解释

传入参数

参数功能

参数说明

xTask

指定要修改的任务句柄。

若传入非空句柄,返回对应任务的当前优先级;若传入 NULL,则返回调用该函数的任务自身的优先级。

uxNewPriority

要为目标任务设置的优先级值。

范围为 0configMAX_PRIORITIES - 1

示例 1:修改并查询任务优先级

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

TaskHandle_t xHandleTask1 = NULL;

// 任务 1 函数
void vTask1 (void *pvParameters)
{
    // 延迟一个 tick,确保优先级被正确更新后再开始执行
    vTaskDelay(pdMS_TO_TICKS(10));

    while (1) {
        printf("Task 1 running at priority %d\n", uxTaskPriorityGet(NULL));
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

// 任务 2 函数
void vTask2 (void *pvParameters)
{
    // 在任务 2 中创建任务 1,确保创建后立即获取其句柄,便于后续修改其优先级
    xTaskCreate(vTask1, "Task1", 2048, NULL, 1, &xHandleTask1);

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

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

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

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void app_main(void)
{
    xTaskCreate(vTask2, "Task2", 2048, NULL, 2, NULL);
}

以上代码执行结果如下:

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

该示例展示了如何在运行时动态修改任务优先级,并通过 uxTaskPriorityGet()vTaskPrioritySet() 函数实现对任务调度行为的控制。

任务 2 在创建任务 1 后,获取其任务句柄并判断是否需要提升其优先级。为避免任务 1 启动时读取到未更新的优先级,任务 1 在起始处使用 vTaskDelay() 延迟一个 tick,确保其首次打印显示的是更新后的优先级。

程序输出结果表明,任务 1 初始优先级为 1,任务 2 发现其低于自身优先级 2 后,通过 vTaskPrioritySet() 提升其至 2。此后,任务 1 运行并打印其当前优先级为 2,验证优先级修改生效。 后续循环中,两任务优先级保持一致,无需进一步调整。

任务状态管理 API

任务状态管理 API 用于控制任务的执行状态,如挂起、恢复、中止延迟等,从而实现更灵活的任务调度与控制。

vTaskSuspend()

vTaskSuspend() 用于挂起指定任务,使其停止运行,直到被其他任务恢复。被挂起的任务不会参与调度,无论其优先级如何,几乎不占用系统资源,适用于临时让出 CPU 的场景。

API 原型

void vTaskSuspend( TaskHandle_t xTaskToSuspend );
参数解释

传入参数

参数功能

参数说明

xTaskToSuspend

指定要挂起的任务句柄。

若传入非空句柄,挂起对应任务;若传入 NULL,则挂起当前任务自身。

示例 1:优先级相同任务创建与挂起

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

TaskHandle_t xHandleTask2 = NULL;
static int cnt = 0;

// 任务 1 函数
void vTask1 (void *pvParameters)
{
    int *cnt = pvParameters;
    // 任务函数的主体通常为死循环
    while (1) {
        if (*cnt != 2) {
            printf("Task 1 is running\n");
        }
        else {
            printf("Task 2 is suspended\n");
            vTaskSuspend(xHandleTask2);
        }
        (*cnt)++;
        vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1 s
    }
}

// 任务 2 函数
void vTask2 (void *pvParameters)
{
    while (1) {
        printf("Task 2 is running\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

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

以上代码执行结果如下:

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

该示例展示了在两个优先级相同的任务中,使用 vTaskSuspend() 挂起另一个任务的基本用法。任务 1 和任务 2 均以优先级 1 创建,当计数变量 cnt 增加至 2 时,任务 1 通过已保存的任务句柄挂起任务 2,导致任务 2 后续不再执行,仅任务 1 持续运行。

该示例验证了即便两个任务优先级相同,只要任务处于运行状态,即可通过 vTaskSuspend() 挂起另一个任务,实现任务间的动态调度控制。

同时,对于不同优先级的任务,也可以通过 vTaskSuspend() 进行挂起操作。 然而,建议由高优先级任务挂起低优先级任务,因为低优先级任务在系统调度中可能长时间得不到执行机会,无法及时或可靠地对高优先级任务进行挂起操作,可能引发预期外的行为或调度延迟。 由高优先级任务主动管理低优先级任务的状态,可提高系统的确定性与响应效率。

vTaskResume()

vTaskResume() 用于恢复因 vTaskSuspend() 而被挂起的任务,使其重新进入就绪态并参与调度。

备注

被多次挂起的任务仅需调用一次 vTaskResume() 即可恢复,但多次挂起可能导致状态管理混乱,在设计上应尽量避免对同一任务重复挂起。

API 原型

void vTaskResume( TaskHandle_t xTaskToResume );
参数解释

传入参数

参数功能

参数说明

xTaskToResume

指定要恢复的任务句柄。

传入的任务必须已通过 vTaskSuspend() 被挂起,否则调用无效。

示例 1:任务挂起及恢复

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

TaskHandle_t xHandleTask2 = NULL;
static int cnt = 0;

// 任务 1 函数
void vTask1 (void *pvParameters)
{
    int *cnt = pvParameters;

    while (1) {
        if (*cnt == 2) {  // 挂起任务 2
            printf("Task 2 is suspended\n");
            vTaskSuspend(xHandleTask2);
        }
        else if (*cnt == 4){  // 恢复任务 2
            printf("Task 2 is resumed\n");
            vTaskResume(xHandleTask2);
        }
        else {
            printf("Task 1 is running\n");
        }
        (*cnt)++;
        vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1 s
    }
}

// 任务 2 函数
void vTask2 (void *pvParameters)
{
    while (1) {
        printf("Task 2 is running\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    xTaskCreate(vTask1, "Task1", 2048, (void *)&cnt, 2, NULL);
    xTaskCreate(vTask2, "Task2", 2048, NULL, 1, &xHandleTask2);
}

以上代码执行结果如下:

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

该示例展示了如何结合使用 vTaskSuspend()vTaskResume() 控制任务的挂起与恢复。

任务 1 通过计数器变量 cnt 控制逻辑流程:当计数器为 2 时调用 vTaskSuspend() 挂起任务 2,使其停止输出;当计数器为 4 时,再通过 vTaskResume() 使任务 2 恢复执行。

输出结果清晰体现了任务 2 的挂起与恢复过程,验证了任务可在运行时动态控制其他任务的执行状态。

这种机制可用于根据特定条件灵活启用或停用任务,从而更有效地管理系统资源与行为。

备注

如果一个任务挂起了另一个关键任务,而未能确保及时恢复,可能导致系统逻辑卡住。建议搭配条件判断或超时机制,避免这种情况。