文章目录
- 前言
- 一、软件定时器结构体
- 二、软件定时器的工作机制
- 三、创建软件定时器
- 四、启动软件定时器
- 五、软件定时器如何知道什么时候被调用
- 总结
前言
除了有硬件定时器,还有软件定时器,那么这篇文章将带大家学习一下软件定时器是如何工作的,以及分析软件定时器的内部源码。
一、软件定时器结构体
软件定时器的本质其实也是一个结构体,在FreeRTOS中会使用一个结构体来管理软件定时器。
软件定时器结构体:
typedef struct tmrTimerControl /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
const char * pcTimerName; /*<< Text name. This is not used by the kernel, it is included simply to make debugging easier. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
ListItem_t xTimerListItem; /*<< Standard linked list item as used by all kernel features for event management. */
TickType_t xTimerPeriodInTicks; /*<< How quickly and often the timer expires. */
void * pvTimerID; /*<< An ID to identify the timer. This allows the timer to be identified when the same callback is used for multiple timers. */
TimerCallbackFunction_t pxCallbackFunction; /*<< The function that will be called when the timer expires. */
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTimerNumber; /*<< An ID assigned by trace tools such as FreeRTOS+Trace */
#endif
uint8_t ucStatus; /*<< Holds bits to say if the timer was statically allocated or not, and if it is active or not. */
} xTIMER;
参数解释:
pcTimerName (const char):*
用途:文本名称,主要用于调试目的,不被内核使用。
说明:该成员是一个指向常量字符的指针,表示定时器的文本名称。在调试过程中,可以使用这个名称来标识定时器,但它对于内核本身并没有实际的功能。
xTimerListItem (ListItem_t):
用途:链表项,用于事件管理。
说明:这是一个用于链接定时器的标准链表项。它被用于将定时器链接到一个链表中,以进行事件管理。链表通常由RTOS内核用于跟踪定时器。
xTimerPeriodInTicks (TickType_t):
用途:定时器的周期,以时钟节拍为单位。
说明:表示定时器的周期,即定时器多久触发一次,以时钟节拍(Tick)为单位。时钟节拍是RTOS中的基本时间单位。
pvTimerID (void):*
用途:用于标识定时器的ID。
说明:这是一个指向void类型的指针,用于标识定时器。当相同的回调函数用于多个定时器时,通过这个ID可以唯一标识定时器。
pxCallbackFunction (TimerCallbackFunction_t):
用途:定时器到期时调用的回调函数。
说明:这是一个指向定时器到期时将被调用的回调函数的指针。当定时器到期时,RTOS将调用此函数执行相应的操作。
uxTimerNumber (UBaseType_t):
用途:由跟踪工具分配的定时器ID。
说明:在启用了跟踪工具(如FreeRTOS+Trace)的情况下,该成员表示由这些工具分配的定时器ID。
ucStatus (uint8_t):
用途:包含位信息,指示定时器的静态分配状态和活动状态。
说明:这是一个包含位信息的字节,用于表示定时器的状态。其中的位可能包括指示定时器是否静态分配、定时器是否活动等信息。
二、软件定时器的工作机制
使用软件定时器的这些API其实就是在给一个队列发送消息,那么谁来接收这些消息并执行呢?
答案就是守护任务来接收消息并执行。
在xTimerCreateTimerTask函数中会调用prvCheckForValidListAndQueue函数创建出队列和两个链表用于管理软件定时器。
创建链表和队列代码:
这里会创建两个链表:pxCurrentTimerList链表和pxOverflowTimerList链表。
当前定时器列表 pxCurrentTimerList:
插入新定时器: 当系统创建并激活新的定时器时,该定时器会以超时时间升序的方式插入到 pxCurrentTimerList 列表中。这样的设计可以使得链表中的定时器按照即将到期的顺序排列。
扫描与处理: 定时器任务在系统运行中会扫描 pxCurrentTimerList 中的第一个定时器,检查是否已经超时。如果已经超时,就会调用与该定时器关联的回调函数进行相应的处理。如果还未超时,可能会将定时器任务挂起等待,直到下一次扫描。
溢出定时器列表 pxOverflowTimerList:
与 pxCurrentTimerList 相似: pxOverflowTimerList 的作用与 pxCurrentTimerList 类似,用于存储那些因为系统计数器溢出而暂时不活动的定时器。
处理溢出情况: 当系统的节拍计数器溢出时,说明经过了很长一段时间,此时原本在 pxCurrentTimerList 中的定时器可能已经到期。这时,这些到期的定时器将会被移动到 pxOverflowTimerList 中。
列表交换:
溢出后的交换: 当系统节拍计数器发生溢出时,两个列表的功能会进行交换。也就是说,pxOverflowTimerList 变为当前列表,而 pxCurrentTimerList 变为溢出列表。这样交换后,之前在 pxCurrentTimerList 中因为溢出而移动到 pxOverflowTimerList 的定时器将成为下一轮的活动定时器,而 pxCurrentTimerList 将被重新用于新的定时器的插入。
这样的设计使得系统在处理定时器时能够更高效地管理和利用定时器,特别是在考虑到系统计数器溢出的情况下。通过不断地在两个列表之间交换,系统能够有效地处理长时间运行和定时器溢出的情况,确保定时器功能的可靠性。
消息队列xTimerQueue,这个队列会存储接收到的消息,守护任务可以读取这个队列中的消息并进行处理。
守护任务在启动调度器时会自动被创建:
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
守护任务的优先级通常需要设置为最高,可以通过配置这个configTIMER_TASK_PRIORITY宏来改变优先级。
在守护任务内部会处理队列中的消息:
软件定时器工作机制:
三、创建软件定时器
重要代码分析:
首先需要使用pvPortMalloc申请一个软件定时器结构体。
Timer_t * pxNewTimer;
pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) );
初始化创建出来的软件定时器:
prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer );
对软件定时器中的成员进行赋值,并且初始化链表项。
pxNewTimer->pcTimerName = pcTimerName;
pxNewTimer->xTimerPeriodInTicks = xTimerPeriodInTicks;
pxNewTimer->pvTimerID = pvTimerID;
pxNewTimer->pxCallbackFunction = pxCallbackFunction;
vListInitialiseItem( &( pxNewTimer->xTimerListItem ) );
四、启动软件定时器
启动软件定时器其实就是调用xTimerGenericCommand函数向队列发送消息。
#define xTimerStart( xTimer, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
构造消息:
DaemonTaskMessage_t xMessage;
xMessage.xMessageID = xCommandID;
xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;
xMessage.u.xTimerParameters.pxTimer = xTimer;
发送消息:
if( xCommandID < tmrFIRST_FROM_ISR_COMMAND )
{
if( xTaskGetSchedulerState() == taskSCHEDULER_RUNNING )
{
xReturn = xQueueSendToBack( xTimerQueue, &xMessage, xTicksToWait );
}
else
{
xReturn = xQueueSendToBack( xTimerQueue, &xMessage, tmrNO_DELAY );
}
}
else
{
xReturn = xQueueSendToBackFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );
}
启动软件定时器后会将软件定时器挂入pxCurrentTimerList链表中。
启动软件定时器其实就是给队列发送了命令,那么守护任务接收到命令后就会进行处理:
根据超时时间的长短挂入pxCurrentTimerList链表中。
五、软件定时器如何知道什么时候被调用
在守护任务中有一个无限循环会一直判断是否有软件定时器超时。
/* 获取最近一次定时器超时时间 */
xNextExpireTime = prvGetNextExpireTime(&xListWasEmpty);
/* 处理超时的定时器或者让队列阻塞 */
prvProcessTimerOrBlockTask(xNextExpireTime, xListWasEmpty);
/* 处理队列接收到的命令 */
prvProcessReceivedCommands();
总结
本篇文章就讲解到这里,下篇文章继续给大家讲解。