目录

1. 概述

1.1 软件定时器 & 硬件定时器

1.2 软件定时器精度

1.3 单次模式 & 周期模式

2. 软件定时器组件

2.1 定时器任务

2.2 定时器列表

2.3 定时器命令队列

2.4 定时器控制结构

3. 软件定时器操作

3.1 创建定时器

3.2 启动定时器

3.2.1 任务级启动定时器

3.2.2 中断级启动定时器

3.3 停止定时器

3.4 复位定时器

3.5 修改定时值

3.6 删除定时器

4. 定时器任务详解

4.1 prvGetNextExpireTime函数

4.2 prvProcessTimerOrBlockTask函数

4.2.1 函数主体

4.2.2 prvSampleTimeNow函数

4.2.3 prvSwitchTimerList函数

4.2.4 prvProcessExpiredTimer函数

4.2.5 prvInsertTimerInActiveList函数

4.2.6 vQueueWaitForMessageRestricted函数

4.3 prvProcessReceivedCommands函数

4.4 定时器任务工作机制总结

4.4.1 总体机制

4.4.2 交换定时器列表处理pxCurrentTimerList

4.4.3 定时器回调函数编程要求


1. 概述

1.1 软件定时器 & 硬件定时器

① 硬件定时器由SoC提供,定时精度高,以触发中断方式运行,但是个数有限

② 软件定时器由操作系统提供,他构建在硬件定时器之上,定时精度无法和硬件定时器相比,但是个数不限

因此软件定时器适用于对定时精度要求不高的任务

1.2 软件定时器精度

① 软件定时器回调函数在任务上下文中运行(定时器任务),所以会被中断及更高优先级的任务抢占,所以无法确保很高的定时精度

② 软件定时器的定时值必须是系统节拍周期的整数倍,否则无法精确表示。例如系统节拍周期为10ms,则软件定时器的定时值可以设置为10ms的整数倍,如果设置为15ms,则无法精确表示

1.3 单次模式 & 周期模式

① 单次模式

当用户创建并启动定时器后,定时时间到达,只执行一次回调函数之后就将该定时器删除,不再重新执行

② 周期模式

定时器按照设置的定时时间循环执行回调函数,直到用户将定时器删除

2. 软件定时器组件

2.1 定时器任务

static void prvTimerTask( void *pvParameters )
{TickType_t xNextExpireTime;BaseType_t xListWasEmpty;( void ) pvParameters;for( ;; ){// 获取下一到期定时器时间xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );// 处理到期定时器,并将定时器任务阻塞prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );// 接收并处理定时器命令prvProcessReceivedCommands();}
}

说明1:创建时机

定时器任务在启动调度器时创建,函数调用关系如下,

vTaskStartScheduler // tasks.c
--> xTimerCreateTimerTask // timers.c--> xTaskCreate(prvTimerTas) // tasks.c,创建定时器任务

说明2:相关配置项

① configUSE_TIMERS

定时器功能总开关

② configTIMER_TASK_PRIORITY

定时器任务优先级,如果优先级设置得高,定时器命令队列中的命令和定时器回调函数就可以及时得到处理。因此定时器任务优先级一般设置为最高,

#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES-1)

③ configTIMER_TASK_STACK_DEPTH

定时器任务栈大小,由于定时器回调函数是在定时器任务中调用,因此栈的大小要根据定时器回调函数来设置,下面给出一个示例,

// 设置为最小任务栈的2倍
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE*2)

2.2 定时器列表

static List_t xActiveTimerList1;
static List_t xActiveTimerList2;
static List_t *pxCurrentTimerList;
static List_t *pxOverflowTimerList;

与延时列表类似,定时器列表由2个,分别处理未溢出 & 溢出的情况

说明:初始化时机

在首次创建软件定时器时,会初始化定时器列表,详见下文对prvCheckForValidListAndQueue函数的分析

2.3 定时器命令队列

PRIVILEGED_DATA static QueueHandle_t xTimerQueue = NULL;

说明1:初始化时机

在首次创建软件定时器时,会初始化定时器列表,详见下文对prvCheckForValidListAndQueue函数的分析

说明2:工作模式

① 任务或中断通过FreeRTOS提供的定时器API向定时器命令队列发送消息,即定时器控制命令

② 定时器任务从定时器命令队列接收消息,并执行控制命令

2.4 定时器控制结构

