原文地址:http://blog.csdn.net/qq_18150497/article/details/52874310
简述
考虑平台硬件定时器个数限制的, FreeRTOS 通过一个 Daemon 任务(启动调度器时自动创建)管理软定时器, 满足用户定时需求. Daemon 任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。
对于硬件定时器的中断服务程序, 我们知道不应该在里面执行复杂,可能导致阻塞的工作,相应的, 虽然软定时器实际是在定时Daemon 任务中执行,但是阻塞的话会导致其他定时器调用被延时, 所以实际使用也应该避免。
软定时器是通过一个任务来辅助实现,该功能时刻裁剪的 , 只有设置 FreeRTOSConfig.h
中configUSE_TIMERS == 1
将相关代码编译进来, 才能正常使用相关功能。
分析的源码版本是 v9.0.0
使用定时器
开始先介绍下如何在自己的工程中使用 FreeRTOS 的软件定时器。
配置定时器服务任务
程序中需要使用到软件定时器, 需要先在 FreeRTOSConfig.h
中正确配置如下宏 :
* configUSE_TIMERS
是否编译定时器相关代码, 如需要使用定时器, 设置为 1
* configTIMER_TASK_PRIORITY
设置定时器Daemon 任务优先级, 如果优先级太低, 可能导致定时器无法及时执行
* configTIMER_QUEUE_LENGTH
设置定时器Daemon 任务的命令队列深度, 设置定时器都是通过发送消息到该队列实现的。
* configTIMER_TASK_STACK_DEPTH
设置定时器Daemon 任务的栈大小
创建 启动 停止定时器
如下示例代码所示
TimerHandle_t xTimerUser;
void vTimerCallback( TimerHandle_t xTimer )
{
static uin32_t ulCount = ( uint32_t ) pvTimerGetTimerID( xTimer );
++ulCount;
vTimerSetTimerID( xTimer, ( void * ) ulCount );
if (ulCount == 10) {
xTimerStop( xTimer, 0 );
}
}
void fun()
{
xTimerUser = xTimerCreate
("Timer's name",
100,
pdTRUE,
( void * ) 0,
vTimerCallback);
if( xTimerUser != NULL ) {
xTimerStart( xTimerUser, 0 );
}
}
- 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
如上所示, 调用函数 xTimerCreate
申请,配置定时器, 通过 xTimerStart
启动定时器, 当定时器计数溢出时, 系统回调注册的函数。
定时器可以设置为一次性 One-shot 或者自动重载 Auto-reload 两种, 第一种溢出后停止定时器, 第二种溢出后会再次启动定时器。
修改定时器
在申请定时器的时候设置的定时器周期, 可以通过函数 xTimerChangePeriod
修改, 如下示例 :
void vAFunction_2( TimerHandle_t xTimer )
{
if( xTimerIsTimerActive( xTimer ) != pdFALSE )
{
}
else
{
if( xTimerChangePeriod( xTimer,
500 / portTICK_PERIOD_MS,
100 ) == pdPASS )
{
xTimerDelete( xTimer );
}
else
{
}
}
}
- 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
如上, 该函数会修改定时器并使定时器 开始运行!!!
另外, 可以通过函数 xTimerReset
重启定时器, 如果已经启动计数, 重新开始计数; 如果没有启动,启动定时器。
定时器使用系统提供 API,涉及 Queue 操作, 如果是在中断程序中调用,需要调用对应带 FromISR
的接口。
获取定时器状态
其他获取定时器信息的函数
pcTimerGetName()
xTimerGetPeriod()
xTimerGetExpiryTime()
定时器实现
FreeRTOS 软定时器的实现在源码目录 Source/include/timers.h, 涉及 链表 和 消息队列(后续文章分析)。
数据结构
使用定时器前,需要先申请定时器, 见 配置定时器服务任务 中, 通过函数 xTimerCreate
获取一个定时器, 实际上是向系统申请了一块内存存储定时器控制块的数据结构, 并将参数填写到该结构体中。
定时器控制块
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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
成功申请定时器后, 定时器并没有开始工作, 需要调用函数将该定时器中的 xTimerListItem
插入到定时器管理链表中, Daemon 任务才能在该定时器设定的溢出时刻调用其回调函数。
定时器管理链表
timers.c
中定义了如下几个链表变量用于管理定时器, 定时器根据其溢出时刻从小到大插入链表进行管理。
使用两个链表是为了应对系统 TickCount 溢出的问题,在 FreeRTOS 任务调度 系统节拍 介绍过。
PRIVILEGED_DATA static List_t xActiveTimerList1;
PRIVILEGED_DATA static List_t xActiveTimerList2;
PRIVILEGED_DATA static List_t *pxCurrentTimerList;
PRIVILEGED_DATA static List_t *pxOverflowTimerList;
命令队列
文章开头提到的使用定时器的函数, 大部分都带有一个参数,用于设置调用后允许阻塞的最大时间, 原因是, 这些函数并没有直接操作定时器管理链表, 而是向定时器Daemon 任务的消息队列 xTimerQueue
发送消息命令。 之后, 定时器Daemon 任务会从消息队列取出消息并响应操作。
定时器服务任务
此处,从系统启动的定时器Daemon 任务展开分析 FreeRTOS 的软定时器的实现 。
该任务主体的执行流程如下所示 :
永久循环部分的代码 :
for( ;; )
{
xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
prvProcessReceivedCommands();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
回调定时器
定时器任务中, 取出下一个定时器溢出的时间,并把它传递给函数prvProcessTimerOrBlockTask
, 该函数负责处理溢出定时器, 应对节拍计数器溢出问题等, 并设置合适的时间阻塞 Daemon 任务, 让出 CPU 使用权直到下一个定时器溢出或者接收到新的命令。
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();
}
}
}
- 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
处理节拍计数器溢出
上面提到, 通过函数 prvSampleTimeNow
判断节拍计数器是否发发生溢出, 并执行相应处理, 此处看看该函数内容 :
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
可以看到, 该函数每次调用都会记录节拍值, 下一次调用,通过比较相邻两次调用的值判断节拍计数器是否溢出过。
当节拍计数器溢出, 需要处理掉当前链表上的定时器(应为这条链表上的定时器都已经溢出了), 然后切换链表。
对于处理这部分任务的函数, 主要要注意其对于需要重载的定时器的处理 :
类比一下 , 一个自动重载的定时器, 每月需要执行一次, 上次调用是2016 年6月, 之后由于优先级问题,导致下一次调用时间等到第二年2017年 1月了,也就是跨年了(节拍计数器溢出了), 切换日历(链表)前, 需要把旧的先处理掉, 那么实际该定时器在2016年 7~ 12月每月都需要执行一次,所以要补偿回来,直到第二年1月, 才发送消息,插到新日历里面(链表)。
即使时间延迟了,但是该调用几次,是保证的!!
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;
}
- 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
函数 prvProcessTimerOrBlockTask
中, 当节拍计数器没有溢出, 判断当前管理链表上溢出定时器并进行处理的函数 prvProcessExpiredTimer
整体和上面介绍差别不大, 执行函数回调, 判断是否需要重载等。
命令处理
用户将需要处理的定时器命令发送到定时器的消息队列, Daemon 任务每次执行期间回去读取并执行, 这部分工作有任务主体中的函数 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 )
#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 );
configASSERT( xResult );
( void ) xResult;
}
}
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;
( void ) prvInsertTimerInActiveList( pxTimer,
( xTimeNow + pxTimer->xTimerPeriodInTicks ),
xTimeNow, xTimeNow );
break;
case tmrCOMMAND_DELETE :
break;
default :
break;
}
}
}
}
- 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
函数处理定时器,开头不管后面命令是什么,如果定时器原本在运行, 直接移除。
参考