ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FreeRTOS学习(六)- 软件定时器模块详解

FreeRTOS学习(六)- 软件定时器模块详解 源码文件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.cconfigTIMER_TASK_PRIORITY定时器守护任务的优先级建议设为最高或较高否则到期回调会被其他任务拖延configTIMER_QUEUE_LENGTH命令队列的容量至少 ≥ 你计划同时并发的操作数configTIMER_TASK_STACK_DEPTH守护任务栈大小与回调中使用的栈深度相关默认通常 128~256 字configTIMER_SERVICE_TASK_NAME守护任务名字默认 Tmr SvcconfigUSE_DAEMON_TASK_STARTUP_HOOK启用vApplicationDaemonTaskStartupHook()回调INCLUDE_xTimerPendFunctionCall启用挂起函数到守护任务执行功能configTIMER_SERVICE_TASK_CORE_AFFINITYSMP 下守护任务绑核V11 新增三、核心数据结构3.1 Timer_t定时器控制块定义timers.c#L83-L94typedef 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 值不是剩余 tickucStatus的 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-L150static 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-L837vTaskSuspendAll() // 挂起调度器防其他任务改表 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 prvSwitchTimerListstick 溢出处理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 发送侧xTimerGenericCommandFromTask / FromISR见 timers.c#L448-L536。所有对外 APIxTimerStart/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/RESETxTaskGetTickCount()命令发送时的 tick记为 xCommandTime后续用于计算下一次到期值CHANGE_PERIOD 新周期xNewPeriod5.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否则 configASSERTxAutoReloadpdTRUE 周期型pdFALSE 单次型pvTimerID用户 ID回调里通过pvTimerGetTimerID(xTimer)获取pxCallbackFunction回调签名void f(TimerHandle_t xTimer)返回 TimerHandle_topaque 指针创建完成后处于休眠态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 (PconfigTIMER_TASK_PRIORITY) │ │ │ t0 xTimerCreate(My,100,TRUE, │ │ id, vMyCallback) │ │ → malloc Timer_t, 初始化ucStatus4 │ │ │ │ t5 xTimerStart(h, 0) │ │ → xTimerGenericCommand │ │ Send msg (IDSTART, val5, pxTimer)│ │ 入队到 xTimerQueue ──────────────►│ │ │ │ Tmr 从阻塞醒来 │ [prvProcessReceivedCommands] │ - 摘出 xTimerListItem不在任何列表忽略 │ - ucStatus | ACTIVE (0x01 → 0x05) │ - xNextExpiry 5 100 105 │ - 插入 pxCurrentTimerList按 105 排序 │ │ t105 SysTicktickCount105 │ │ prvGetNextExpireTime 105 │ prvProcessTimerOrBlockTask: 105105 是 │ prvProcessExpiredTimer: │ 摘出 xTimerListItem │ AUTO_RELOAD → prvReloadTimer │ 重插 NextExpiry 105100205 │ 调用 vMyCallback(xTimer) ←★ 用户回调 │ │ t120 某个 ISR 触发 │ │ xTimerStopFromISR(h, xHPW) │ │ → Send (IDSTOP_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 │ │ ├─ ID0: 调用 pxCallbackFunction (PendFunction) │ │ └─ ID0: switch START/STOP/RESET/CHANGE/DELETE │ │ → 改 Timer_t.ucStatus │ │ → prvInsertTimerInActiveList │ │ ↘ pxCurrentTimerList (按xNextExpiry升序)│ │ ↘ pxOverflowTimerList │ └──────────────────────────────────────────────────────────┘
返回列表