typedef struct tmrTimerControl
{// 定时器名称const char *pcTimerName;// 定时器列表项ListItem_t xTimerListItem;// 定时器定时值,以tick为单位TickType_t xTimerPeriodInTicks;// 单次模式或周期模式UBaseType_t uxAutoReload;// 定时器编号,每个定时器可赋予一个唯一的编号void *pvTimerID;// 定时器回调函数TimerCallbackFunction_t pxCallbackFunction;#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && \( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )// 标识定时器内存分配方式uint8_t ucStaticallyAllocated;#endif
} xTIMER;
typedef xTIMER Timer_t;

说明:pvTimerID的用途

FreeRTOS中允许多个软件定时器使用同一个定时器回调函数,通过唯一的定时器编号pvTimerID,可以在定时器回调函数中区分不同的软件定时器

3. 软件定时器操作

3.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 /* configSUPPORT_STATIC_ALLOCATION */}return pxNewTimer;
}

说明1:prvInitialiseNewTimer函数

static void prvInitialiseNewTimer(   const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
Timer_t *pxNewTimer )
{// 定时器定时值必须大于0,否则在周期模式下就会导致定时器回调函数一直被调用// 因为定时器始终到期configASSERT( ( xTimerPeriodInTicks > 0 ) );if( pxNewTimer != NULL ){// 初始化定时器列表与定时器命令队列prvCheckForValidListAndQueue();pxNewTimer->pcTimerName = pcTimerName;pxNewTimer->xTimerPeriodInTicks = xTimerPeriodInTicks;pxNewTimer->uxAutoReload = uxAutoReload;pxNewTimer->pvTimerID = pvTimerID;pxNewTimer->pxCallbackFunction = pxCallbackFunction;vListInitialiseItem( &( pxNewTimer->xTimerListItem ) );}
}

说明2:prvCheckForValidListAndQueue函数

static void prvCheckForValidListAndQueue( void )
{// 进入临界段taskENTER_CRITICAL();{// 定时器列表与定时器命令队列只初始化一次if( xTimerQueue == NULL ){// 初始化定时器列表vListInitialise( &xActiveTimerList1 );vListInitialise( &xActiveTimerList2 );pxCurrentTimerList = &xActiveTimerList1;pxOverflowTimerList = &xActiveTimerList2;// 创建定时器命令队列#if( configSUPPORT_STATIC_ALLOCATION == 1 ){static StaticQueue_t xStaticTimerQueue;static uint8_t ucStaticTimerQueueStorage[configTIMER_QUEUE_LENGTH * sizeof( DaemonTaskMessage_t ) ];xTimerQueue = xQueueCreateStatic(( UBaseType_t ) configTIMER_QUEUE_LENGTH,sizeof( DaemonTaskMessage_t ),&( ucStaticTimerQueueStorage[ 0 ] ), &xStaticTimerQueue );}#else{xTimerQueue = xQueueCreate(( UBaseType_t ) configTIMER_QUEUE_LENGTH,sizeof( DaemonTaskMessage_t ) );}#endif}}taskEXIT_CRITICAL();
}

说明3:cofigTIMER_QUEUE_LENGTH配置项

设置定时器命令队列长度,可由用户在FreeRTOSConfig.h文件中配置

说明4:定时器命令类型

定时器命令队列的长度是通过配置项设置的,而命令的类型则是FreeRTOS定义的,

typedef struct tmrTimerParameters
{// 传递定时器命令所需消息TickType_t xMessageValue;// 要操作的定时器指针Timer_t *pxTimer;
} TimerParameter_t;// 定时器命令类型
typedef struct tmrTimerQueueMessage
{// 定时器命令码BaseType_t xMessageID;union{TimerParameter_t xTimerParameters;#if ( INCLUDE_xTimerPendFunctionCall == 1 )CallbackParameters_t xCallbackParameters;#endif /* INCLUDE_xTimerPendFunctionCall */} u;
} DaemonTaskMessage_t;

说明5:静态创建定时器版本

TimerHandle_t xTimerCreateStatic(    const char * const pcTimerName,
const TickType_t xTimerPeriodInTicks,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction,
StaticTimer_t *pxTimerBuffer )
{// 省略
}

静态创建定时器的流程与动态创建类似,只是需要预先分配定时器内存,类型为StaticTumer_t,该类型也实现了信息隐藏

3.2 启动定时器

新创建的定时器并未处于运行状态,需要调用相关API才能启动

3.2.1 任务级启动定时器

// xTaskGetTickCount函数返回当前系统时间
#define xTimerStart( xTimer, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, \
( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )#define tmrNO_DELAY ( TickType_t ) 0UBaseType_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 = ( 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;
}

