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);
运行与验证
- 编译烧录后,用逻辑分析仪探头连接LED引脚(PA5)与GND
- 观察方波信号:
vTaskDelayUntil版本周期应稳定在500ms±1ms(含系统tick误差) - 对比
vTaskDelay版本:由于任务负载10ms,实际周期约为510ms,且误差随负载增加而累积 - 通过条件:
vTaskDelayUntil版本在加入负载前后,周期偏差小于1个系统tick(通常1ms)
关键说明
xLastWakeTime必须在首次调用前初始化,且不能在任务外修改- 延时值必须大于任务最大执行时间,否则
vTaskDelayUntil会立即返回,失去定频效果 - 系统tick频率(
configTICK_RATE_HZ)决定理论精度,本案例默认1000Hz即1ms分辨率
图中上方波形显示vTaskDelay因任务执行时间导致周期逐渐拉长;下方波形显示vTaskDelayUntil通过补偿执行时间,始终保持固定周期输出。