FreeRTOS 软件定时器(Software Timer)
下面详细讲解 FreeRTOS 软件定时器(Software Timer) 的相关知识点
TimerHandle_t xTimer;voidvTimerCallback(TimerHandle_t xTimer){printf("Timer callback executed!\r\n");}//void main() {// // 创建周期为1000ms的软件定时器// xTimer = xTimerCreate("MyTimer", pdMS_TO_TICKS(1000), // pdTRUE, NULL, vTimerCallback);// xTimerStart(xTimer, 0);// vTaskStartScheduler();//}
一、什么是软件定时器?
软件定时器是由 FreeRTOS 内核提供的一种基于系统时钟节拍(Tick)的定时服务,允许用户设置一个函数(回调函数)在指定的未来时间点执行。与硬件定时器相比,软件定时器具有以下特点:
-
不依赖特定硬件:使用任务和系统时基实现,可以在任何支持 FreeRTOS 的平台上运行。 -
数量不限:只需配置足够的内存( configTIMER_TASK_STACK_DEPTH和configUSE_TIMERS)。 -
精度受系统节拍限制:超时周期必须是 tick 周期的整数倍(通常 1ms ~ 100ms)。 -
回调函数在定时器服务任务上下文中执行,需要遵循特殊规则(不能阻塞、不能延时、不能调用会阻塞的 API)。
软件定时器通常用于:
-
周期性任务(如 LED 闪烁、传感器轮询) -
一次性延时执行(超时处理、去抖动) -
软件超时机制(如等待应答超时)
二、软件定时器的两种类型
-
单次定时器(One-shot Timer)启动后,到期时执行一次回调,然后自动停止。需重新启动才会再次触发。
-
周期定时器(Auto-reload Timer / Periodic Timer)启动后,到期时执行回调,并自动重启,以固定周期持续触发。
示例中:
xTimerCreate(..., pdTRUE, ...); // pdTRUE 表示周期定时器
三、核心 API 函数详解
1. 创建定时器:xTimerCreate
TimerHandle_t xTimerCreate( constchar * const pcTimerName,const TickType_t xTimerPeriodInTicks,const UBaseType_t uxAutoReload,void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction );
参数说明:
-
pcTimerName:定时器名称(调试用,不参与逻辑)。 -
xTimerPeriodInTicks:定时周期,单位是 tick。使用pdMS_TO_TICKS(ms)将毫秒转换为 tick。 -
uxAutoReload: -
pdTRUE→ 周期定时器。 -
pdFALSE→ 单次定时器。 -
pvTimerID:自定义标识符,可用于多个定时器共享同一个回调函数时区分是哪个定时器触发的。 -
pxCallbackFunction:回调函数指针。 -
返回值:定时器句柄(失败返回 NULL,通常是堆内存不足)。
静态创建版本:xTimerCreateStatic(需提供 StaticTimer_t 缓冲区)。
2. 启动定时器:xTimerStart
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
-
将定时器加入活跃队列,开始计时。 -
如果定时器已经运行, xTimerStart会重置其周期(重新开始计时,相当于先停止再启动)。 -
xTicksToWait:如果定时器命令队列已满,调用任务最多等多久。通常设为 0,因为定时器操作应当非常快速。若从任务调用且命令队列可能满,可以指定短超时。 -
返回值: pdPASS表示成功,pdFAIL表示命令队列满且超时。
中断安全版本:xTimerStartFromISR。
3. 其他常用 API
-
xTimerStop:停止定时器(如果正在运行)。 -
xTimerReset:重置定时器(若运行则重新开始计时;若未运行则相当于启动)。 -
xTimerChangePeriod:动态改变定时周期(可同时改变自动重载标志)。 -
xTimerIsTimerActive:查询定时器是否处于活动状态。 -
xTimerGetPeriod/xTimerGetExpiryTime/xTimerGetTimerID等查询函数。
所有“从 ISR 调用的版本”都以 FromISR 结尾,并需要一个 BaseType_t *pxHigherPriorityTaskWoken 参数来指示是否需要上下文切换。
四、示例代码分析
TimerHandle_t xTimer;voidvTimerCallback(TimerHandle_t xTimer){printf("Timer callback executed!\r\n");}voidmain(){ xTimer = xTimerCreate("MyTimer", pdMS_TO_TICKS(1000), pdTRUE, NULL, vTimerCallback); xTimerStart(xTimer, 0); vTaskStartScheduler();}
功能预期
-
创建一个周期为 1 秒的周期定时器。 -
启动定时器,回调函数 vTimerCallback会每秒钟执行一次,打印信息。 -
调度器启动后,系统正常运行,定时器任务会管理定时器队列,在到期时调用回调。
注意事项
-
需要配置 FreeRTOS: configUSE_TIMERS必须设为1,并且要定义configTIMER_TASK_PRIORITY(定时器服务任务优先级)、configTIMER_TASK_STACK_DEPTH(堆栈大小)。 -
回调函数的限制: -
不允许调用可能会阻塞的 API(如 vTaskDelay、xQueueReceive带非零超时、xSemaphoreTake等)。 -
不能调用可能导致阻塞的挂起、删除任务等操作。 -
应尽量短小,快速执行。因为所有定时器回调都在同一个任务(定时器服务任务)中串行执行,长时间运行会延迟其他定时器的触发。 -
可以使用 printf(前提是printf不阻塞,例如输出到串口时使用中断驱动或非阻塞缓冲)。 -
pvTimerID为 NULL:如果需要区分多个定时器,可以设置不同的 ID,回调内用pvcTimerGetTimerID获取。 -
xTimerStart的超时为 0:如果定时器命令队列满,xTimerStart将立即返回pdFAIL。由于队列通常很空(命令数量有限),风险很小。但严格起见,可以检查返回值并处理失败。
五、软件定时器的内部实现原理
-
定时器服务任务(Timer Daemon Task)FreeRTOS 在启动调度器时自动创建一个高优先级的任务(优先级由 configTIMER_TASK_PRIORITY定义),该任务维护一个定时器列表(按超时时间排序),并调用xTaskDelayUntil来实现精确的到期唤醒。 -
命令队列用户程序(如 xTimerStart)并不是直接操作定时器数据结构,而是向定时器服务任务发送一个命令。这样可以避免并发访问问题,所有定时器操作都在统一的任务上下文中串行执行。 -
到期处理定时器服务任务被唤醒后,检查所有到期的定时器,调用其回调函数,然后如果是周期定时器则重新插入定时器列表。
为什么需要命令队列?
-
用户任务可能在任何优先级下调用 xTimerStart,如果不通过命令队列,直接操作定时器列表会导致竞态条件。 -
将所有定时器操作集中到同一个任务中,简化了同步并确保回调执行环境的一致性。
六、使用注意事项
1. 配置要求
#define configUSE_TIMERS 1#define configTIMER_TASK_PRIORITY 2 // 通常设得比大多数任务高一些#define configTIMER_TASK_STACK_DEPTH 256 // 视回调函数复杂度调整#define configUSE_DAEMON_TASK_STARTUP_HOOK 0 // 可选
2. 回调函数中的禁忌
-
禁止调用 vTaskDelay、vTaskDelayUntil、xQueueReceive(非0超时)、xSemaphoreTake(非0超时)、xEventGroupWaitBits等可能阻塞的函数。 -
禁止调用 vTaskSuspend或vTaskDelete挂起/删除当前任务。 -
禁止调用 会间接导致阻塞的函数(如某些打印函数等待互斥量)。 -
允许使用 xQueueSend、xSemaphoreGive等无阻塞的 API(超时设为 0),以及xTimerChangePeriod等定时器控制 API(但注意可能再次引发命令队列发送,嵌套深度有限)。
3. 启动调度器前创建定时器
-
定时器可以在调度器启动前创建。调度器启动后自动创建定时器服务任务并开始处理命令。 -
如果调度器还未启动, xTimerStart等函数将延迟生效(实际执行等到服务任务运行)。
4. 定时器的精度
-
超时时间是你指定的 tick 数,实际精确度受系统 tick 频率影响。例如节拍为 1ms,则定时器周期精度为 ±1ms。 -
多个定时器的触发时刻可能因为服务任务优先级和系统负载而略有 jitter。
七、与硬件定时器的对比
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
八、扩展示例:单次定时器 + 使用 Timer ID
voidvOneShotCallback(TimerHandle_t xTimer){uint32_t id = (uint32_t)pvTimerGetTimerID(xTimer);printf("One-shot timer with ID %ld fired!\n", id);}voidcreateAndStartOneShot(void){ TimerHandle_t t = xTimerCreate("OneShot", pdMS_TO_TICKS(500), pdFALSE, (void*)123, vOneShotCallback);if (t) xTimerStart(t, 0);}
总结
-
FreeRTOS 软件定时器通过内核任务管理,提供灵活的定时服务。 -
创建时指定周期、是否自动重载、回调函数。 -
启动/停止/重置等操作通过命令队列与定时器服务任务通信。 -
回调函数必须非阻塞、快速完成,避免破坏系统实时性。 -
需要正确配置 configUSE_TIMERS及相关宏。
代码示例正确地展示了周期性软件定时器的基本用法,只需确保 FreeRTOS 配置正确,即可实现每秒打印一条信息的功能。如果有更精确的定时需求或硬实时约束,则应考虑使用硬件定时器。
常见疑惑补充说明
-
TimerHandle_t需要引用哪个头文件? -
#include “timers.h” -
xTimerStart(t, 0); -
定时器服务任务的命令队列深度通常足够大(由 configTIMER_QUEUE_LENGTH 配置),且系统中定时器操作不频繁,因此队列满的概率很低。 -
设为 0 可以避免调用任务因等待命令队列而进入阻塞态,保证实时性。 -
需要启动定时器的任务通常不希望在这里被长时间阻塞,所以使用 0 作为超时参数是常见且安全的选择。 -
函数原型:BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait ); -
t:定时器句柄(TimerHandle_t 类型),指向之前通过 xTimerCreate 创建的定时器对象。 -
0:这是 xTicksToWait 参数,表示如果定时器命令队列已满,调用任务最多等待多少个 tick。 -
为什么通常设为 0
附录
完整代码示例:
// my_task.cTimerHandle_t xTimer;voidvTimerCallback(TimerHandle_t xTimer){printf("Timer callback executed!\r\n");}// main.cvoidCurrent_Program(void){ LED_Init(); // 初始化LED设备 UART1_Config(); // UART1串口初始化 TimerHandle_t xTimer;voidvTimerCallback(TimerHandle_t xTimer);// 创建一个周期为 1 秒的周期定时器。 xTimer = xTimerCreate("MyTimer", pdMS_TO_TICKS(1000), // 使用 pdMS_TO_TICKS(ms) 将毫秒转换为 tick pdTRUE, // pdTRUE 表示周期定时器 NULL, // 自定义标识符,可用于多个定时器共享同一个回调函数时区分是哪个定时器触发的 vTimerCallback); // 回调函数指针// 启动定时器,回调函数 vTimerCallback 会每秒钟执行一次,打印信息。 xTimerStart(xTimer, 0);// 启动调度器(永远不会返回) vTaskStartScheduler(); // 理论上程序不会执行到这里,但为了安全,可以加一个死循环while(1){ } }
实验现象演示:

夜雨聆风