说明1:FreeRTOS软件定时器API分析的重点,就是传递的xCommandID和xOptionalValue

启动定时器传递的xOptionalValue为发送定时器命令时的系统时间,也就是计算定时器到期绝对时间的基准

说明2:异步处理定时器命令

① 用户可以在任务或ISR中发送定时器命令到定时器命令消息队列,之后由定时器任务接收并处理该命令,因此是一个异步处理流程

② 因为是异步处理,所以需要考虑实际处理定时器命令与发送定时器命令之间的时延

以启动定时器为例,可得到如下关系式,

定时器到期时间 = 定时器命令发送时间 + 定时器定时值

当定时器任务实际处理这条命令时,需要考虑当前时间与上面计算的定时器到期时间之间的关系,如果定时器已经到期,则需要立即执行定时器回掉函数;否则应该将定时器加入合适的定时器列表

3.2.2 中断级启动定时器

#define xTimerStartFromISR( xTimer, pxHigherPriorityTaskWoken ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START_FROM_ISR, \
( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )

与任务级调用相比,

① 不能带发送延时

② 需要通过pxHigherPriorityTaskWoken返回是否需要触发任务调度

3.3 停止定时器

// 任务级
#define xTimerStop( xTimer, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP, 0U, NULL, \
( xTicksToWait ) )// 中断级
#define xTimerStopFromISR( xTimer, pxHigherPriorityTaskWoken ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_STOP_FROM_ISR, 0, \
( pxHigherPriorityTaskWoken ), 0U )

停止定时器传递的xOptionalValue为0

3.4 复位定时器

// 任务级
#define xTimerReset( xTimer, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET, \
( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )// 中断级
#define xTimerResetFromISR( xTimer, pxHigherPriorityTaskWoken ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_RESET_FROM_ISR, \
( xTaskGetTickCountFromISR() ), ( pxHigherPriorityTaskWoken ), 0U )

复位定时器传递的xOptionalValue为系统当前时间,也是计算定时器到期绝对时间的基准

3.5 修改定时值

// 任务级
#define xTimerChangePeriod( xTimer, xNewPeriod, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD, \
( xNewPeriod ), NULL, ( xTicksToWait ) )// 中断级
#define xTimerChangePeriodFromISR( xTimer, xNewPeriod, \
pxHigherPriorityTaskWoken ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, \
( xNewPeriod ), ( pxHigherPriorityTaskWoken ), 0U )

修改定时值传递的xOptionalValue为新的定时值

3.6 删除定时器

// 任务级
#define xTimerDelete( xTimer, xTicksToWait ) \
xTimerGenericCommand( ( xTimer ), tmrCOMMAND_DELETE, 0U, NULL, \
( xTicksToWait ) )

删除定时值传递的xOptionalValue为0

说明:删除定时器只有任务级API

4. 定时器任务详解

上文已经列出了定时器任务的流程,就是在无限循环中轮流调用prvGetNextExpireTime / prvProcessTimerOrBlockTask / prvProcessReceivedCommands函数,下面逐一进行分析

在此之前,先来整体了解函数的调用关系,

prvTimerTask // 定时器任务函数
--> prvGetNextExpireTime // 获取pxCurrentTimerList队首定时器到期绝对时间
--> prvProcessTimerOrBlockTask // 处理到期定时器并阻塞在定时器命令队列上--> prvSampleTimeNow // 获取当前时间,如果溢出则交换定时器列表--> prvSwitchTimerLists // 交换定时器列表,并处理pxCurrentTimerList--> prvProcessExpiredTimer // 处理到期的定时器--> prvInsertTimerInActiveList // 将定时器加入定时器列表,已超时则直接返回--> vQueueWaitForMessageRestricted // 将任务加入定时器命令队列等待接收列表--> vTaskPlaceOnEventListRestricted
--> prvProcessReceivedCommands // 接收定时器命令并处理

4.1 prvGetNextExpireTime函数

// pxListWasEmpty:出参,标识当前定时器列表是否为空
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{// 如果定时器列表为空,则返回0xNextExpireTime = ( TickType_t ) 0U;}return xNextExpireTime;
}

① 如果pxCurrentTimerList为空,则通过pxListWasEmpty标识,并返回0

② 如果pxCurrentTimerList不为空,也通过pxListWasEmpty标识,并返回队首定时器的到期时间(到期的绝对时间)

4.2 prvProcessTimerOrBlockTask函数

4.2.1 函数主体

