软件定时器(Software Timers)与时间管理
本课目标:掌握 FreeRTOS 软件定时器的概念、常用 API 与最佳实践,以及与任务延时(
vTaskDelay/vTaskDelayUntil)的比较。附带实验示例:用软件定时器触发周期性处理并给出代码与流程图。
0. 为什么需要软件定时器?
软件定时器是 RTOS 提供的一种在内核定时服务(Timer Daemon Task)上下文中运行的回调机制,用于把时间驱动的动作从任务中解耦出来。它适合:
- 想要在将来某个时间点执行短小操作
- 不想为定时唤醒写一个单独的高优先级任务
- 需要"轻量级的延时回调"而不涉及硬件定时器复杂性
软件定时器的回调在定时器服务任务里运行(即任务上下文),不是 ISR。回调中不应长时间阻塞;若需要长时间处理,应使用回调来通知一个任务(例如通过信号量或任务通知)。
1. 常用 Timer API(精讲)
1.1 xTimerCreate
c
TimerHandle_t xTimerCreate(
const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * pvTimerID,
TimerCallbackFunction_t pxCallbackFunction
);
pcTimerName:调试用名字xTimerPeriodInTicks:以 tick 为单位的周期uxAutoReload:pdTRUE表示周期性(auto-reload),pdFALSE表示一次性(one-shot)pvTimerID:用户自定义指针(可在回调中通过pvTimerGetTimerID取回)pxCallbackFunction:回调函数,签名为void vTimerCallback(TimerHandle_t xTimer)
1.2 xTimerStart / xTimerReset / xTimerStop / xTimerChangePeriod
xTimerStart(xTimer, xTicksToWait):启动计时器(若已启动可返回成功或错误,视实现而定)xTimerReset(xTimer, xTicksToWait):把计时器余时重置为原始周期(常用于 one-shot)xTimerStop(xTimer, xTicksToWait):停止计时器xTimerChangePeriod(xTimer, xNewPeriod, xTicksToWait):改变周期(常用于动态调整)
所有这些 API 都可以在任务上下文中阻塞一段
xTicksToWait,但最好不要在回调中阻塞。
1.3 FromISR 版本
FreeRTOS 提供 ISR 安全的版本(如 xTimerStartFromISR、xTimerStopFromISR 等),允许在中断中控制软件定时器。
1.4 xTimerDelete
删除计时器并释放资源:
c
BaseType_t xTimerDelete(TimerHandle_t xTimer, TickType_t xTicksToWait);
2. 回调上下文的重要注意点
- 回调执行于定时器服务任务 ,它有自己的优先级(由
configTIMER_TASK_PRIORITY指定)。 - 回调不是 ISR,不应使用 FromISR 版本的 API
- 回调中不要做长时间工作 或调用可能阻塞的 API(例如
xSemaphoreTake(..., portMAX_DELAY)) - 回调里安全可用的策略:发信号、给队列/任务通知(注意使用非阻塞
xQueueSendFromISR的替代;在回调中使用标准版本xQueueSend是可以的,因为回调是任务上下文)
3. 软件定时器 vs vTaskDelay / vTaskDelayUntil
| 特性 | 软件定时器 | vTaskDelay / vTaskDelayUntil |
|---|---|---|
| 运行上下文 | 定时器服务任务回调 | 普通任务上下文 |
| 精度 | 受 tick 精度影响 | 受 tick 精度影响 |
| 适用 | 异步回调、一次性定时;减少任务数量 | 任务内周期性工作、需要复杂逻辑时 |
| 可阻塞 | 回调中尽量不要阻塞 | 任务可以阻塞等待或做复杂处理 |
| 开销 | 定时器服务任务调度 + 内存 | 简单直接,任务切换开销 |
选择建议:
- 若只是需要在未来某时触发一个"短小动作"(例如超时、重试、心跳触发)------用软件定时器
- 若需要周期性且任务需要做大量工作------用独立任务 +
vTaskDelayUntil - 若需要精确控制执行顺序和阻塞行为,把复杂工作放在任务中,用定时器作为触发器(回调只发通知)
4. 用软件定时器触发周期性处理
c
#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"
static TimerHandle_t xPeriodicTimer = NULL;
static TaskHandle_t xWorkerTask = NULL;
void vTimerCallback(TimerHandle_t xTimer)
{
// 在定时器服务任务中运行:通知 worker 任务
xTaskNotifyGive(xWorkerTask);
}
void vWorkerTask(void *pv)
{
for (;;) {
// 等待来自定时器的通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 收到通知,执行工作
printf("[Worker] Timer fired at tick %lu\n", (unsigned long)xTaskGetTickCount());
// 模拟短工作
vTaskDelay(pdMS_TO_TICKS(50));
}
}
int main(void)
{
// 创建周期性软件定时器,周期 500ms
xPeriodicTimer = xTimerCreate("Periodic", pdMS_TO_TICKS(500), pdTRUE, NULL, vTimerCallback);
if (xPeriodicTimer == NULL) {
printf("Failed to create timer\n");
return -1;
}
xTaskCreate(vWorkerTask, "Worker", 256, NULL, tskIDLE_PRIORITY + 2, &xWorkerTask);
// 启动定时器
if (xTimerStart(xPeriodicTimer, 0) != pdPASS) {
printf("Failed to start timer\n");
return -1;
}
vTaskStartScheduler();
for (;;);
}
对比代码
c
void vWorkerPeriodic(void *pv)
{
TickType_t last = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(500);
for (;;) {
// 做工作
printf("[WorkerDelay] tick %lu\n", (unsigned long)xTaskGetTickCount());
vTaskDelayUntil(&last, period);
}
}
对比要点:
- 使用
vTaskDelayUntil的任务在其自身上下文里运行,做更多复杂工作更方便 - 使用软件定时器可以减少任务数量(定时器回调分发),更易于集中管理超时/重试逻辑