软件定时器
软件定时器
FreeRTOS 提供了软件定时器(Software Timer)机制,可以在指定时间到达后执行回调函数。
软件定时器并不依赖 MCU 的硬件定时器,而是由 FreeRTOS 的定时器服务任务(Timer Service Task)统一管理。
可以简单理解为:
软件定时器 = 定时器服务任务 + 定时器控制块 + 定时器命令队列 + 回调函数。
使用前的配置
使用软件定时器之前,需要在 FreeRTOSConfig.h 中开启相关配置:
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY ...
#define configTIMER_QUEUE_LENGTH ...
#define configTIMER_TASK_STACK_DEPTH ...其中:
configUSE_TIMERS:是否启用软件定时器。configTIMER_TASK_PRIORITY:定时器服务任务优先级。configTIMER_QUEUE_LENGTH:定时器命令队列长度。configTIMER_TASK_STACK_DEPTH:定时器服务任务栈大小。
工作原理
软件定时器由定时器服务任务统一管理。用户任务调用 xTimerStart()、xTimerStop()、xTimerReset() 等 API 时,FreeRTOS 会向内部的定时器命令队列发送命令,定时器服务任务再处理这些命令。
整体流程如下:
用户任务
│
│ xTimerStart()
▼
定时器命令队列
│
▼
定时器服务任务
│
▼
管理软件定时器
│
▼
定时器超时
│
▼
执行回调函数定时器服务任务可以同时管理多个软件定时器:
定时器服务任务
│
┌─────────┼─────────┐
▼ ▼ ▼
Timer1 Timer2 Timer3
│ │ │
▼ ▼ ▼
回调1 回调2 回调3因此:
一个软件定时器并不对应一个任务,多个软件定时器由同一个定时器服务任务统一管理。
软件定时器分类
FreeRTOS 软件定时器主要分为两种。
单次定时器
单次定时器超时后执行一次回调函数,然后停止。
创建时将 uxAutoReload 设置为 pdFALSE:
xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000),
pdFALSE,
NULL,
vTimerCallback
);运行过程:
启动
│
▼
等待 1000 ms
│
▼
执行回调函数
│
▼
停止周期定时器
周期定时器超时后执行回调函数,然后自动重新计时。
创建时将 uxAutoReload 设置为 pdTRUE:
xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000),
pdTRUE,
NULL,
vTimerCallback
);运行过程:
启动
│
▼
等待 1000 ms
│
▼
执行回调
│
▼
重新计时
│
▼
执行回调
│
▼
……因此:
pdFALSE → 单次定时器
pdTRUE → 周期定时器定时器控制块
每个软件定时器都有自己的控制块,用于保存定时器相关信息。
典型结构如下:
typedef struct tmrTimerControl
{
const char *pcTimerName;
ListItem_t xTimerListItem;
TickType_t xTimerPeriodInTicks;
UBaseType_t uxAutoReload;
void *pvTimerID;
TimerCallbackFunction_t pxCallbackFunction;
} xTIMER;主要成员:
| 成员 | 作用 |
|---|---|
pcTimerName | 定时器名称 |
xTimerListItem | 用于挂入定时器链表 |
xTimerPeriodInTicks | 定时时间,单位为 Tick |
uxAutoReload | 是否自动重载 |
pvTimerID | 用户自定义数据 |
pxCallbackFunction | 回调函数指针 |
回调函数类型为:
typedef void (*TimerCallbackFunction_t)(TimerHandle_t xTimer);因此回调函数需要满足:
void vTimerCallback(TimerHandle_t xTimer)
{
// 用户代码
}创建软件定时器
创建软件定时器使用:
TimerHandle_t xTimerCreate(
const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction
);参数分别表示:
pcTimerName
↓
定时器名称
xTimerPeriodInTicks
↓
定时时间,单位为 Tick
uxAutoReload
↓
是否自动重载
pvTimerID
↓
用户自定义数据
pxCallbackFunction
↓
回调函数返回值:
TimerHandle_t也就是软件定时器句柄,后续通过这个句柄操作对应的定时器。
例如:
TimerHandle_t xMyTimer = xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000),
pdTRUE,
(void *)1,
vTimerCallback
);启动软件定时器
创建定时器以后,不会自动开始计时,还需要调用 xTimerStart()。
任务中:
xTimerStart(xMyTimer, 0);中断中:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTimerStartFromISR(
xMyTimer,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);停止软件定时器
任务中:
xTimerStop(xMyTimer, 0);中断中:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTimerStopFromISR(
xMyTimer,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);复位软件定时器
复位的作用是重新开始计时。
例如定时器周期为 5 秒,已经运行了 3 秒,此时调用 xTimerReset(),那么剩余的 2 秒不会继续计时,而是重新从 5 秒开始。
任务中:
xTimerReset(xMyTimer, 0);中断中:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTimerResetFromISR(
xMyTimer,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);软件定时器的使用步骤
在代码中使用一个软件定时器,主要可以分为以下 4 个步骤。
第一步:编写回调函数
void vTimerCallback(TimerHandle_t xTimer)
{
printf("Timer timeout!\r\n");
}第二步:创建定时器
TimerHandle_t xMyTimer;
xMyTimer = xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000),
pdTRUE,
NULL,
vTimerCallback
);第三步:检查创建是否成功
xTimerCreate() 创建失败时会返回 NULL,因此应该进行判断:
if (xMyTimer == NULL)
{
// 创建失败
}第四步:启动定时器
if (xTimerStart(xMyTimer, 0) != pdPASS)
{
// 启动失败
}因此可以记成:
使用软件定时器
① 编写回调函数
↓
② 创建定时器
↓
③ 检查创建是否成功
↓
④ 启动定时器如果只记最核心的调用关系:
xTimerCreate()
↓
xTimerStart()
↓
等待超时
↓
回调函数执行回调函数注意事项
软件定时器回调函数运行在定时器服务任务中,而不是中断中。
因此回调函数不能长时间占用 CPU,也不应该调用会使定时器服务任务阻塞的 API。
例如不要这样写:
void vTimerCallback(TimerHandle_t xTimer)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}也不要在回调函数中执行长时间的循环、复杂计算等耗时操作。
推荐的方式是:
软件定时器回调
│
│ 快速发送事件
▼
队列 / 任务通知
│
▼
普通任务
│
▼
执行耗时操作这样可以避免长时间占用定时器服务任务。
Timer ID 的使用
创建定时器时,可以通过 pvTimerID 保存用户数据:
xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000),
pdTRUE,
(void *)1,
vTimerCallback
);然后在回调函数中获取:
void vTimerCallback(TimerHandle_t xTimer)
{
void *pvID = pvTimerGetTimerID(xTimer);
printf("Timer ID = %d\r\n", (int)pvID);
}这样可以让多个软件定时器共用一个回调函数,再通过 Timer ID 区分具体是哪一个定时器触发。
完整示例
下面是一个完整的周期软件定时器:
// 回调函数
void vTimerCallback(TimerHandle_t xTimer)
{
void *pvID = pvTimerGetTimerID(xTimer);
printf("Timer ID = %d, timeout!\r\n", (int)pvID);
}
void Timer_Init(void)
{
TimerHandle_t xMyTimer;
// 创建定时器
xMyTimer = xTimerCreate(
"MyTimer",
pdMS_TO_TICKS(1000),
pdTRUE,
(void *)1,
vTimerCallback
);
// 检查创建结果
if (xMyTimer == NULL)
{
printf("Timer create failed!\r\n");
return;
}
// 启动定时器
if (xTimerStart(xMyTimer, 0) != pdPASS)
{
printf("Timer start failed!\r\n");
return;
}
}该定时器的工作过程为:
创建 Timer
│
▼
周期 = 1000 ms
│
▼
启动 Timer
│
▼
等待 1000 ms
│
▼
执行 vTimerCallback()
│
▼
自动重载
│
▼
等待 1000 ms
│
▼
再次执行 vTimerCallback()
│
▼
……总结
FreeRTOS 软件定时器的核心结构可以概括为:
软件定时器
│
▼
定时器服务任务
│
┌─────────┴─────────┐
▼ ▼
定时器命令队列 定时器链表
│ │
└─────────┬─────────┘
▼
管理定时器
│
▼
超时
│
▼
回调函数需要掌握的核心知识点:
xTimerCreate():创建软件定时器。xTimerStart():启动软件定时器。xTimerStop():停止软件定时器。xTimerReset():重新开始计时。pdFALSE:单次定时器。pdTRUE:周期定时器。TimerHandle_t:软件定时器句柄。TimerCallbackFunction_t:回调函数类型。- 软件定时器由定时器服务任务统一管理。
- 回调函数运行在定时器服务任务上下文中,因此不能长时间阻塞。

