源码文件: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_AFFINITY | SMP 下守护任务绑核(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_ISR3.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)都只是宏,内部调用这两个函数之一:
xTimerGenericCommandFromTask:xQueueSendToBack(xTimerQueue, &xMessage, xTicksToWait)xTimerGenericCommandFromISR:xQueueSendToBackFromISR(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,否则 configASSERTxAutoReload: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 次回调 →prvReloadTimer以xExpiredTime + 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(比如
xQueueSend、xSemaphoreGive),不必加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 │ └──────────────────────────────────────────────────────────┘