FreeRTOS 的软件定时器(Software Timer)实现基于任务调度和队列机制,其核心设计围绕 守护任务(Timer Daemon Task) 和 命令队列(Timer Command Queue) 展开。
一、软件定时器的核心结构
1. 定时器控制块(Timer_t)
在 timers.c
中,定时器通过 Timer_t
结构体管理:
typedef struct tmrTimerControl {
const char *pcTimerName; // 定时器名称(调试用)
ListItem_t xTimerListItem; // 链表项,用于插入定时器列表
TickType_t xTimerPeriodInTicks; // 定时周期(以系统节拍为单位)
void *pvTimerID; // 用户自定义ID
TimerCallbackFunction_t pxCallbackFunction; // 回调函数
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTimerNumber; /**< An ID assigned by trace tools such as FreeRTOS+Trace */
#endif
uint8_t ucStatus; // 状态位(激活/静态分配/自动重载)
} Timer_t;
- ucStatus:通过位掩码管理定时器状态(如
tmrSTATUS_IS_ACTIVE
表示定时器已激活)。
2. 定时器列表管理
双列表设计:xActiveTimerList1
和 xActiveTimerList2
用于处理系统节拍溢出。
static List_t xActiveTimerList1, xActiveTimerList2;
static List_t *pxCurrentTimerList = &xActiveTimerList1;
static List_t *pxOverflowTimerList = &xActiveTimerList2;
- 溢出处理:当系统节拍计数器溢出时,切换当前列表和溢出列表(
prvSwitchTimerLists
)。
二、守护任务(Timer Daemon Task)
1. 任务入口函数 prvTimerTask
守护任务的核心逻辑:
static void prvTimerTask(void *pvParameters) {
TickType_t xNextExpireTime;
BaseType_t xListWasEmpty;
for (;;) {
// 1. 获取下一个定时器的到期时间
xNextExpireTime = prvGetNextExpireTime(&xListWasEmpty);
// 2. 处理到期定时器或阻塞等待
prvProcessTimerOrBlockTask(xNextExpireTime, xListWasEmpty);
// 3. 处理来自队列的命令(如启动/停止定时器)
prvProcessReceivedCommands();
}
}
2. 关键操作流程
- 步骤 1:获取下一个到期时间
通过prvGetNextExpireTime
从当前活动列表中获取最近到期时间。 - 步骤 2:处理到期或阻塞
- 如果定时器到期,调用
prvProcessExpiredTimer
处理回调。 - 如果无到期定时器,通过
vQueueWaitForMessageRestricted
阻塞等待命令或超时。
- 如果定时器到期,调用
- 步骤 3:处理命令
从命令队列xTimerQueue
中读取并执行操作(如启动、停止、修改周期)。
三、命令队列与定时器操作
1. 命令队列 (xTimerQueue
)
- 作用:所有定时器操作(如
xTimerStart()
、xTimerStop()
)通过发送命令到队列,由守护任务统一处理。 - 命令结构体:
DaemonTaskMessage_t
封装命令类型和参数。typedef struct tmrTimerQueueMessage { BaseType_t xMessageID; // 命令类型(如 tmrCOMMAND_START) union { TimerParameter_t xTimerParameters; // 定时器相关参数 CallbackParameters_t xCallbackParameters; // 回调函数参数 } u; } DaemonTaskMessage_t;
2. 定时器启动流程(以 xTimerStart
为例)
- 用户调用
xTimerStart()
生成DaemonTaskMessage_t
消息,发送到xTimerQueue
。 - 守护任务接收消息
在prvProcessReceivedCommands
中解析命令,执行以下操作:- 将定时器插入活动列表(
prvInsertTimerInActiveList
)。 - 如果定时器已到期,立即触发回调。
- 将定时器插入活动列表(
四、定时器到期处理
1. 处理到期定时器 (prvProcessExpiredTimer
)
static void prvProcessExpiredTimer(TickType_t xNextExpireTime, TickType_t xTimeNow) {
Timer_t *pxTimer = (Timer_t *)listGET_OWNER_OF_HEAD_ENTRY(pxCurrentTimerList);
// 1. 从活动列表中移除定时器
uxListRemove(&pxTimer->xTimerListItem);
// 2. 如果是自动重载定时器,重新计算到期时间并插入列表
if (pxTimer->ucStatus & tmrSTATUS_IS_AUTORELOAD) {
prvReloadTimer(pxTimer, xNextExpireTime, xTimeNow);
} else {
pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE; // 标记为非激活
}
// 3. 执行回调函数
pxTimer->pxCallbackFunction((TimerHandle_t)pxTimer);
}
2. 自动重载逻辑 (prvReloadTimer
)
- 计算新的到期时间:
xNextExpireTime + xTimerPeriodInTicks
。 - 将定时器重新插入活动列表,处理可能的溢出情况。
五、关键设计细节
1. 系统节拍溢出处理
- 双列表切换:当检测到节拍计数器溢出(
xTimeNow < xLastTime
),调用prvSwitchTimerLists
切换当前列表和溢出列表。 - 溢出列表作用:存放因节拍溢出无法立即处理的定时器。
2. 中断安全操作
- 中断中操作定时器:使用
xTimerStartFromISR
或xTimerResetFromISR
,通过xQueueSendFromISR
发送命令到队列。
3. 静态与动态分配
- 静态分配:通过
xTimerCreateStatic
使用预分配的StaticTimer_t
内存。 - 动态分配:通过
xTimerCreate
动态申请内存,需在删除时释放。
六、常见问题
回调函数为何不能阻塞?
回调在守护任务上下文中执行,阻塞会导致其他定时器无法及时处理。定时器精度如何保证?
依赖系统节拍频率(configTICK_RATE_HZ
),提高频率可提升精度,但增加系统负载。命令队列溢出如何处理?
需合理设置configTIMER_QUEUE_LENGTH
,并在发送命令时检查返回值。
总结
FreeRTOS 的软件定时器通过 守护任务 和 命令队列 实现异步管理,其核心设计包括:
- 双列表处理节拍溢出:确保长时间运行下的稳定性。
- 线程安全的命令队列:所有操作由守护任务统一处理。
- 灵活的回调机制:支持单次、周期定时及动态修改。
通过分析源码,可以深入理解定时器的生命周期管理、命令处理流程及底层数据结构设计。