FreeRTOS学习(六)- 软件定时器模块详解
2026/8/25 11:42:21 网站建设 项目流程

源码文件:timers.c + 头文件 timers.h 启用条件:configUSE_TIMERS == 1


一、设计哲学:为什么是"软件定时器"?

硬件定时器数量少、不统一且绑定特定芯片。FreeRTOS 软件定时器基于 SysTick 的节拍计数 + 一条守护任务(Timer Service/Daemon Task)+ 一条命令队列实现,特点:

  • 数量不受限:只要堆/静态内存够,就能创建
  • 精度为 1 个 tick(非硬件级高精度,对大多数 LED 闪烁、超时检测、周期任务调度足够)
  • 所有回调运行在同一个守护任务的上下文中(不是 ISR,回调里可以调用非 FromISR 的 FreeRTOS API,但要注意阻塞行为)
  • 线程安全:用户任务和 ISR 通过发送命令到队列来操作定时器,而非直接修改定时器结构

二、关键配置宏(FreeRTOSConfig.h)

含义
configUSE_TIMERS必须 1 才编译 timers.c
configTIMER_TASK_PRIORITY定时器守护任务的优先级(建议设为最高或较高,否则到期回调会被其他任务拖延)
configTIMER_QUEUE_LENGTH命令队列的容量,至少 ≥ 你计划同时并发的操作数
configTIMER_TASK_STACK_DEPTH守护任务栈大小,与回调中使用的栈深度相关(默认通常 128~256 字)
configTIMER_SERVICE_TASK_NAME守护任务名字,默认 "Tmr Svc"
configUSE_DAEMON_TASK_STARTUP_HOOK启用vApplicationDaemonTaskStartupHook()回调
INCLUDE_xTimerPendFunctionCall启用"挂起函数到守护任务执行"功能
configTIMER_SERVICE_TASK_CORE_AFFINITYSMP 下守护任务绑核(V11 新增)

三、核心数据结构

3.1 Timer_t(定时器控制块)

定义:timers.c#L83-L94

