freertos源码分析DAY12 (软件定时器)
目录
1. 软件定时器结构
2. 创建软件定时器
3. 软件定时器运行原理
4.软件定时器命令
4.1 软件定时器启动
软件定时器可在硬件定时器资源不够用时,作为额外的定时器资源来供用户使用。但其计时精度较低,最高精度只有1ms,且在软件定时器的定时过程中是极有可能被其它中断所打断。故它只适用于一些对计时精度要求不苛刻的场景。
下面先来介绍软件定时器的结构,了解其基本结构信息后再去看其原理及使用流程。
1. 软件定时器结构
软件定时器的结构体包含了一个软件定时器所需的所有信息,其结构体如下所示(它在Timers.c源文件中):
typedef struct tmrTimerControl
{
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. */
UBaseType_t uxAutoReload; /*<< Set to pdTRUE if the timer should be automatically restarted once expired. Set to pdFALSE if the timer is, in effect, a one-shot timer. */
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
#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
uint8_t ucStaticallyAllocated; /*<< Set to pdTRUE if the timer was created statically so no attempt is made to free the memory again if the timer is later deleted. */
#endif
} xTIMER;
typedef xTIMER Timer_t;
可概括为下图结构:
定时器名:定时器名称,只有标识作用;
定时器节点:当前定时器对应的节点对象;
计数周期:定时器从计时开始,直到到期所要花费的时间;
重装载标记:
1 表定时器周期触发,触发间隔为计数周期;
0 表定时器非周期触发,到期后定时器就会被删除,并回收资源;
定时器ID:定时器ID,数字形式。该 ID 典型的用法是当一个回调函数分配给一个或者多个软件定时器时,在回调函数里面根据 ID 号来处理不同的软件定时器。
要执行的回调函数:定时器到期后,要执行的回调函数;
标记定时器使用的内存:删除时判断是否需要释放内存;
2. 创建软件定时器
一个软件定时器,需要在创建后才能被使用。创建软件定时器的API函数如下所示:
TimerHandle_t xTimerCreate( const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
{
Timer_t *pxNewTimer;
pxNewTimer = ( Timer_t * ) pvPortMalloc( sizeof( Timer_t ) );
if( pxNewTimer != NULL )
{
prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer );
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
/* Timers can be created statically or dynamically, so note this
timer was created dynamically in case the timer is later
deleted. */
pxNewTimer->ucStaticallyAllocated = pdFALSE;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
}
return pxNewTimer;
}
参数:即软件定时器所需要的信息;(参数具体含义与软件结构体内的成员名一致)
1. 申请一片软件定时器空间,用于存储软件定时器所需要的信息;
2. 调用 prvInitialiseNewTimer 函数初始化软件定时器结构体,其内部操作就是将软件定时器所需的参数信息赋予到动态申请的空间中,并初始化软件定时器节点;
3. 软件定时器运行原理
首先使用Rtos的软件定时器需要将软件定时器宏“configUSE_TIMERS”设置为1。之后软件定时器任务会在启动调度器时被创建。软件定时器的创建过程如下所示:
BaseType_t xTimerCreateTimerTask( void )
{
BaseType_t xReturn = pdFAIL;
prvCheckForValidListAndQueue();
if( xTimerQueue != NULL )
{
xReturn = xTaskCreate( prvTimerTask,
configTIMER_SERVICE_TASK_NAME,
configTIMER_TASK_STACK_DEPTH,
NULL,
( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT, &xTimerTaskHandle );
}
return xReturn;
}
1、在创建软件定时器任务之前,首先会调用函数 prvCheckForValidListAndQueue 初始化软件定时器链表和软件定时器溢出链表,以及创建一个用于软件定时器发送接收命令的队列;
2、创建一个软件定时器任务,用于软件定时器的运作,为保证定时器的运行精度它的优先级一般是当前任务中最高的;
软件定时器任务的实现具体如下:
static portTASK_FUNCTION( prvTimerTask, pvParameters )
{
TickType_t xNextExpireTime;
BaseType_t xListWasEmpty;
( void ) pvParameters;
for( ; ; )
{
xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
prvProcessReceivedCommands();
}
}
软件定时器任务具体做了以下三个操作:
1、调用函数 prvGetNextExpireTime 得到下一个定时器的到期时间,第一个到期的定时器会是被挂载在软件定时器链表的第一个元素;
2、调用函数 prvProcessTimerOrBlockTask 查看当前是否存在到期定时器,存在则执行软件定时器所对应的回调函数。若不存在到期定时器就将软件定时器任务阻塞且消息队列中无消息存在(则将软件定时任务的事件节点插入到消息队列的接收等待队列),其阻塞时间为到期时间当前时间与下一次定时器到期时间的差值(若定时器链表中还未有任何软件定时器,则此时将任务一直阻塞同样其会被插入到消息队列的接收等待队列,直到有软件定时器发送命令到队列); // 红色字段要知道消息队列的运行规则才能理解
3、若软件定时器的消息队列中存在消息,则软件定时器任务不会被阻塞。此时软件定时器任务调用函数 prvProcessReceivedCommands 来处理队列中的命令(这个函数会一直接收消息队列的命令直至没有命令为止);
可以看到在软件定时器任务只会在两种情况下阻塞:
1、没有软件定时器到期时,但软件定时器链表存在软件定时器节点情况;(此时任务阻塞,且阻塞时间为下次到期时间和当前时间的差值)
2、软件定时器任务内没有挂载,且消息队列无命令的情况;(任务永久阻塞)
4.软件定时器命令
首先要了解一点,软件定时器的任务在第一次被创建后,其会因为内部软件定时器链表上没有任何软件定时器节点且消息队列上不存在任何消息而被永久阻塞。接受处理命令的过程只以软件定时器启动的过程为例。其余发送命令的方式和定时器启动方式一致。
4.1 软件定时器启动
软件定时器开始计时的函数 xTimerStart 具体如下(需和prvProcessReceivedCommands处理命令函数一起看):
// timers.h中
#define xTimerStart( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )
// timers.c中
BaseType_t xTimerGenericCommand( 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;
if( xTimerQueue != NULL )
{
xMessage.xMessageID = xCommandID;
xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;
xMessage.u.xTimerParameters.pxTimer = ( Timer_t * ) 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 );
}
}
return xReturn;
}
从上不难看出xTimerStart函数调用的其实是xTimerGenericCommand定时器通用发送消息函数,其内部做了如下操作:
1、判断命令类型,是否是ISR在中断中发送的。若是则调用中断中发送消息接口将命令发送到消息队列。
2、若不是,则首先判断调度器当前是否挂起?若调度器处于正常调度状态,则正常发送消息到队列,且队列满溢时发送消息任务的阻塞时间为设定值。否则,不等待消息发送;
可以看到软件定时器的启动就是向队列发送一个消息,供软件定时任务识别并执行。
调用xQueueSendToBack函数发送启动消息到队列的过程中,会核对挂在在消息队列接受等待列表中的任务。此任务就是创建后即被阻塞的软件定时器任务,此时它会被唤起,并调用prvProcessReceivedCommands 函数处理当即过来的命令,prvProcessReceivedCommands函数具体如下所示:(为方便观看,此函数已做部分删减)
static void prvProcessReceivedCommands( void )
{
DaemonTaskMessage_t xMessage;
Timer_t *pxTimer;
BaseType_t xTimerListsWereSwitched, xResult;
TickType_t xTimeNow;
while( xQueueReceive( xTimerQueue, &xMessage, tmrNO_DELAY ) != pdFAIL )
{
#if ( INCLUDE_xTimerPendFunctionCall == 1 )
{
if( xMessage.xMessageID < ( BaseType_t ) 0 )
{
const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );
pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );
}
}
#endif
if( xMessage.xMessageID >= ( BaseType_t ) 0 )
{
pxTimer = xMessage.u.xTimerParameters.pxTimer;
if( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) == pdFALSE )
{
( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
}
xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
switch( xMessage.xMessageID )
{
case tmrCOMMAND_START :
case tmrCOMMAND_START_FROM_ISR :
case tmrCOMMAND_RESET :
case tmrCOMMAND_RESET_FROM_ISR :
case tmrCOMMAND_START_DONT_TRACE :
if( prvInsertTimerInActiveList( pxTimer, xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, xTimeNow, xMessage.u.xTimerParameters.xMessageValue ) != pdFALSE )
{
pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );
if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )
{
xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, NULL, tmrNO_DELAY );
( void ) xResult;
}
}
break;
default :
break;
}
}
}
}
但Start命令到达时,该函数具体做了如下操作:
1、调用xQueueReceive,接受消息队列中的消息,之后做相应处理。直到消息队列中的命令被清空为止;
2、首先判断定时器命令是否有效。有效则将定时器从定时器链表上移除,表当前定时器要用于处理新命令了;
3、记录当前系统时间;
4、开始判断命令,并执行对应操作;
5、当前命令为START开始命令(START开始命令的目的是为了让创建的软件定时器开始工作,所以这里首先要将软件定时器挂在到正在工作的链表上);
6、prvInsertTimerInActiveList函数目的是为了找到当前活跃的软件定时器链表。这个函数内部寻找活跃链表的方法分为两种:
1、普通情况:到期时间<当前时基。此种情况又分为两种情况。
第一种:软件定时器在发送命令的过程中到期。此时立即处理定时器;
第二种:软件定时器下次到期时间<当前时基是因为时基溢出而导致的,此时将软件定时器插入到软件定时器溢出链表;
2、特殊情况:到期时间>当前时基。此种情况同样分为两种情况。
第一种:软件定时器发送命令时系统时基还未溢出,而当前系统时基溢出。且软件定时器到期时间>发送命令时间(表软件定时器到期的时候,系统时基还未溢出。系统时基溢出就表示定时器一定溢出),此时立即处理定时器;
第二种:正常未溢出情况下。软件定时器还未到期。此时将软件定时器插入到当前软件定时器链表;
其余的软件定时器功能函数和软件定时器的启动流程基本一致。