3. 使用vTaskDelayUntil实现精确周期任务

任务目标

在FreeRTOS中,vTaskDelay()的延时从调用时刻开始计时,任务执行时间会导致实际周期漂移。本案例使用vTaskDelayUntil()实现固定频率的任务调度,并通过翻转GPIO测量实际周期,验证周期精度。

核心原理

vTaskDelayUntil(pxPreviousWakeTime, xTimeIncrement)以时间为基准:将*pxPreviousWakeTime加上延时量作为目标唤醒时刻,若任务提前完成则休眠至该时刻,从而补偿任务执行时间造成的漂移。与之对比,vTaskDelay()是相对延时,每次从调用时刻起算,执行时间会累积误差。

硬件与依赖

  • 开发板:任意带LED的STM32或ESP32板卡(本案例以STM32F103为例)
  • 依赖:FreeRTOS内核(已移植),GPIO驱动(HAL或标准库)
  • 测量工具:逻辑分析仪或示波器(测量周期精度)

完整实现

#include "FreeRTOS.h"
#include "task.h"
#include "main.h"

/* LED引脚定义(以PA5为例) */
#define LED_GPIO_PORT   GPIOA
#define LED_PIN         GPIO_PIN_5

/* 任务周期:500ms(即2Hz) */
#define TASK_PERIOD_MS  pdMS_TO_TICKS(500)

static void vPeriodicTask(void *pvParameters)
{
    TickType_t xLastWakeTime;
    const TickType_t xFrequency = TASK_PERIOD_MS;

    /* 初始化上次唤醒时间 */
    xLastWakeTime = xTaskGetTickCount();

    for (;;)
    {
        /* 翻转LED,产生方波便于测量 */
        HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_PIN);

        /* 延时:等待至 xLastWakeTime + xFrequency */
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

void StartPeriodicTask(void)
{
    xTaskCreate(vPeriodicTask, "Periodic", 128, NULL, 2, NULL);
}

对比实验(可选)

将上述代码中的vTaskDelayUntil替换为vTaskDelay(xFrequency),并在翻转LED前加入for循环模拟10ms任务负载(如空转约10000次),对比两种实现的实际周期:

/* 模拟任务负载:约10ms(具体次数需按主频调整) */
for (volatile int i = 0; i < 10000; i++);

/* 相对延时版本 */
vTaskDelay(xFrequency);

运行与验证

  1. 编译烧录后,用逻辑分析仪探头连接LED引脚(PA5)与GND
  2. 观察方波信号:vTaskDelayUntil版本周期应稳定在500ms±1ms(含系统tick误差)
  3. 对比vTaskDelay版本:由于任务负载10ms,实际周期约为510ms,且误差随负载增加而累积
  4. 通过条件:vTaskDelayUntil版本在加入负载前后,周期偏差小于1个系统tick(通常1ms)

关键说明

  • xLastWakeTime必须在首次调用前初始化,且不能在任务外修改
  • 延时值必须大于任务最大执行时间,否则vTaskDelayUntil会立即返回,失去定频效果
  • 系统tick频率(configTICK_RATE_HZ)决定理论精度,本案例默认1000Hz即1ms分辨率
t vTaskDelay(相对延时) 周期漂移↑ vTaskDelayUntil(延时) 固定周期 T 任务执行时间 阻塞等待

图中上方波形显示vTaskDelay因任务执行时间导致周期逐渐拉长;下方波形显示vTaskDelayUntil通过补偿执行时间,始终保持固定周期输出。