软件定时器是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守护任务),并且可以看到是通过宏定义的方式选择编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void vTaskStartScheduler( void ) { ...略去部分代码 #if ( configUSE_TIMERS == 1 ) { if( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif ...略去部分代码 }
|
xTimerCreateTimerTask()只是一个函数名,它内部的函数内容如下。
2.2 创建Daemon任务
软件定时器任务(Daemon任务)的创建是通过xTaskCreate
方法来创建,在创建守护任务之前,还要先通过prvCheckForValidListAndQueue
函数创建两个列表和一个消息队列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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 } else { mtCOVERAGE_TEST_MARKER(); }
configASSERT( xReturn ); return xReturn; }
|
创建列表与消息队列的具体函数内容如下:
2.3 创建列表与消息队列
由于系统节拍采用32位变量进行计数,总有一天会溢出,所以软件定时器使用了两个列表:
当前定时器列表 pxCurrentTimerList
:系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList列表中。系统在定时器任务中扫描pxCurrentTimerList中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数,否则将定时器任务挂起。
溢出定时器列表pxOverflowTimerList
:在软件定时器溢出的时候使用,作用与pxCurrentTimerList一致。
定时器列表会按照唤醒时间从早到晚挂接在当前定时器列表中,唤醒时间如果溢出了就挂接在溢出定时器列表中。当系统节拍溢出之后,两个列表的功能会进行交换,即当前列表变为溢出列表,溢出列表变为当前列表。
此外,FreeRTOS的软件定时器还使用了一个消息队列xTimerQueue
,利用“定时器命令队列”向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器,复位、删除、改变周期等。
假如定时器任务处于阻塞状态,我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中
(注:事件标志组在中断中设置事件标志,实际也是通过队列发送消息给软件定时器任务来执行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| static void prvCheckForValidListAndQueue( void ) { taskENTER_CRITICAL(); { if( xTimerQueue == NULL ) { vListInitialise( &xActiveTimerList1 ); 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 } else { mtCOVERAGE_TEST_MARKER(); } } taskEXIT_CRITICAL(); }
|
既然消息队列是用来处理软件定时器的一些操作指令的,那这些在哪里呢?其实就是软件定时器的一些API函数,如下。
2.4 软件定时器API函数实际原理
软件定时器的多种API函数,如启动、停止、删除、复位、改变周期等,实际是通过宏定义的方式提供:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #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
消息队列,由软件定时器任务(守护任务来)接收并进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| 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任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static void prvTimerTask( void *pvParameters ) { TickType_t xNextExpireTime; BaseType_t xListWasEmpty;
( void ) pvParameters;
#if( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 ) ...略去部分代码 #endif
for( ;; ) { xNextExpireTime = prvGetNextExpireTime(&xListWasEmpty); prvProcessTimerOrBlockTask(xNextExpireTime, xListWasEmpty); prvProcessReceivedCommands(); } }
|
以上介绍了从启动调度器到实现Daemon任务的具体过程,下面来详细分析Daemon任务中的三部分功能的细节。
3 软件定时器任务三部分功能分析
先来一张整体结构图:
首先是从定时器列表中获取下一次的溢出时间,因为各定时器的溢出时间是按照升序排列的,因此只需获取下一次的溢出时间。
3.1 获取下一个定时超时时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 { xNextExpireTime = ( TickType_t ) 0U; }
return xNextExpireTime; }
|
3.2 处理或阻塞软件定时器任务
那系统如何处理软件定时器列表?系统在不断运行,而xTimeNow(xTickCount)随着SysTick的触发一直在增长,在软件定时器任务运行的时候会获取下一个要唤醒的定时器:
- 比较当前系统时间xTimeNow是否大于或等于下一个定时器唤醒时间xTicksToWait
- 若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数
- 否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| 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 获取当前时间并决定是否切换列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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), 下一次调用,通过比较相邻两次调用的值判断节拍计数器是否溢出。当系统节拍计数器溢出, 必须切换计时器列表。如果当前计时器列表中仍然引用任何计时器,那么它们一定已经过期,应该在切换列表之前进行处理。
切换列表的具体内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| 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 处理超时的定时器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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()函数,定时器定时时间还没到,将当前任务挂起,直到定时器到期才唤醒或者收到命令的时候唤醒:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 任务每次执行期间回去读取并执行,下面看看该函数的具体内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
| 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 ); configASSERT( pxCallback );
pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 ); } else { mtCOVERAGE_TEST_MARKER(); } } #endif
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 : 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 break;
default : break; } } } }
|
4 软件定时器的使用
4.1 软件定时器控制块(结构体)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| typedef struct tmrTimerControl { const char *pcTimerName; ListItem_t xTimerListItem; TickType_t xTimerPeriodInTicks; UBaseType_t uxAutoReload; void *pvTimerID; 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 创建一个软件定时器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) TimerHandle_t xTimerCreate( const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, 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 }
return pxNewTimer; } #endif
|
成功申请定时器后, 定时器并没有开始工作, 需要调用启动或复位等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接口,在回调函数中也绝对不允许出现死循环。
创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。