FreeRTOS源码分析八:timer管理(一)
系列文章目录
FreeRTOS源码分析一:task创建(RISCV架构)
FreeRTOS源码分析二:task启动(RISCV架构)
FreeRTOS源码分析三:列表数据结构
FreeRTOS源码分析四:时钟中断处理响应流程
FreeRTOS源码分析五:资源访问控制(一)
FreeRTOS源码分析六:vTaskDelay vs xTaskDelayUntil任务延时
FreeRTOS源码分析七:队列 xQueue
文章目录
- 系列文章目录
- 前言
- Timer 如何使用
- xTimerCreate() API函数
- xTimerStart() API函数
- 示例程序
- 管理 Timer 的数据结构
- Timer 如何管理
- 定时器任务
- xTimerCreate 创建定时器对象
- xTimerStart 启动定时器
- 其余命令类型
- 时钟任务如何处理队列消息
- 小结
- 总结
前言
本文简单先理一下 timer
的管理。涉及一部分非常简单的源码分析。
Timer 如何使用
参考官方文档中的说明,有如下内容
xTimerCreate() API函数
-
xTimerCreate()用于创建软件定时器,并返回一个TimerHandle_t以引用所创建的软件定时器。软件定时器创建时处于休眠状态。
-
xTimerCreate()的函数原型如下:
TimerHandle_t xTimerCreate( const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const BaseType_t xAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction );
-
参数说明
- pcTimerName:定时器的描述性名称,FreeRTOS不使用该名称,仅作为调试辅助,通过人类可读的名称识别定时器比通过句柄识别更简单。
- xTimerPeriodInTicks:定时器的周期,以tick为单位。pdMS_TO_TICKS()宏可用于将以毫秒为单位的时间转换为以tick为单位的时间,该参数不能为0。
- xAutoReload:将xAutoReload设置为pdTRUE可创建自动重载定时器,设置为pdFALSE可创建一次性定时器。
- pvTimerID:每个软件定时器都有一个ID值,该ID是一个void指针,应用程序编写者可将其用于任何目的。当多个软件定时器使用同一个回调函数时,ID特别有用,可用于提供定时器特定的存储。本章中的示例演示了定时器ID的使用。pvTimerID为所创建任务的ID设置初始值。
- pxCallbackFunction:软件定时器回调函数是符合
void ATimerCallback( TimerHandle_t xTimer )
原型的C函数。pxCallbackFunction参数是指向用作所创建软件定时器回调函数的函数(实际上就是函数名)的指针。
-
返回值:如果返回NULL,则无法创建软件定时器,因为FreeRTOS没有足够的堆内存来分配必要的数据结构。如果返回非NULL值,则表示软件定时器已成功创建,返回值是所创建定时器的句柄。第3章提供了更多关于堆内存管理的信息。
xTimerStart() API函数
-
xTimerStart()用于启动处于休眠状态的软件定时器,或重置(重新启动)处于运行状态的软件定时器。xTimerStop()用于停止处于运行状态的软件定时器,停止软件定时器等同于将定时器转换到休眠状态。
-
xTimerStart()的函数原型如下:
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
-
参数说明
- xTimer:要启动或重置的软件定时器的句柄,该句柄是从用于创建软件定时器的xTimerCreate()调用中返回的。
- xTicksToWait:xTimerStart()使用定时器命令队列向守护任务发送“启动定时器”命令。如果队列已满,xTicksToWait指定调用任务应保持在阻塞状态以等待定时器命令队列上出现空间的最长时间。如果xTicksToWait为0且定时器命令队列已满,xTimerStart()将立即返回。阻塞时间以tick周期为单位,因此它所代表的绝对时间取决于tick频率。pdMS_TO_TICKS()宏可用于将以毫秒为单位的时间转换为以tick为单位的时间。如果FreeRTOSConfig.h中的INCLUDE_vTaskSuspend设置为1,则将xTicksToWait设置为portMAX_DELAY将导致调用任务无限期地保持在阻塞状态(没有超时),以等待定时器命令队列中出现空间。
-
返回值
- pdPASS:仅当“启动定时器”命令成功发送到定时器命令队列时才返回pdPASS。如果守护任务的优先级高于调用xTimerStart()的任务的优先级,则调度器将确保在xTimerStart()返回之前处理启动命令,这是因为只要定时器命令队列中有数据,守护任务就会抢占调用xTimerStart()的任务。如果指定了阻塞时间(xTicksToWait不为0),则在函数返回之前,调用任务可能已进入阻塞状态以等待定时器命令队列中出现空间,但在阻塞时间到期之前,数据已成功写入定时器命令队列。
- pdFAIL:如果由于队列已满而无法将“启动定时器”命令写入定时器命令队列,则返回pdFAIL。如果指定了阻塞时间(xTicksToWait不为0),则调用任务将进入阻塞状态以等待守护任务在定时器命令队列中腾出空间,但在这之前指定的阻塞时间已到期。
示例程序
下面是官方 Demo 中使用定时器的一个示例代码:
static void prvQueueSendTimerCallback( TimerHandle_t xTimerHandle )
{const uint32_t ulValueToSend = mainVALUE_SENT_FROM_TIMER;( void ) xTimerHandle;xQueueSend( xQueue, &ulValueToSend, 0U );
}void main_blinky( void )
{const TickType_t xTimerPeriod = mainTIMER_SEND_FREQUENCY_MS;....../* Create the software timer, but don't start it yet. */xTimer = xTimerCreate( "Timer", xTimerPeriod, /* The period of the software timer in ticks. */pdTRUE, /* xAutoReload is set to pdTRUE. */NULL, /* The timer's ID is not used. */prvQueueSendTimerCallback ); /* The function executed when the timer expires. */if( xTimer != NULL ){xTimerStart( xTimer, 0 );}/* Start the tasks and timer running. */vTaskStartScheduler();for( ; ; ){}
}
定时器回调函数会向队列中写入数据。
管理 Timer 的数据结构
核心结构体与全局对象:
Timer_t
(定时器对象)
typedef struct tmrTimerControl {const char * pcTimerName; ListItem_t xTimerListItem; TickType_t xTimerPeriodInTicks; void * pvTimerID; portTIMER_CALLBACK_ATTRIBUTE TimerCallbackFunction_t pxCallbackFunction; uint8_t ucStatus; } xTIMER;typedef xTIMER Timer_t;
- pcTimerName:名字(用于调试)
- xTimerListItem:链表节点,用来挂在“活动定时器链表”里
- xTimerPeriodInTicks:周期(tick 为单位)
- pvTimerID:用户自定义 ID
- pxCallbackFunction:到期的回调函数
- ucStatus:状态位(是否激活/是否自动重装/是否静态创建)
双活动链表(全局,按“到期时间”升序)
xActiveTimerList1、xActiveTimerList2
指针 pxCurrentTimerList / pxOverflowTimerList
:指向“当前周期”和“溢出周期”的两条链
命令队列:xTimerQueue
元素类型 DaemonTaskMessage_t
(见下)
定时器服务任务句柄:xTimerTaskHandle
PRIVILEGED_DATA static List_t xActiveTimerList1;PRIVILEGED_DATA static List_t xActiveTimerList2;PRIVILEGED_DATA static List_t * pxCurrentTimerList;PRIVILEGED_DATA static List_t * pxOverflowTimerList;PRIVILEGED_DATA static QueueHandle_t xTimerQueue = NULL;PRIVILEGED_DATA static TaskHandle_t xTimerTaskHandle = NULL;
命令消息体:DaemonTaskMessage_t
typedef struct tmrTimerQueueMessage{BaseType_t xMessageID; union{TimerParameter_t xTimerParameters;#if ( INCLUDE_xTimerPendFunctionCall == 1 )CallbackParameters_t xCallbackParameters;#endif } u;} DaemonTaskMessage_t;
xMessageID
:命令码(Start/Stop/Reset/ChangePeriod/Delete,以及负数的“挂起函数调用”)
u.xTimerParameters
:{xMessageValue, pxTimer}(常用:xMessageValue是命令发生的时间戳或新周期值)
u.xCallbackParameters
:挂起回调(pxCallbackFunction, pvParameter1, ulParameter2)
Timer 如何管理
FreeRTOS 软件定时器的管理方式是:用户程序通过 API 只负责发命令,真正的定时器对象、到期排序和回调触发,全都由一个专门的服务任务,通过命令队列和双链表集中管理。
定时器任务
FreeRTOS源码分析二:task启动(RISCV架构) 中我们知道,vTaskStartScheduler 调度器启动函数在开启调度之前,会创建定时器任务。
BaseType_t xTimerCreateTimerTask( void )
{return xTaskCreate( &prvTimerTask,configTIMER_SERVICE_TASK_NAME,configTIMER_TASK_STACK_DEPTH,NULL,( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,&xTimerTaskHandle );
}void vTaskStartScheduler( void )
{BaseType_t xReturn;// 创建 Idle Task(空闲任务)xReturn = prvCreateIdleTasks();// 启用了软件定时器(configUSE_TIMERS)#if ( configUSE_TIMERS == 1 ){if( xReturn == pdPASS ){// 创建 Timer Service Task。xReturn = xTimerCreateTimerTask();}}#endif /* configUSE_TIMERS */......
}
定时器任务处理函数的死循环做很简单的事情:
- 找最近到期的定时器,等待直到这个定时器到期
- 处理到期定时器,若为一次性定时器 → 调回调后标记为非激活,若为周期性定时器 → 重新计算下次到期时间,插回链表,每次到期都会调用用户注册的回调函数。
- 清空命令队列:按消息类型修改链表或执行挂起回调
static portTASK_FUNCTION( prvTimerTask, pvParameters ){TickType_t xNextExpireTime;BaseType_t xListWasEmpty;/* Just to avoid compiler warnings. */( void ) pvParameters;for( ; configCONTROL_INFINITE_LOOP(); ){xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );prvProcessReceivedCommands();}}
xTimerCreate 创建定时器对象
TimerHandle_t xTimerCreate( const char * const pcTimerName,const TickType_t xTimerPeriodInTicks,const BaseType_t xAutoReload,void * const pvTimerID,TimerCallbackFunction_t pxCallbackFunction ){Timer_t * pxNewTimer;pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) );if( pxNewTimer != NULL ){pxNewTimer->ucStatus = 0x00;prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, xAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer );}return pxNewTimer;}
xTimerCreate
只做对象分配与初始化 不会发命令。
真正让定时器生效,要调用 xTimerStart()
,此时才会通过 xTimerQueue
发送一个 Start
命令 (tmrCOMMAND_START)
给定时器服务任务。
服务任务取到命令后才会把 Timer_t
的 xTimerListItem
挂到 pxCurrentTimerList
中,开始计时。
xTimerStart 启动定时器
xTimerStart 调用 xTimerGenericCommand 向请求队列末尾加入 START 命令
#define xTimerStart( xTimer, xTicksToWait ) \xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
BaseType_t xTimerGenericCommandFromTask( TimerHandle_t xTimer,const BaseType_t xCommandID,const TickType_t xOptionalValue,BaseType_t * const pxHigherPriorityTaskWoken,const TickType_t xTicksToWait ){BaseType_t xReturn = pdFAIL;DaemonTaskMessage_t xMessage;( void ) pxHigherPriorityTaskWoken;if( ( xTimerQueue != NULL ) && ( xTimer != NULL ) ){/* Send a command to the timer service task to start the xTimer timer. */xMessage.xMessageID = xCommandID;xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;xMessage.u.xTimerParameters.pxTimer = xTimer;configASSERT( xCommandID < tmrFIRST_FROM_ISR_COMMAND );if( xCommandID < tmrFIRST_FROM_ISR_COMMAND ){if( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING ){xReturn = xQueueSendToBack( xTimerQueue, &xMessage, xTicksToWait );}else{xReturn = xQueueSendToBack( xTimerQueue, &xMessage, tmrNO_DELAY );}}}return xReturn;}
简单来说,我们要构造一个类型为 DaemonTaskMessage_t
的 message
,设置好 message
的属性之后把 message
加入时钟任务的处理队列 xTimerQueue
之中,等待其处理。
其余命令类型
xTimerDelete()
API 函数可删除定时器。软件定时器的周期通过 xTimerChangePeriod()
函数更改。定时器通过 xTimerReset()
API 函数进行重置。
#define xTimerReset( xTimer, xTicksToWait ) \xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )#define xTimerStop( xTimer, xTicksToWait ) \xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP, 0U, NULL, ( xTicksToWait ) )#define xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait ) \xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD, ( xNewPeriod ), NULL, ( xTicksToWait ) )#define xTimerDelete( xTimer, xTicksToWait ) \xTimerGenericCommand( ( xTimer ), tmrCOMMAND_DELETE, 0U, NULL, ( xTicksToWait ) )
可以看到,其余命令也是通过构造发送不同类型命令到命令队列。
时钟任务如何处理队列消息
循环处理命令队列中每一条数据,立马处理
static void prvProcessReceivedCommands( void ){while( xQueueReceive( xTimerQueue, &xMessage, tmrNO_DELAY ) != pdFAIL ){switch( xMessage.xMessageID ){case tmrCOMMAND_START:case tmrCOMMAND_START_FROM_ISR:case tmrCOMMAND_RESET:case tmrCOMMAND_RESET_FROM_ISR:......break;case tmrCOMMAND_STOP:case tmrCOMMAND_STOP_FROM_ISR:......break;case tmrCOMMAND_CHANGE_PERIOD:case tmrCOMMAND_CHANGE_PERIOD_FROM_ISR:......break;case tmrCOMMAND_DELETE:......}}
我们现在不看具体怎么处理,只看整体逻辑。
- 定时器服务任务在每轮主循环尾部调用
prvProcessReceivedCommands()
,其职责是把命令队列里的消息一次性“清空”。 while (xQueueReceive(xTimerQueue, &xMessage, tmrNO_DELAY) != pdFAIL) { ... }
使用#define tmrNO_DELAY 0
,表示不等待,把此刻所有可读消息全部处理完。- 随后根据
xMessageID
分派,处理
小结
FreeRTOS 的 软件定时器 本质上是一个由 内核任务统一管理 的“回调触发器”。它和硬件定时器不同,不直接依赖外设,而是利用系统的 tick 中断 与 调度机制 来实现。整体架构有几个关键点:
-
用户接口简单
- 通过
xTimerCreate()
创建定时器对象(只是初始化,还未运行); - 调用
xTimerStart()
、xTimerStop()
、xTimerReset()
、xTimerChangePeriod()
、xTimerDelete()
等 API 发送命令。 - 应用层完全不需要操作链表或管理定时器状态,只需发命令。
- 通过
-
核心数据结构
- 每个定时器对应一个
Timer_t
结构,包含名字、周期、回调函数、ID 等信息; - 内核维护两条活动链表
xActiveTimerList1
、xActiveTimerList2
,配合pxCurrentTimerList
和pxOverflowTimerList
实现 tick 溢出处理; - 所有 API 命令通过统一的 命令队列
xTimerQueue
传递给服务任务,避免多任务直接操作链表引发并发问题。
- 每个定时器对应一个
-
服务任务统一管理
-
定时器服务任务是一个普通任务(优先级可配),循环执行:
- 找出下一个即将到期的定时器 → 阻塞等待;
- 定时器到期 → 执行用户回调,并根据是否自动重载决定是否重新入链;
- 清空命令队列 → 按消息类型修改链表或执行挂起函数。
-
因为所有定时器都在这个任务里集中处理,所以逻辑清晰、避免竞争,但也意味着回调代码必须尽量轻量,避免拖慢整个定时器系统。
-
-
命令驱动机制
- API 只是构造消息,真正逻辑由服务任务完成;
- 不同命令(Start、Stop、Reset、ChangePeriod、Delete、PendFunctionCall)对应不同的处理分支;
- ISR 场景下可使用
*_FROM_ISR
变体,保证安全性。
FreeRTOS 软件定时器的设计哲学是 “命令队列 + 服务任务 + 双链表”:
应用层只管发命令,服务任务统一接管所有定时器的调度、到期和回调触发。这样既保证了线程安全,又让用户 API 简单清晰。
总结
完结撒花!!!