软件定时器是FreeRTOS中的一个重要模块,使用软件定时器可以方便的实现一些与超时或周期性相关的功能,本篇从FreeRTOS的源码入手,来分析FreeRTOS软件定时器的运行机理。
1
基础知识
1.1 软件定时器与硬件定时器的区别
硬件定时器
- 每次在定时时间到达之后就会自动触发一个中断,用户在中断服务函数中处理信息
- 硬件定时器的精度一般很高,可以达到纳秒级别
- 硬件定时器是芯片本身提供的定时功能
软件定时器
- 指定时间到达后要调用回调函数(也称超时函数),用户在回调函数中处理信息
- 硬件定时器的定时精度与系统时钟的周期有关,一般系统利用SysTick作为软件定时器的基础时钟,系统节拍配置为FreeRTOSConfig.h中的
configTICK_RATE_HZ
,默认是1000,那么系统的时钟节拍周期就为1ms - 软件定时器是由操作系统提供的一类系统接口
注意:软件定时器回调函数的上下文是任务,回调函数要快进快出,且回调函数中不能有任何阻塞任务运行的情况,如vTaskDelay()以及其它能阻塞任务运行的函数。
1.2 软件定时器的两种工作模式
FreeRTOS提供的软件定时器支持单次模式和周期模式
- 单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器删除,不再重新执行。
- 周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除
2
软件定时器工作原理
通过查看FreeRTOS的源码,可以发现,软件定时器的运行原理实际是FreeRTOS 通过一个 prvTimerTask任务(也叫守护任务Daemon)管理软定时器,它是在启动调度器时自动创建的。另外,软件定时器在FreeRTOS中是可选功能,如果需要使用软件定时器,需要设置 FreeRTOSConfig.h 中的宏定义configUSE_TIMERS
为1 。
先用一个图来表示整个创建过程:
下面来看一下启动调度器时是怎么创建Daemon任务的。
2.1 任务调度器函数创建Daemon任务
main函数的最后会启动FreeRTOS的任务调度函数,在该函数中会创建软件定时器任务(即Daemon守护任务),并且可以看到是通过宏定义的方式选择编译:
代码语言:javascript复制/* 启动调度器 */
void vTaskStartScheduler( void )
{
...略去部分代码
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
/* 创建软件定时器任务(守护任务) */
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
...略去部分代码
}
xTimerCreateTimerTask()只是一个函数名,它内部的函数内容如下。
2.2 创建Daemon任务
软件定时器任务(Daemon任务)的创建是通过xTaskCreate
方法来创建,在创建守护任务之前,还要先通过prvCheckForValidListAndQueue
函数创建两个列表和一个消息队列:
BaseType_t xTimerCreateTimerTask( void )
{
BaseType_t xReturn = pdFAIL;
/* 创建列表与消息队列 */
prvCheckForValidListAndQueue();
if( xTimerQueue != NULL )
{
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
...略去部分代码
#else
{
/* 创建软件定时器任务(守护任务) */
xReturn = xTaskCreate( prvTimerTask,
"Tmr Svc",
configTIMER_TASK_STACK_DEPTH,
NULL,
( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
&xTimerTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
configASSERT( xReturn );
return xReturn;
}
创建列表与消息队列的具体函数内容如下:
2.3 创建列表与消息队列
由于系统节拍采用32位变量进行计数,总有一天会溢出,所以软件定时器使用了两个列表:
- 当前定时器列表
pxCurrentTimerList
:系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList列表中。系统在定时器任务中扫描pxCurrentTimerList中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数,否则将定时器任务挂起。 - 溢出定时器列表
pxOverflowTimerList
:在软件定时器溢出的时候使用,作用与pxCurrentTimerList一致。
定时器列表会按照唤醒时间从早到晚挂接在当前定时器列表中,唤醒时间如果溢出了就挂接在溢出定时器列表中。当系统节拍溢出之后,两个列表的功能会进行交换,即当前列表变为溢出列表,溢出列表变为当前列表。
此外,FreeRTOS的软件定时器还使用了一个消息队列xTimerQueue
,利用“定时器命令队列”向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器,复位、删除、改变周期等。
假如定时器任务处于阻塞状态,我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中
(注:事件标志组在中断中设置事件标志,实际也是通过队列发送消息给软件定时器任务来执行)
代码语言:javascript复制/* 检查是否有可用的列表和队列 */
static void prvCheckForValidListAndQueue( void )
{
/* 进入临界区 */
taskENTER_CRITICAL();
{
/* 还没有创建队列 */
if( xTimerQueue == NULL )
{
/* 初始化定时器列表1 */
vListInitialise( &xActiveTimerList1 );
/* 初始化定时器列表2 */
vListInitialise( &xActiveTimerList2 );
/* 当前定时器列表 */
pxCurrentTimerList = &xActiveTimerList1;
/* 溢出定时器列表 */
pxOverflowTimerList = &xActiveTimerList2;
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
...略去部分代码
#else
{
/* 创建定时器消息队列 */
xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );
}
#endif
#if ( configQUEUE_REGISTRY_SIZE > 0 )
{
if( xTimerQueue != NULL )
{
vQueueAddToRegistry( xTimerQueue, "TmrQ" );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configQUEUE_REGISTRY_SIZE */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
}
既然消息队列是用来处理软件定时器的一些操作指令的,那这些在哪里呢?其实就是软件定时器的一些API函数,如下。
2.4 软件定时器API函数实际原理
软件定时器的多种API函数,如启动、停止、删除、复位、改变周期等,实际是通过宏定义的方式提供:
代码语言:javascript复制/*启动定时器*/
#define xTimerStart(xTimer, xTicksToWait) xTimerGenericCommand((xTimer), tmrCOMMAND_START, (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))
/*复位定时器*/
#define xTimerReset(xTimer, xTicksToWait) xTimerGenericCommand((xTimer), tmrCOMMAND_RESET, (xTaskGetTickCount()), NULL, (xTicksToWait))
/*从中断中启动定时器*/
#define xTimerStartFromISR(xTimer, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_START_FROM_ISR, (xTaskGetTickCountFromISR()), (pxHigherPriorityTaskWoken), 0U)
/*从中断中停止定时器*/
#define xTimerStopFromISR(xTimer, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_STOP_FROM_ISR, 0, (pxHigherPriorityTaskWoken), 0U)
/*从中断中改变定时器周期*/
#define xTimerChangePeriodFromISR(xTimer, xNewPeriod, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, (xNewPeriod), (pxHigherPriorityTaskWoken), 0U)
/*从中断中复位定时器*/
#define xTimerResetFromISR(xTimer, pxHigherPriorityTaskWoken) xTimerGenericCommand((xTimer), tmrCOMMAND_RESET_FROM_ISR, (xTaskGetTickCountFromISR()), (pxHigherPriorityTaskWoken), 0U)
这些API函数对应的宏定义,本质上又都是调用了xTimerGenericCommand
函数来实现对消息的打包和发送。
2.5 软件定时器打包命令与发送
该函数将命令打包成队列项发送给xTimerQueue
消息队列,由软件定时器任务(守护任务来)接收并进行处理。
/* 软件定时器打包命令与发送 */
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;
configASSERT( xTimer );
if( xTimerQueue != NULL )
{
/* 命令码 */
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);
}
traceTIMER_COMMAND_SEND( xTimer, xCommandID, xOptionalValue, xReturn );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
return xReturn;
}
上面分析的差不多了,现在回到重点,回顾2.2的xTimerCreateTimerTask()
函数,在创建列表与消息队列后,会使用xTaskCreate
方法来创建软件定时器任务prvTimerTask()
,该任务实体的具体内容如下:
2.6 软件定时器任务基本功能(三部分)
软件定时器任务的具体内容可分为三部分:
- 获取最近一次定时器超时时间
- 处理超时的定时器或者让队列阻塞
- 处理队列接收到的命令
三部分不断循环处理实现Daemon任务。
代码语言:javascript复制static void prvTimerTask( void *pvParameters )
{
TickType_t xNextExpireTime;
BaseType_t xListWasEmpty;
/* Just to avoid compiler warnings. */
( void ) pvParameters;
#if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
...略去部分代码
#endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */
for( ;; )
{
/* 获取最近一次定时器超时时间 */
xNextExpireTime = prvGetNextExpireTime(&xListWasEmpty);
/* 处理超时的定时器或者让队列阻塞 */
prvProcessTimerOrBlockTask(xNextExpireTime, xListWasEmpty);
/* 处理队列接收到的命令 */
prvProcessReceivedCommands();
}
}
以上介绍了从启动调度器到实现Daemon任务的具体过程,下面来详细分析Daemon任务中的三部分功能的细节。
3
软件定时器任务功能分析
先来一张整体结构图:
首先是从定时器列表中获取下一次的溢出时间,因为各定时器的溢出时间是按照升序排列的,因此只需获取下一次的溢出时间。
3.1 获取下一个定时超时时间
代码语言:javascript复制/* 获取下一次的定时器超时时间 */
static TickType_t prvGetNextExpireTime( BaseType_t * const pxListWasEmpty )
{
TickType_t xNextExpireTime;
/* 判断当前定时器列表是否为空 */
*pxListWasEmpty = listLIST_IS_EMPTY( pxCurrentTimerList );
/* 当前列表非空 */
if( *pxListWasEmpty == pdFALSE )
{
/* 获取最近超时时间 */
xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );
}
else /* 当前列表为空 */
{
/*超时时间设为0,使任务非阻塞 */
xNextExpireTime = ( TickType_t ) 0U;
}
return xNextExpireTime;
}
3.2 处理或阻塞软件定时器任务
那系统如何处理软件定时器列表?系统在不断运行,而xTimeNow(xTickCount)随着SysTick的触发一直在增长,在软件定时器任务运行的时候会获取下一个要唤醒的定时器:
- 比较当前系统时间xTimeNow是否大于或等于下一个定时器唤醒时间xTicksToWait
- 若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数
- 否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息
/* 处理或阻塞软件定时器任务 */
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime, BaseType_t xListWasEmpty )
{
TickType_t xTimeNow;
BaseType_t xTimerListsWereSwitched;
/* 挂起调度器 */
vTaskSuspendAll();
{
/* 获取当前时间,并判断是否需要切换定时器列表,如果需要则切换 */
xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
/* 定时器列表没有切换 */
if( xTimerListsWereSwitched == pdFALSE )
{
/* 当前列表中有定时器,且下次唤醒时间小于当前时间,即超时了 */
if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )
{
/* 解除调度器挂起 */
( void )xTaskResumeAll();
/* 处理超时的定时器 */
prvProcessExpiredTimer( xNextExpireTime, xTimeNow );
}
else/* 定时器列表为空,或者没有超时 */
{
/* 定时器列表为空 */
if( xListWasEmpty != pdFALSE )
{
/* 判断溢出列表是否为空,如果两个列表都为空,则无限期阻塞 */
xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );
}
/* 定时器定时时间还没到,将当前任务挂起,让队列按照给定的时间进行阻塞 */
vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );
/* 解除调度器挂起 */
if( xTaskResumeAll() == pdFALSE )
{
/* 申请切换任务 */
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
/* 解除调度器挂起 */
( void ) xTaskResumeAll();
}
}
}
3.2.1 获取当前时间并决定是否切换列表
代码语言:javascript复制static TickType_t prvSampleTimeNow( BaseType_t * const pxTimerListsWereSwitched )
{
TickType_t xTimeNow;
/*静态变量 记录上一次调用时系统节拍值*/
PRIVILEGED_DATA static TickType_t xLastTime = ( TickType_t ) 0U;
/*获取本次调用节拍结束器值*/
xTimeNow = xTaskGetTickCount();
/*判断节拍计数器是否溢出过*/
if( xTimeNow < xLastTime )
{
/*发生溢出,处理当前链表上所有定时器并切换管理链表*/
prvSwitchTimerLists();
*pxTimerListsWereSwitched = pdTRUE;
}
else
{
*pxTimerListsWereSwitched = pdFALSE;
}
/*更新时间记录*/
xLastTime = xTimeNow;
return xTimeNow;
}
可以看到, 该函数每次调用都会记录当前系统节拍时间(TickCount), 下一次调用,通过比较相邻两次调用的值判断节拍计数器是否溢出。当系统节拍计数器溢出, 必须切换计时器列表。如果当前计时器列表中仍然引用任何计时器,那么它们一定已经过期,应该在切换列表之前进行处理。
切换列表的具体内容如下:
代码语言:javascript复制static void prvSwitchTimerLists( void )
{
TickType_t xNextExpireTime, xReloadTime;
List_t *pxTemp;
Timer_t *pxTimer;
BaseType_t xResult;
/* 列表非空,循环处理,直至将该列表处理完 */
while( listLIST_IS_EMPTY( pxCurrentTimerList ) == pdFALSE )
{
xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );
/* 从列表中移除软件定时器 */
pxTimer = ( Timer_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );
( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
traceTIMER_EXPIRED( pxTimer );
/*执行回调函数*/
pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );
/*对于周期定时器*/
if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )
{
/*计算重新加载值:下个溢出时间 定时周期*/
xReloadTime = ( xNextExpireTime pxTimer->xTimerPeriodInTicks );
/*如果重新加载值>下个溢出时间,应该将计时器重新插入当前列表,以便在此循环中再次处理它*/
if( xReloadTime > xNextExpireTime )
{
listSET_LIST_ITEM_VALUE( &( pxTimer->xTimerListItem ), xReloadTime );
listSET_LIST_ITEM_OWNER( &( pxTimer->xTimerListItem ), pxTimer );
vListInsert( pxCurrentTimerList, &( pxTimer->xTimerListItem ) );
}
else/*否则,应该发送一个命令来重新启动计时器,以确保它只插入到列表之后列表已被交换*/
{
xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xNextExpireTime, NULL, tmrNO_DELAY );
configASSERT( xResult );
( void ) xResult;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pxTemp = pxCurrentTimerList;
pxCurrentTimerList = pxOverflowTimerList;
pxOverflowTimerList = pxTemp;
}
(切换列表这里还没完全弄明白)
下面来看一下如何处理到时(或超时)的定时器:
3.2.2 处理超时的定时器
代码语言:javascript复制/* 处理超时的定时器 */
static void prvProcessExpiredTimer( const TickType_t xNextExpireTime, const TickType_t xTimeNow )
{
BaseType_t xResult;
/* 获取最近的超时定时器 */
Timer_t *const pxTimer = ( Timer_t * )listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );
/* 将最近的超时定时器从活跃列表中移除 */
(void)uxListRemove( &( pxTimer->xTimerListItem ) );
traceTIMER_EXPIRED(pxTimer);
/* 周期定时 */
if( pxTimer->uxAutoReload == ( UBaseType_t )pdTRUE )
{
/* 重新计算超时时间并加入活跃列表,如果下一次超时时间都已经过了 */
if( prvInsertTimerInActiveList( pxTimer, ( xNextExpireTime pxTimer->xTimerPeriodInTicks ), xTimeNow, xNextExpireTime ) != pdFALSE )
{
/* 通知守护任务来处理(将定时器插入活跃列表) */
xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xNextExpireTime, NULL, tmrNO_DELAY );
configASSERT( xResult );
( void )xResult;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 调用回调函数 */
pxTimer->pxCallbackFunction( ( TimerHandle_t )pxTimer );
}
3.2.3 让队列按照给定的时间进行阻塞
回顾prvProcessTimerOrBlockTask()函数,定时器定时时间还没到,将当前任务挂起,直到定时器到期才唤醒或者收到命令的时候唤醒:
代码语言:javascript复制/* 让队列按照给定的时间进行阻塞 */
void vQueueWaitForMessageRestricted( QueueHandle_t xQueue, TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely )
{
Queue_t *const pxQueue = xQueue;
/* 锁定队列 */
prvLockQueue( pxQueue );
/* 队列为空 */
if( pxQueue->uxMessagesWaiting == ( UBaseType_t )0U )
{
/* 将任务插入等待接收队列项而阻塞的事件列表,并加入延时列表进行阻塞延时 */
vTaskPlaceOnEventListRestricted( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait, xWaitIndefinitely );
}
/* 队列不为空 */
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 解锁队列 */
prvUnlockQueue(pxQueue);
}
3.3 处理命令队列中接收的消息
用户将需要处理的定时器命令发送到定时器的消息队列, Daemon 任务每次执行期间回去读取并执行,下面看看该函数的具体内容:
代码语言:javascript复制/*处理命令队列中接收的消息*/
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 )
{
/* 命令码小于等于0 (事件标志组中断中置位的命令)*/
if( xMessage.xMessageID < ( BaseType_t ) 0 )
{
const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );
configASSERT( pxCallback );
/* 执行回调函数 */
pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* INCLUDE_xTimerPendFunctionCall */
/* 命令码大于等于0 (软件定时器命令)*/
if( xMessage.xMessageID >= ( BaseType_t ) 0 )
{
/* 定时器句柄 */
pxTimer = xMessage.u.xTimerParameters.pxTimer;
/* 定时器队列项包含该定时器 */
if( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) == pdFALSE )
{
/* 移除该定时器 */
( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
traceTIMER_COMMAND_RECEIVED( pxTimer, xMessage.xMessageID, xMessage.u.xTimerParameters.xMessageValue );
/* 获取当前时间,并判断是否需要切换定时器列表,如果需要则切换 */
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 :
/* 计算超时时间,超时时间没过加入活跃列表,超时时间已过返回pdTrue */
if( prvInsertTimerInActiveList( pxTimer, xMessage.u.xTimerParameters.xMessageValue pxTimer->xTimerPeriodInTicks, xTimeNow, xMessage.u.xTimerParameters.xMessageValue ) != pdFALSE )
{
/* 在加入列表前已经超时,执行对应的回调函数 */
pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );
traceTIMER_EXPIRED( pxTimer );
/*如果是周期定时器*/
if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )
{
/* 发送消息,通知守护任务将定时器插入当前列表 */
xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xMessage.u.xTimerParameters.xMessageValue pxTimer->xTimerPeriodInTicks, NULL, tmrNO_DELAY );
configASSERT( xResult );
( void ) xResult;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
break;
/* 停止定时器 */
case tmrCOMMAND_STOP :
case tmrCOMMAND_STOP_FROM_ISR :
/* 定时器已经从活跃列表中移除,所以什么都不做 */
break;
/* 改变定时器周期 */
case tmrCOMMAND_CHANGE_PERIOD :
case tmrCOMMAND_CHANGE_PERIOD_FROM_ISR :
/* 取出新的频率 */
pxTimer->xTimerPeriodInTicks = xMessage.u.xTimerParameters.xMessageValue;
configASSERT( ( pxTimer->xTimerPeriodInTicks > 0 ) );
/* 计算超时时间,超时时间没过则加入活跃列表 */
( void ) prvInsertTimerInActiveList( pxTimer, ( xTimeNow pxTimer->xTimerPeriodInTicks ), xTimeNow, xTimeNow );
break;
/* 删除定时器 */
case tmrCOMMAND_DELETE :
#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
{
/* 释放软件定时器内存 */
vPortFree( pxTimer );
}
#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
{
if( pxTimer->ucStaticallyAllocated == ( uint8_t ) pdFALSE )
{
/* 释放软件定时器内存 */
vPortFree( pxTimer );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
break;
default :
/* Don't expect to get here. */
break;
}
}
}
}
4
4软件定时器的使用
4.1 软件定时器控制块(结构体)
代码语言:javascript复制/* 软件定时器结构体 */
typedef struct tmrTimerControl
{
const char *pcTimerName; /* 定时器名字 */
ListItem_t xTimerListItem; /* 定时器列表项 */
TickType_t xTimerPeriodInTicks; /* 定时器定时时间 */
UBaseType_t uxAutoReload; /* 定时器周期模式 */
void *pvTimerID; /* 定时器ID */
TimerCallbackFunction_t pxCallbackFunction; /* 定时器回调函数 */
#if (configUSE_TRACE_FACILITY == 1)
UBaseType_t uxTimerNumber;
#endif
#if ((configSUPPORT_STATIC_ALLOCATION == 1) && (configSUPPORT_DYNAMIC_ALLOCATION == 1))
uint8_t ucStaticallyAllocated; /*标记定时器使用的内存, 删除时判断是否需要释放内存*/
#endif
}xTIMER;
typedef xTIMER Timer_t;
4.2 创建一个软件定时器
代码语言:javascript复制#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
TimerHandle_t xTimerCreate( const char * const pcTimerName, /* 定时器名字 */
const TickType_t xTimerPeriodInTicks, /* 定时器定时时间 */
const UBaseType_t uxAutoReload, /* 定时器周期模式 */
void * const pvTimerID, /* 定时器ID */
TimerCallbackFunction_t pxCallbackFunction ) /* 定时器回调函数 */
{
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 )
{
/* 定时器可以静态创建,也可以动态创建,注意这个计时器是动态创建的,以防稍后删除计时器 */
pxNewTimer->ucStaticallyAllocated = pdFALSE;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
}
return pxNewTimer;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
成功申请定时器后, 定时器并没有开始工作, 需要调用启动或复位等API函数将该定时器中的 xTimerListItem
插入到定时器管理链表中, Daemon 任务才能在该定时器设定的溢出时刻调用其回调函数。
4.3 启动定时器
当用户创建并启动一个软件定时器时, FreeRTOS会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表
下面来看一下当启动多个软件定时器时,软件定时器列表是如何来管理这些定时器的:
例如:系统当前时间xTimeNow值为0,注意:xTimeNow其实是一个局部变量,是根据xTaskGetTickCount()函数获取的,实际它的值就是全局变量xTickCount的值,表示当前系统时间。
4.3.1 例子1
- 在当前系统中已经创建并启动了1个定时时间为200定时器Timer1
- 当系统时间xTimeNow为20的时候,用户创建并且启动一个定时时间为100的定时器Timer2,此时Timer2的溢出时间xTicksToWait就为定时时间 系统当前时间(100 20=120),然后将Timer2按xTicksToWait升序插入软件定时器列表中
- 当系统时间xTimeNow为40的时候,用户创建并且启动了一个定时时间为50的定时器Timer3,那么此时Timer3的溢出时间xTicksToWait就为40 50=90,同样安装xTicksToWait的数值升序插入软件定时器列表中
4.3.2 例子2
创建并且启动在已有的两个定时器中间的定时器也是一样的:
- 创建定Timer1并且启动后,假如系统经过了50个tick, xTimeNow从0增长到50,与Timer1的xTicksToWait值相等, 这时会触发与Timer1对应的回调函数,从而转到回调函数中执行用户代码,同时将Timer1从软件定时器列表删除,如果软件定时器是周期性的,那么系统会根据Timer1下一次唤醒时间重新将Timer1添加到软件定时器列表中,按照xTicksToWait的升序进行排列。
- 同理,在xTimeNow=40的时候创建的Timer3,在经过130个tick后(此时系统时间xTimeNow是40,130个tick就是系统时间xTimeNow为170的时候),与Timer3定时器对应的回调函数会被触发,接着将Timer3从软件定时器列表中删除,如果是周期性的定时器,还会按照xTicksToWait升序重新添加到软件定时器列表中。
5
总结与注意事项
- 编译定时器相关代码, 如需要使用定时器,需要先在
FreeRTOSConfig.h
中正确配置宏configUSE_TIMERS
为 1 - 软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为
configTIMER_TASK_PRIORITY
, 如果优先级太低, 可能导致定时器无法及时执行,所以为了更好响应,该优先级应设置为所有任务中最高的优先级。 - 定时器任务的消息队列深度为
configTIMER_QUEUE_LENGTH
, 设置定时器都是通过发送消息到该队列实现的 - 定时器任务的堆栈大小默认为
configTIMER_TASK_STACK_DEPTH
个字节。 - 软件定时器的回调函数中应快进快出,绝对不允许使用任何可能引软件定时器起任务挂起或者阻塞的API接口,在回调函数中也绝对不允许出现死循环。
- 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。