/*
* xNextExpireTime:prvGetNextExpireTime函数返回的下个定时器到期时间
* 如果pxCurrentTimerList为空,则返回0
* xListWasEmpty:prvGetNextExpireTime函数返回的pxCurrentTimerList是否为空
*/
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime,
BaseType_t xListWasEmpty )
{TickType_t xTimeNow;BaseType_t xTimerListsWereSwitched;// 关闭调度器// 保护定时器列表vTaskSuspendAll();{// 返回当前系统时间// 如果发生系统计时溢出,则处理pxCurrentTimerList上的所有定时器,// 并且会交换pxCurrentTimerList和pxOverflowTimerList列表,// 是否发生交换,通过xTimerListWereSwitched标识xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );if( xTimerListsWereSwitched == pdFALSE ){// 如果没有发生计时溢出并交换定时器列表// 如果pxCurrentTimerList不为空且有定时器到期,// 则处理到期的定时器if( ( xListWasEmpty == pdFALSE ) &&( xNextExpireTime <= xTimeNow ) ){( void ) xTaskResumeAll();prvProcessExpiredTimer( xNextExpireTime, xTimeNow );}else{// 进入该分支有2种可能,// 1. pxCurrentTimerList列表为空// 2. pxCurrentTimerList列表不为空,但队首定时器尚未到期// 如果pxCurrentTimerList列表为空,// 则判断pxOverflowTimerList列表是否为空// 如果2个定时器列表均为空,下面的等待就可以直接死等了if( xListWasEmpty != pdFALSE ){xListWasEmpty =listLIST_IS_EMPTY( pxOverflowTimerList );}// 进入这个分支的另一种情况是pxCurrentTimerList不为空// 但是队首任务尚未到期// 此时就保持了xListWasEmpty为pdFALSE,下面的延时就不会死等// 在定时器命令队列上进行阻塞等待// 此处通过xListWasEmpty决定是否要死等,// 也就是直接加入挂起(suspend)列表而不是延时(delay)列表// 如果pxCurrentTimerList为空,则xNextExpireTime为0// pxCurrentTimerList有未到期定时器时间线// --- xTimeNow --- xNextExpireTime --->// pxCurrentTimerList为空的时间线// xNextExpireTime(0)--- xTimeNow ----->// 1. 如果pxOverflowTimerList为空,那么就可以死等// 2. 如果pxOverflowTimerList不为空,那么此次等待就只会按照// 最大计时值未溢出的部分进行等待,被唤醒后就可以进行计时// 溢出的处理vQueueWaitForMessageRestricted( xTimerQueue,( xNextExpireTime - xTimeNow ), xListWasEmpty );if( xTaskResumeAll() == pdFALSE ){// 此处一定要触发任务调度,// 因为此时定时器任务要进入阻塞态,此时已经不在就绪列表中// 更正:上面的理解是有部分错误的// 只有当定时器命令消息队列为空时,才会时机加入// 等待接收队列并开始阻塞// 但是由于可能会阻塞,所以此处仍需要判断xTaskResumeAll// 函数的返回值,确保此时任务被调度走portYIELD_WITHIN_API();}}}else{// 如果发生溢出并交换了定时器列表// 说明pxCuurentTimerList上的定时器均已被处理// 则直接恢复调度器并退出// 此时定时器列表已经交换,因此需要进入下一轮prvTimerTask循环// 判断交换后的pxCurrentTimerList上的定时器状态( void ) xTaskResumeAll();}}
}

说明1:仅有prvProcessTimerOrBlockTask函数可以让定时器任务进入阻塞状态,此时所有到期定时器已经处理,并且定时器命令队列中没有消息要处理

说明2:系统计时未溢出时,prvProcessTimerOrBlockTask处理流程伪代码

对应prvProcessTimerOrBlockTask函数中,没有发生定时器队列交换的场景,即if (xTimerListsWereSwitched == pdFALSE)条件成立的分支

if (如果有定时器到期)
{判断定时器到期时,下面2个条件必须同时满足,1. xListWasEmpty == pdFALSE,也就是pxCurrentTimerList不为空2. xNextExpireTime <= xTimeNow需要满足第1个条件,是因为当pxCurrentTimerList为空时,xNextExpireTime会被置为0,此时肯定也是满足第2个条件的
}
else
{该分支对应没有定时器到期,如果此时定时器命令队列中没有消息,则需要将定时器任务阻塞在定时器命令列表的等待接收列表中此处更进一步,在pxCurrentTimerList为空的情况下,又判断了pxOverflowTimerList是否为空,如果定时器溢出列表也为空的话,后续的阻塞就可以设置为"死等"了,即加入任务挂起列表,而不是延时等待列表
}

