乐于分享
好东西不私藏

FreeRTOS 软件定时器(Software Timer)

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 闪烁、传感器轮询)
  • 一次性延时执行(超时处理、去抖动)
  • 软件超时机制(如等待应答超时)

二、软件定时器的两种类型

  1. 单次定时器(One-shot Timer)启动后,到期时执行一次回调,然后自动停止。需重新启动才会再次触发。

  2. 周期定时器(Auto-reload Timer / Periodic Timer)启动后,到期时执行回调,并自动重启,以固定周期持续触发。

示例中:

xTimerCreate(..., pdTRUE, ...);   // pdTRUE 表示周期定时器

三、核心 API 函数详解

1. 创建定时器:xTimerCreate

TimerHandle_t xTimerCreateconstchar * 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 会每秒钟执行一次,打印信息。
  • 调度器启动后,系统正常运行,定时器任务会管理定时器队列,在到期时调用回调。

注意事项

  • 需要配置 FreeRTOSconfigUSE_TIMERS 必须设为 1,并且要定义 configTIMER_TASK_PRIORITY(定时器服务任务优先级)、configTIMER_TASK_STACK_DEPTH(堆栈大小)。
  • 回调函数的限制
    • 不允许调用可能会阻塞的 API(如 vTaskDelayxQueueReceive 带非零超时、xSemaphoreTake 等)。
    • 不能调用可能导致阻塞的挂起、删除任务等操作。
    • 应尽量短小,快速执行。因为所有定时器回调都在同一个任务(定时器服务任务)中串行执行,长时间运行会延迟其他定时器的触发。
    • 可以使用 printf(前提是 printf 不阻塞,例如输出到串口时使用中断驱动或非阻塞缓冲)。
  • pvTimerID 为 NULL:如果需要区分多个定时器,可以设置不同的 ID,回调内用 pvcTimerGetTimerID 获取。
  • xTimerStart 的超时为 0:如果定时器命令队列满,xTimerStart 将立即返回 pdFAIL。由于队列通常很空(命令数量有限),风险很小。但严格起见,可以检查返回值并处理失败。

五、软件定时器的内部实现原理

  1. 定时器服务任务(Timer Daemon Task)FreeRTOS 在启动调度器时自动创建一个高优先级的任务(优先级由 configTIMER_TASK_PRIORITY 定义),该任务维护一个定时器列表(按超时时间排序),并调用 xTaskDelayUntil 来实现精确的到期唤醒。
  2. 命令队列用户程序(如 xTimerStart)并不是直接操作定时器数据结构,而是向定时器服务任务发送一个命令。这样可以避免并发访问问题,所有定时器操作都在统一的任务上下文中串行执行。
  3. 到期处理定时器服务任务被唤醒后,检查所有到期的定时器,调用其回调函数,然后如果是周期定时器则重新插入定时器列表。

为什么需要命令队列?

  • 用户任务可能在任何优先级下调用 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. 回调函数中的禁忌

  • 禁止调用vTaskDelayvTaskDelayUntilxQueueReceive(非0超时)、xSemaphoreTake(非0超时)、xEventGroupWaitBits 等可能阻塞的函数。
  • 禁止调用vTaskSuspend 或 vTaskDelete 挂起/删除当前任务。
  • 禁止调用 会间接导致阻塞的函数(如某些打印函数等待互斥量)。
  • 允许使用xQueueSendxSemaphoreGive 等无阻塞的 API(超时设为 0),以及 xTimerChangePeriod 等定时器控制 API(但注意可能再次引发命令队列发送,嵌套深度有限)。

3. 启动调度器前创建定时器

  • 定时器可以在调度器启动前创建。调度器启动后自动创建定时器服务任务并开始处理命令。
  • 如果调度器还未启动,xTimerStart 等函数将延迟生效(实际执行等到服务任务运行)。

4. 定时器的精度

  • 超时时间是你指定的 tick 数,实际精确度受系统 tick 频率影响。例如节拍为 1ms,则定时器周期精度为 ±1ms。
  • 多个定时器的触发时刻可能因为服务任务优先级和系统负载而略有 jitter。

七、与硬件定时器的对比

特性
软件定时器
硬件定时器
资源占用
需要 RTOS 任务和内存
专用硬件外设
数量
理论上不限
受 MCU 外设数量限制
精度
受系统 tick 和任务调度影响(通常 1-10ms 级)
可达微秒或纳秒级
回调上下文
任务上下文
中断上下文
适用场景
非实时、毫秒级以上周期性任务
高精度、实时控制
功耗
增加 CPU 唤醒频率
低功耗唤醒可能更优

八、扩展示例:单次定时器 + 使用 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需要引用哪个头文件?
  • 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){ } }

实验现象演示:

在这里插入图片描述