typedef struct tmrTimerControl { const char *pcTimerName; // 调试用字符串名 ListItem_t xTimerListItem; // 挂入 pxCurrentTimerList/OverflowList 的节点 // xItemValue = 下次到期的绝对 tick 值 TickType_t xTimerPeriodInTicks; // 周期(tick 数) void *pvTimerID; // 用户自定义 ID,同一回调多个定时器时用来区分 TimerCallbackFunction_t pxCallbackFunction; // 到期回调函数指针 UBaseType_t uxTimerNumber; // trace 用(可选) uint8_t ucStatus; // 位标志: // BIT0 tmrSTATUS_IS_ACTIVE (0x01) 是否运行 // BIT1 tmrSTATUS_IS_STATIC... (0x02) 是否静态分配 // BIT2 tmrSTATUS_IS_AUTORELOAD(0x04) 是否自动重载 } Timer_t;

说明

  • xTimerListItem.xItemValue存的是到期的绝对 tick 值,不是"剩余 tick"
  • ucStatus的 3 个 bit 同时编码了状态、分配方式、是否自动重载

3.2 命令消息 DaemonTaskMessage_t

定义:timers.c#L122-L135

用户任务通过xTimerQueue 队列发命令给守护任务。消息结构:

typedef struct tmrTimerQueueMessage { BaseType_t xMessageID; // 命令 ID。负号 = 回调执行;正号 = 定时器操作 union { TimerParameter_t xTimerParameters; // .xMessageValue(可选值) + .pxTimer CallbackParameters_t xCallbackParameters; // PendFunctionCall 专用 } u; } DaemonTaskMessage_t;

命令 ID 定义见 timers.h#L55-L68:

负值(Pend 回调类): -2 tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR -1 tmrCOMMAND_EXECUTE_CALLBACK 正值(任务/ISR 操作类): 1-5 任务上下文版本 tmrCOMMAND_START / RESET / STOP / CHANGE_PERIOD / DELETE 6-9 ISR 上下文版本 tmrCOMMAND_START_FROM_ISR / RESET_FROM_ISR / STOP_FROM_ISR / CHANGE_PERIOD_FROM_ISR

3.3 全局变量

见 timers.c#L143-L150:

static List_t xActiveTimerList1; // 双链表:当前 tick 计数域的到期表(按 xNextExpiryTime 升序) static List_t xActiveTimerList2; // 双链表:溢出时 tick 计数域的到期表 static List_t *pxCurrentTimerList; // → 指向上面其中之一 static List_t *pxOverflowTimerList; // → 指向另一个 static QueueHandle_t xTimerQueue = NULL; // 命令队列 static TaskHandle_t xTimerTaskHandle = NULL; // 守护任务句柄

双列表用于解决 tick 溢出(类似任务调度的延迟双表)。后面详细说明。


四、守护任务主循环

入口:prvTimerTask()

static portTASK_FUNCTION( prvTimerTask, pvParameters ) { for( ;; ) { // 第 1 步:找下一个最先到期的定时器 xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty ); // 第 2 步:如果到点就处理;否则阻塞在命令队列上直到"下一次到期"或"有新命令" prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty ); // 第 3 步:一次性抽空命令队列(用户发的 Start/Stop/Reset/Delete 等) prvProcessReceivedCommands(); } }

注意三步顺序先处理到期定时器 → 再处理命令队列。因为命令会改变定时器状态,而到期处理需要基于"命令还未被处理前"的状态。这个循环设计极其关键,保证每一个 tick 后最先检查到到期的定时器,最少一个周期延迟触发回调。

4.1 prvGetNextExpireTime:取下一个到期点

见 timers.c#L840-L864

定时器的xTimerListItem已按到期绝对时间升序插入(vListInsert按 itemValue 升序),所以直接取表头即可 O(1) 得到最近到期时间。如果当前表为空,返回 0(等待 tick 溢出)。

4.2 prvProcessTimerOrBlockTask:处理到期 or 阻塞

见 timers.c#L778-L837

vTaskSuspendAll() // 挂起调度器(防其他任务改表) xTimeNow = prvSampleTimeNow(...) // 顺便检测是否发生 tick 溢出 → 是则切换表+清到期项 if (未溢出 && 当前有到期定时器) { xTaskResumeAll(); prvProcessExpiredTimer(next, now); // 处理它(删表、重装载、回调) } else { // 没到期 → 以 xNextExpireTime - xTimeNow 为阻塞上限 // 调用 vQueueWaitForMessageRestricted(xTimerQueue, diff, empty) // → 该 API 在关调度下等消息/超时,不会引起调度(因调度器仍挂起) xTaskResumeAll(); 若 ResumeAll 未触发 yield → taskYIELD_WITHIN_API() 让任务真正进入 Blocked }

这里用了vQueueWaitForMessageRestricted而不是普通xQueueReceive,因为它在调度器挂起期间就能判断"队列非空 / 超时到否",避免因挂起调度器无法阻塞的尴尬。

4.3 prvSampleTimeNow + prvSwitchTimerLists:tick 溢出处理

prvSampleTimeNow:取xTaskGetTickCount()并与上次xLastTime比较。若now < last说明TickType_t发生了回绕(比如 32-bit 从 0xFFFFFFFF → 0x00000000),此时必须:

prvSwitchTimerLists()

while pxCurrentTimerList 非空: 把 pxCurrentTimerList 上的定时器逐个当作已过期 → prvProcessExpiredTimer(, tmrMAX_TIME_BEFORE_OVERFLOW) swap(pxCurrentTimerList, pxOverflowTimerList)

为什么要两张表?

  • 创建定时器时:若xNextExpiryTime = xCommandTime + xPeriodInTicks计算出的绝对时间小于xCommandTime,说明它跨过了下一次回绕点→ 放入pxOverflowTimerList
  • 否则放入pxCurrentTimerList
  • 当 tick 真的回绕时(由 prvSampleTimeNow 检测),两个表互换角色:overflow 表变成 current 表,current 表清空作为下一轮 overflow 表。

这套机制与 tasks.c 中pxDelayedTaskList / pxOverflowDelayedTaskList双表互换思想完全一致。


五、定时器命令的发送与处理

5.1 发送侧:xTimerGenericCommand(FromTask / FromISR)

见 timers.c#L448-L536。所有对外 API(xTimerStart/Stop/Reset/ChangePeriod/Delete)都只是宏,内部调用这两个函数之一:

  • xTimerGenericCommandFromTaskxQueueSendToBack(xTimerQueue, &xMessage, xTicksToWait)
  • xTimerGenericCommandFromISRxQueueSendToBackFromISR(xTimerQueue, ...)

例如(timers.h 中):

#define xTimerStart( xTimer, xTicksToWait ) \ xTimerGenericCommand( xTimer, tmrCOMMAND_START, xTaskGetTickCount(), NULL, xTicksToWait )

关键参数xOptionalValue的含义随命令而变:

  • START/RESET:=xTaskGetTickCount()(命令发送时的 tick,记为 xCommandTime),后续用于计算下一次到期值
  • CHANGE_PERIOD:= 新周期xNewPeriod

5.2 处理侧:prvProcessReceivedCommands

见 timers.c#L934-L1086

循环 xQueueReceive(xTimerQueue, ..., tmrNO_DELAY) 直到空: 1) 若 xMessageID < 0 → 是 PendFunctionCall(非定时器命令) 直接调用 pxCallback->pxCallbackFunction(pvParameter1, ulParameter2) 2) 若 xMessageID >= 0 → 是定时器操作命令 pxTimer = xTimerParameters.pxTimer 若 pxTimer->xTimerListItem 正在某个链表中 → 先 uxListRemove 摘出 xTimeNow = prvSampleTimeNow(...) switch (xMessageID): ┌── START / RESET / _FROM_ISR ──┐ │ pxTimer->ucStatus |= ACTIVE │ │ xNextExpiry = xMessageValue │ │ (xCommandTime) + xPeriod │ │ if prvInsertTimerInActiveList │ │ 返回"已经过期要立刻处理": │ │ 自动重载 → prvReloadTimer │ │ 单次 → 清 ACTIVE │ │ 直接 pxCallbackFunction() │ └────────────────────────────────┘ ┌── STOP / _FROM_ISR ──┐ │ ucStatus &= ~ACTIVE │ │ (之前已从列表移除)│ └──────────────────────┘ ┌── CHANGE_PERIOD / _FROM_ISR ──┐ │ ucStatus |= ACTIVE │ │ xTimerPeriodInTicks = newValue│ │ 以 xTimeNow 为命令时间重插 │ └────────────────────────────────┘ ┌── DELETE ───────────────────────┐ │ 动态创建的:vPortFree(pxTimer) │ │ 静态创建的:清 ACTIVE │ └─────────────────────────────────┘

5.3 prvInsertTimerInActiveList:插入到正确的"到期表"

见 timers.c#L890-L931

核心判断:

xNextExpiryTime <= xTimeNow ? ├─ YES:"理论上已过期" │ if (xTimeNow - xCommandTime) >= Period → 已经晚了超过一个周期 → return pdTRUE 让调用方立即执行回调 │ else → 放到 pxOverflowTimerList 中(因为它本来就应该在"下一个 tick 域"到期) └─ NO:尚未过期 if (xTimeNow < xCommandTime 且 xNextExpiry >= xCommandTime) → 命令发出到处理之间发生过溢出 → 立即执行回调 else → 正常放入 pxCurrentTimerList(按 itemValue 升序插)

这种"命令发出 → 命令实际被处理之间有延迟"的容错,是守护任务+命令队列模式下不可或缺的防漂移。

5.4 prvProcessExpiredTimer / prvReloadTimer:到期执行与自动重装载

prvProcessExpiredTimer()

从 pxCurrentTimerList 表头摘除 pxTimer if AUTO_RELOAD: prvReloadTimer(pxTimer, xNextExpireTime, xTimeNow) else: ucStatus &= ~ACTIVE 调用 pxTimer->pxCallbackFunction(xTimer) ← 用户回调!

prvReloadTimer()处理周期性定时器的"掉帧补偿"

while (prvInsertTimerInActiveList(pxTimer, xExpiredTime + Period, xTimeNow, xExpiredTime)) { xExpiredTime += Period; // 再加一个周期 pxTimer->pxCallbackFunction(pxTimer); // 补回调(即使到期时间已经过,也按"错过多少次补多少次") }

也就是说,如果守护任务因为被更高优先级任务抢占、错过了 N 个周期,当它再运行时会严格补齐 N 次回调。这一点与vTaskDelayUntil的语义相似。

⚠️注意:因为所有回调都运行在同一个守护任务的上下文,若某个回调长时间阻塞或执行过久,其他定时器的回调会被整体拖延。所以定时器回调应当短小、非阻塞(就像 ISR 一样的风格)。


六、创建、启动、自动重载 vs 单次

6.1 xTimerCreate / xTimerCreateStatic

见 timers.c#L336-L445

  • 参数:
    • xTimerPeriodInTicks:周期,必须 > 0,否则 configASSERT
    • xAutoReload:pdTRUE 周期型;pdFALSE 单次型
    • pvTimerID:用户 ID,回调里通过pvTimerGetTimerID(xTimer)获取
    • pxCallbackFunction:回调,签名void f(TimerHandle_t xTimer)
  • 返回 TimerHandle_t(opaque 指针)
  • 创建完成后处于休眠态ucStatus的 ACTIVE 位为 0,不挂入任何 active list),必须 xTimerStart 才启动

6.2 单次 vs 自动重载

项目xAutoReload = pdFALSE(单次)xAutoReload = pdTRUE(周期)
到达到期时间执行 1 次回调 → 清 ACTIVE → 不挂回任何表 → 除非重新 Start 否则不再触发执行 1 次回调 →prvReloadTimerxExpiredTime + Period为基准重新插入 → 继续
补回调N/A(只会触发 1 次)若被抢占导致到期过去多次,while 循环补对应次数,保证长期平均频率正确

七、附加能力:PendFunctionCall(在守护任务上下文中执行任意函数)

INCLUDE_xTimerPendFunctionCall == 1时可调用:

BaseType_t xTimerPendFunctionCall(PendedFunction_t xFunctionToPend, void *pvParameter1, uint32_t ulParameter2, TickType_t xTicksToWait); BaseType_t xTimerPendFunctionCallFromISR(...);

实现方式:向 xTimerQueue 发一条xMessageID = -1 或 -2的消息,prvProcessReceivedCommands在收到负值 ID 时直接在此处调用用户给出的函数。这提供了一个"把任意函数的执行环境 defer 到守护任务"的便捷模式,典型用途:

  • ISR 中把耗时处理推到线程模式(因为 ISR 里不能阻塞)
  • ISR 中调用非 FromISR 的 FreeRTOS API 不方便,全部 defer 到这里

⚠️ 当然,也因此和定时器回调共享同一个执行线程:Pend 的函数跑得久,定时器同样被拖延。


八、命令/回调执行全流程示例

以"创建周期 100 tick 自动重载定时器 → Start → ISR 中 Stop"为例:

用户任务 H 守护任务 Tmr Svc (P=configTIMER_TASK_PRIORITY) │ │ │ t=0 xTimerCreate("My",100,TRUE, │ │ &id, vMyCallback) │ │ → malloc Timer_t, 初始化,ucStatus=4 │ │ │ │ t=5 xTimerStart(h, 0) │ │ → xTimerGenericCommand │ │ Send msg (ID=START, val=5, pxTimer)│ │ 入队到 xTimerQueue ──────────────►│ │ │ │ Tmr 从阻塞醒来: │ [prvProcessReceivedCommands] │ - 摘出 xTimerListItem(不在任何列表忽略) │ - ucStatus |= ACTIVE (0x01 → 0x05) │ - xNextExpiry = 5 + 100 = 105 │ - 插入 pxCurrentTimerList(按 105 排序) │ │ t=105 SysTick,tickCount=105 │ │ prvGetNextExpireTime = 105 │ prvProcessTimerOrBlockTask: 105<=105 是 │ prvProcessExpiredTimer: │ 摘出 xTimerListItem │ AUTO_RELOAD → prvReloadTimer │ 重插 NextExpiry = 105+100=205 │ 调用 vMyCallback(xTimer) ←★ 用户回调 │ │ t=120 某个 ISR 触发 │ │ xTimerStopFromISR(h, &xHPW) │ │ → Send (ID=STOP_FROM_ISR,...) 入队 │ │ 若队列满则失败 │ │ 设置 xHPW 指示是否需要 yield │ │ prvProcessReceivedCommands: │ - 摘出 xTimerListItem (在 pxCurrentTimerList) │ - ucStatus &= ~ACTIVE → 0x04

九、实现边界与常见陷阱

1. 回调运行上下文的"错觉"

回调不是 ISR,是守护任务的线程模式。因此:

  • 可以调用任何 FreeRTOS API(比如xQueueSendxSemaphoreGive),不必加FromISR
  • 但绝不能阻塞太久(更不能vTaskDelay或拿不到信号量地等),否则其他定时器/PendFunctionCall 全被耽误
  • 若回调里要执行大计算/阻塞,建议从回调发消息/信号量给一个独立的 Worker 任务去做

2. 优先级的影响

定时器守护任务的优先级configTIMER_TASK_PRIORITY直接决定回调准时性

  • 若低于应用中的其他 CPU 密集任务,则回调总是被延后
  • 经验建议:至少设为和最关键的被调度任务同级或更高;若回调很轻,干脆设为系统最高

3. xTimerQueue 满

所有 Start/Stop/Reset/Delete/PendFunctionCall 都要占用一条队列项。configTIMER_QUEUE_LENGTH过小的典型症状:高并发操作时某些 API 失败(pdFAIL)。建议按"同一时刻潜在并发的命令数"估。

4. tick 溢出的双表机制

不理解双表时,常见"定时器明明设置周期 X,但在 tick wrap 附近表现异常"。FreeRTOS 的解决方式是把 tick 计数划分成"未溢出域"和"溢出域"两张有序表,计数回绕时互换。这和 xTaskIncrementTick 中的taskSWITCH_DELAYED_LISTS同一个思路。

5. 从 ISR 中调用 xTimerStart/Stop

可以,但只能用xxxFromISR版本;且互斥锁不能给 FromISR(定时器内部不使用互斥锁,而是通过命令队列串行化,所以安全)。

6. 最大回调数限制

如果定时器守护任务优先级低,N 个定时器同时到期时它们的回调顺序是"按到期时间排序",不会并发执行。不要依赖多个定时器回调之间的精确先后顺序。


十、定时器模块的组件关系总览

应用任务 / ISR │ xTimerStart() / xTimerStop() / xTimerReset() ... │ xTimerPendFunctionCall() / FromISR ▼ ┌─────────────────────────────────────────────────────────┐ │ xTimerQueue (Queue_t, 先进先出) │ │ 消息 = DaemonTaskMessage_t { xMessageID + union } │ └─────────────────────────────────────────────────────────┘ │ xQueueReceive (在守护任务里) ▼ ┌──────────────────────────────────────────────────────────┐ │ prvTimerTask (守护任务) │ │ │ │ 1. prvGetNextExpireTime → pxCurrentTimerList 表头 │ │ 2. prvProcessTimerOrBlockTask │ │ └─ 到点 → prvProcessExpiredTimer → 用户回调 │ │ └─ 不到点 → 阻塞在 xTimerQueue (上限=下一次到期) │ │ 3. prvProcessReceivedCommands │ │ ├─ ID<0: 调用 pxCallbackFunction (PendFunction) │ │ └─ ID>0: switch START/STOP/RESET/CHANGE/DELETE │ │ → 改 Timer_t.ucStatus │ │ → prvInsertTimerInActiveList │ │ ↘ pxCurrentTimerList (按xNextExpiry升序)│ │ ↘ pxOverflowTimerList │ └──────────────────────────────────────────────────────────┘

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询