4.2.2 prvSampleTimeNow函数

// pxTimerListsWereSwitched:出参,返回是否交换了定时器列表
// 用于处理定时器定时值溢出
static TickType_t prvSampleTimeNow(
BaseType_t * const pxTimerListsWereSwitched )
{TickType_t xTimeNow;// 静态局部变量// 用于记录上次调用prvSampleTimeNow函数的系统时间PRIVILEGED_DATA static TickType_t xLastTime = ( TickType_t ) 0U;xTimeNow = xTaskGetTickCount();// 正常时间线// --- xLastTime --- xTimeNow -------->// 溢出时间线// --- xTimeNow --------- xLastTime -->if( xTimeNow < xLastTime ){// 如果系统计时溢出,则交换定时器列表// 同时会处理pcCurrentTimerList上的所有剩余任务prvSwitchTimerLists();*pxTimerListsWereSwitched = pdTRUE;}else{*pxTimerListsWereSwitched = pdFALSE;}// 记录调用prvSampleTimeNow函数的系统时间xLastTime = xTimeNow;return xTimeNow;
}

4.2.3 prvSwitchTimerList函数

static void prvSwitchTimerLists( void )
{TickType_t xNextExpireTime, xReloadTime;List_t *pxTemp;Timer_t *pxTimer;BaseType_t xResult;// 由于系统计时已经溢出,所以要交换定时器列表// 在交换之前,要先处理完pxCurrentTimerList上的所有剩余定时器// 这种情况是可能发生的while( listLIST_IS_EMPTY( pxCurrentTimerList ) == pdFALSE ){// 在循环中处理完pxCurretnTimerList上的所有剩余定时器// 取出队首定时器的绝对到期时间// 目的是为了设置周期定时器的下次绝对到期时间xNextExpireTime =listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );// 队首定时器出队pxTimer =( Timer_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );( void ) uxListRemove( &( pxTimer->xTimerListItem ) );// 执行定时器回调函数pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE ){// 周期性定时器// 计算下次定时器绝对到期时间xReloadTime = ( xNextExpireTime +pxTimer->xTimerPeriodInTicks );if( xReloadTime > xNextExpireTime ){// 如果下次定时器绝对到期时间没有溢出,// 更准确地说是定时器已经到期// 则将定时器重新按绝对到期时间升序加入pxCurrentTimerList// 这样就会再次进入while循环,再次执行定时器回调函数// 直到该定时器下次绝对到期时间溢出// 从语义上说,在交换定时器列表时,pxCurrentTimerList上的// 所有定时器均到期(包括反复到期),而所有到期的定时器均要处理listSET_LIST_ITEM_VALUE( &( pxTimer->xTimerListItem ),xReloadTime );listSET_LIST_ITEM_OWNER( &( pxTimer->xTimerListItem ),pxTimer );vListInsert( pxCurrentTimerList,&( pxTimer->xTimerListItem ) );}else{// 如果下次定时器绝对到期时间溢出,// 更准确地说是定时器没有到期// 则不能将定时器重新加入pxCurrentTimerList// 需要等待交换定时器列表后才能加入正确的定时器列表// 所以向定时器命令队列发送start_dont_trace命令// xOptionalValue传递的是定时器最后一次被处理的到期时间// 也就是下一次的到期时间的计算基准xResult = xTimerGenericCommand( pxTimer,tmrCOMMAND_START_DONT_TRACE, xNextExpireTime, NULL,tmrNO_DELAY );configASSERT( xResult );( void ) xResult;}}}// 交换定时器列表pxTemp = pxCurrentTimerList;pxCurrentTimerList = pxOverflowTimerList;pxOverflowTimerList = pxTemp;
}

说明1:特别要注意prvSwitchTimerLists函数被调用时的大前提,就是定时器计时已经溢出,所以目前在pxCurrentTimerList上的定时器肯定都已经到期

说明2:定时器重新入队时间线

由于目前的大背景是系统计时已经溢出,所以仍然在溢出前的pxCurrentTimerList上的定时器需要全部被处理,这里还包括autoreload后仍在pxCurrentTimerList上的情况

4.2.4 prvProcessExpiredTimer函数

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 ) );// 如果是周期定时器,则需要将定时器重新加入定时器列表// 1. 如果下个周期的定时器没有到期,则直接加入对应定时器列表// 2. 如果下个周期的定时器已经到期,则发送start_dont_trace命令,// 这样可以触发定时器回调函数被执行一次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;}}// 执行定时器回调函数pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );
}

