软件定时器是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 /* configUSE_TIMERS */
...略去部分代码
}

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 /* configSUPPORT_STATIC_ALLOCATION */
}
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 )
{
/* 初始化定时器列表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函数,如启动、停止、删除、复位、改变周期等,实际是通过宏定义的方式提供:

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;

/* 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 获取下一个定时超时时间

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 /* 当前列表为空 */
{
/*超时时间设为0,使任务非阻塞 */
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 )
{
/* 命令码小于等于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.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; /* 定时器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 创建一个软件定时器

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, /* 定时器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接口,在回调函数中也绝对不允许出现死循环。

  • 创建单次软件定时器,该定时器超时执行完回调函数后,系统会自动删除该软件定时器,并回收资源。