说明1:prvProcessExpiredTimer函数每次仅处理一个到期定时器,但是由于不会进入阻塞,所以可以再次进入定时器任务的循环,进而处理后续到期的定时器

说明2:我们来分析一下prvProcessExpiredTimer函数调用prvInsertTimerInActiveList函数时传递的参数,

① pxTimer:要操作的定时器

② xNextExpireTime + pxTimer->xTimerPeriodInTicks:定时器下次到期时间绝对值

③ xTimeNow:当前时间,即处理这个到期定时器的时间

④ xNextExporeTime:定时器本次到期的绝对时间,也是计算下次到期时间的基准

4.2.5 prvInsertTimerInActiveList函数

这个函数是软件定时器模块中最复杂的函数,他用于将定时器加入正确的定时器列表,如果定时器已经超时,也要予以标识

/*
* xNextExpiryTime:要设置的定时器到期时间,由计算基准 + 定时周期得到
* xTimeNow:当前系统时间
* xCommandTime:计算定时器到期时间的基准
* 这里要重点说明一下xCommandTime参数,根据上文,定时器API都是通过向定时器命令
* 队列发送消息实现的,这里的xCommandTime就是指发送这些命令的时间
* 而xTimeNow就是处理这条消息时的系统时间,而xCommandTime和xTimeNow之间
* 是可能有差值的,甚至可能定时器已经到期
* 如果定时器已经到期,则直接执行定时器回调函数即可,无需在本函数加入定时器列表
*
* prvInsertTimerInActiveList函数的返回值,就是标识要插入的定时器是否已经
* 到期。如果返回pdTRUE,就说明在将定时器加入活动定时器列表前已经到期
*/
static BaseType_t prvInsertTimerInActiveList( Timer_t * const pxTimer,
const TickType_t xNextExpiryTime, const TickType_t xTimeNow,
const TickType_t xCommandTime )
{BaseType_t xProcessTimerNow = pdFALSE;// 设置定时器的下次绝对到期时间listSET_LIST_ITEM_VALUE( &( pxTimer->xTimerListItem ),xNextExpiryTime);listSET_LIST_ITEM_OWNER( &( pxTimer->xTimerListItem ), pxTimer );// 定时器已经到期,或者定时器到期时间溢出if( xNextExpiryTime <= xTimeNow ){// 定时器已经到期,无需加入定时器列表// 对应时间线// --- xCommandTime --- xNextExpiryTime --- xTimeNow --->if( ( ( TickType_t ) ( xTimeNow - xCommandTime ) ) >=pxTimer->xTimerPeriodInTicks )xProcessTimerNow = pdTRUE;}// 定时器到期时间溢出,需要加入pxOverflowTimerList// 对应时间线// --- xNextExpiryTime --- xCommandTime --- xTimeNow --->else{vListInsert( pxOverflowTimerList,&( pxTimer->xTimerListItem ) );}}// 定时器未到期,或者计时溢出(此时定时器已经到期)else{// 定时器已到期,无需加入定时器列表// 对应时间线// --- xTimeNow --- xCommandTime --- xNextExpiryTime --->if( ( xTimeNow < xCommandTime ) &&( xNextExpiryTime >= xCommandTime ) ){xProcessTimerNow = pdTRUE;}else{// 定时器尚未到期,且定时未溢出,插入pxCurrentTimerList即可// 对应时间线// --- xCommandTime --- xTimeNow --- xNextExpiryTime --->vListInsert( pxCurrentTimerList, &( pxTimer->xTimerListItem ));}}return xProcessTimerNow;
}

说明1:prvInsertTimerInActiveList函数的调用时机

prvInsertTimerInActiveList函数的核心就是分析xNextExpiryTime / xTimeNow / xCommandTime之间的位置关系,进而判断需要reload的定时器是否到期

我们这里分析的是prvProcessExpiredTimer函数对他的调用,在这种情况下,有2个时间的相对位置是固定的,也就是xCommandTime一定小于等于xTimeNow,因为上一级函数已经判断出该定时器是超时的,且处理该定时器时计时没有溢出

但是该函数还会被prvProcessReceivedCommands函数调用,此时上面的2个时间之间没有固定的位置关系,需要通盘考虑3个时间的关系

说明2:时间线分析

① 从代码的实现分析,首先是判断了xNextExpiryTime与xTimeNow的位置关系

② xNextExpiryTime <= xTimeNow分支分析

在这种情况下,有2种可能,

a. 定时器reload已经到期

b. 定时器reload溢出,需要加入pxOverflowTimerList

具体情况如下图所示,

这里补充说明一下第③种情况,此时xTimeNow < xCommandTime,那么xTimeNow - xCommandTime在数值上就等于(xTimeNow + portMAX_DELAY - xCommandTime),该值也是超过pxTimer->xTimerPeriodInTicks的(此时xTimerPeriodInTicks的值为xNextExpiryTime + portMAX_DELAY - xCommandTime + 1)

这里的边界条件判断还是比较烧脑的,需要一些补码计算基础

③ xNextExpiryTime > xTimeNow分支分析

在这种情况下,也有2种可能,

a. 定时器reload尚未到期,但是没有溢出,需要加入pxCurrentTimerList

b. 定时器reload已经到期,此时xTimeNow计时溢出

具体情况如下图所示,

在实现代码时,应该是先判断图示的位置关系,之后再写判断式,可见代码的实现非常凝练,尽可能减少了判断次数

4.2.6 vQueueWaitForMessageRestricted函数

// xWaitIndefinitely:标识是否需要死等
void vQueueWaitForMessageRestricted( QueueHandle_t xQueue,
TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely )
{Queue_t * const pxQueue = ( Queue_t * ) xQueue;// 队列上锁prvLockQueue( pxQueue );if( pxQueue->uxMessagesWaiting == ( UBaseType_t ) 0U ){// 如果没有消息可供接收,则阻塞定时器任务vTaskPlaceOnEventListRestricted(&( pxQueue->xTasksWaitingToReceive ), xTicksToWait,xWaitIndefinitely );}prvUnlockQueue( pxQueue );
}void vTaskPlaceOnEventListRestricted( List_t * const pxEventList,
TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely )
{configASSERT( pxEventList );// 将任务事件列表项加入定时器命令队列等待接收列表vListInsertEnd( pxEventList, &( pxCurrentTCB->xEventListItem ) );// 如果要死等,则将定时值设置为portMAX_DELAYif( xWaitIndefinitely != pdFALSE ){xTicksToWait = portMAX_DELAY;}// 将任务加入延时列表// 如果是死等,则会加入挂起列表prvAddCurrentTaskToDelayedList( xTicksToWait, xWaitIndefinitely );
}

注意:有restricted后缀的函数,不应被用户的应用程序调用,仅供内核函数调用

4.3 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( 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;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 );}}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */break;default:/* Don't expect to get here. */break;}}}
}

4.4 定时器任务工作机制总结

4.4.1 总体机制

① 定时器任务大部分时间都被阻塞在定时器命令队列的等待接收列表上,只有2种情况会将其唤醒,

a. 有定时器任务到期(或者portMAX_DELAY延时到期)

b. 接收到定时器命令

② 因此定时器任务在被唤醒后,会做如下2件事,

a. 尝试接收定时器命令

b. 处理到期的定时器

当所有到期的定时器均被处理完成后,将重新进入阻塞态

4.4.2 交换定时器列表处理pxCurrentTimerList

根据定时器任务的总体机制,在交换定时器列表时,正常是不应该有到期定时器被遗留在pxCurrentTimerList中的

发生这种问题的场景,是定时器的到期绝对时间临界portMAX_DELAY,而定时器任务被唤醒后,由于被中断或更高优先级的任务抢占,当要处理该定时器时,系统计时已经溢出。此时就会发生,由于计时溢出要交换定时器列表时,还有定时器在pxCurrentTimerList中

4.4.3 定时器回调函数编程要求

定时器回调函数是在定时器任务中被执行的,而定时器任务是可以被中断或者更高优先级的任务抢占的,所以在定时器回调函数中不能,

① 调用任何可能导致阻塞的函数

② 不能长时间占据CPU

否则会影响其他定时器任务的执行

在定时器回调函数中,也应快进快出

FreeRTOS源码分析与应用开发06:软件定时器相关推荐

  1. FreeRTOS源码分析与应用开发02:任务管理

    目录 1. 任务概述 1.1 任务表示 1.2 任务状态 1.2.1 运行态 1.2.2 就绪态 1.2.3 阻塞态 1.2.4 挂起态 1.3 任务优先级 1.3.1 FreeRTOS优先级配置 1 ...

  2. FreeRTOS源码分析与应用开发01:中断配置与临界段

    目录 1. 异常与中断的基本概念 1.1 异常分类 1.2 中断概述 1.2.1 中断处理宜短暂 1.2.2 临界段影响中断实时性 1.3 中断硬件基础 1.3.1 外设 1.3.2 中断控制器 1. ...

  3. FreeRTOS源码分析与应用开发07:事件标志组

    目录 1. 概述 2. 事件标志组类型 3. 创建事件标志组 4. 删除事件标志组 5. 设置事件标志位 5.1 任务级设置 5.2 中断级设置 6. 清除事件标志位 6.1 任务级清除 6.2 中断 ...

  4. FreeRTOS源码分析与应用开发04:消息队列

    目录 1. 队列结构 2. 创建队列 2.1 动态创建队列 2.1.1 xQueueCreate函数 2.1.2 xQueueGenericCreate函数 2.1.3 xQueueGenericRe ...

  5. FreeRTOS源码分析与应用开发09:低功耗Tickless模式

    目录 1. STM32F4低功耗模式简介 2. Tickless模式详解 2.1 如何降低功耗 2.2 关闭SysTick的问题与解决方案 2.2.1 关闭SysTick导致系统节拍计数器停止 2.2 ...

  6. FreeRTOS源码分析与应用开发11(完):编译、链接与部署

    目录 1. 存储设备布局 2. 链接器脚本 2.1 链接器脚本生成 2.2 链接器脚本分析 2.2.1 分散加载文件 2.2.2 加载区 & 运行区 2.2.3 ER_IROM1运行区分析 2 ...

  7. FreeRTOS源码分析与应用开发10:内存管理

    目录 1. 概述 1.1 RTOS中内存分配特点 1.2 内存堆(heap space)来源 1.2.1 ucHeap数组 1.2.2 链接器设置的堆 1.2.3 多个非连续内存堆 1.3 关于字节对 ...

  8. FreeRTOS源码分析与应用开发08:任务通知

    目录 1. 概述 1.1 任务通知概念 1.2 任务通知控制结构 2. 发送任务通知 2.1 任务级发送 2.2 中断级发送 2.2.1 xTaskNotifyFromISR函数 2.2.2 vTas ...

  9. FreeRTOS源码分析与应用开发05:信号量

    目录 1. 信号量概述 1.1 信号量概念 1.2 4种信号量 1.2.1 二值信号量 1.2.2 计数信号量 1.2.3 互斥信号量 1.2.4 递归互斥信号量 1.3 信号量相关控制结构 1.3. ...

最新文章

  1. Python基于MASK信息抽取ROI子图并构建基于迁移学习(densenet)的图像分类器实战(原始影像和mask文件都是二维的情况)
  2. 北斗芯片服务器,北斗芯片:GPS定位系统,正是再见!你期待吗?
  3. 动态规划 BZOJ1584 [Usaco2009 Mar] Cleaning Up 打扫卫生
  4. Tree-CNN:一招解决深度学习中的「灾难性遗忘」
  5. 【C语言简单说】十七:数组(补)
  6. FlinkAPI_Environment_输入源_算子转化流程
  7. python中难的算法_Python算法很难吗?python神书《算法图解》PDF电子版分享给你
  8. 鸿蒙系统是不是推迟发布了,鸿蒙系统2.0来了,华为Mate40推迟发布
  9. [导入]Nutch 简介 [官方]
  10. Q106:Linux系统下安装编译PBRT-V3
  11. 决策树系列(二)——剪枝
  12. Quart2D文字图像绘制
  13. 使用VMware创建一个虚拟机,并安装乌班图系统
  14. 芯片代理商哪家专业 品质是否有保障
  15. FreeRTOS原理剖析:空闲任务分析
  16. Windows8内核模式下开发NDIS应用 NDIS Filter讲解
  17. 安卓 Installation via USB is disabled
  18. ios自制电话本-swift
  19. 如何安装Redis?
  20. mysql 查询当前年份

热门文章

  1. php 连接socket服务器_PHP-Socket服务端客户端发送接收通信实例详解
  2. ios 横向滚轮效果_iOS列表滚动视差效果
  3. php 添加透明水印,php加水印的代码(支持半透明透明打水印,支持png透明背景)
  4. python 键盘输入int_Python编程 Python如何获取数据
  5. 周长相等的正方形面积一定相等_三年级下册数学期末重点——面积
  6. 使用mysql命令还原student表_自用mysql自带命令实现数据库备份还原的方法
  7. python关键词提取_如何从Python格式字符串中提取关键字? - python
  8. python数据字典排序_Python自动处理数据字典(Python是3.6版本)
  9. CentOS 修改主机名(host)
  10. Mybatis-plus 将字段更新为null