**
文章大部分内容摘抄至B站的韦东山老师的深入了解FreeRTOS操作系统教程,若有不理解的地方,可点击链接学习韦老师的视频。
**
**
我们已经学习完了所有的通信方式,现在我们来介绍一下软件定时器
**
文章目录
前言
定时器可以说是每个 MCU 都有的外设,有的 MCU 其定时器功能异常强大,比如提供 PWM、输入捕获等功能。但是最常用的还是定时器最基础的功能——定时,通过定时器来完成需要周期性处理的事务。
MCU 自带的定时器属于硬件定时器,不同的 MCU 其硬件定时器数量不同,因为要考虑成本的问题。FreeRTOS 也提供了定时器功能,不过是软件定时器,软件定时器的精度肯定没有硬件定时器那么高,但是对于普通的精度要求不高的周期性处理的任务来说够了。当 MCU 的硬件定时器不够的时候就可以考虑使用 FreeRTOS 的软件定时器。我们这一章就重点学习一下软件定时器的工作原理和机制。
一、软件定时器是什么?
FreeRTOS中的软件定时器是一种基于软件的计时器,提供了一种轻量级的任务调度机制,允许用户在RTOS中创建重复定时器和单次定时器。软件定时器可以被用来执行周期性任务,在指定的时间间隔内执行某些任务,或者在指定时间点执行某些任务。一旦定时器被启动,RTOS会定期检查定时器是否已经到期并执行相应的任务。
软件定时器的使用方法也很简单,用户只需要在FreeRTOS中创建一个定时器,设置定时器的周期和需要执行的任务函数,通过启动和停止定时器来控制任务执行的时间。相比于硬件定时器,软件定时器可以更加灵活地控制任务执行的时间,并且便于移植到不同的硬件平台上。
二、创建软件定时器
创建软件定时器可调用xTimerCreate这个函数,我们先了解一下软件定时器的句柄:Timer_t
typedef struct tmrTimerControl //定时器控制块
{
const char * pcTimerName;//定时器名称
ListItem_t xTimerListItem; //定时器链表
TickType_t xTimerPeriodInTicks;//定时器的时间
void * pvTimerID;//用于标识计时器的ID。这允许在同一回调用于多个计时器时识别计时器。
TimerCallbackFunction_t pxCallbackFunction; //定时器回调函数
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTimerNumber;//由跟踪工具(如FreeRTOS+trace)分配的ID
#endif
uint8_t ucStatus; //保留一些位,表示计时器是否静态分配,以及它是否处于活动状态。
} xTIMER;
typedef xTIMER Timer_t;
其中的ucStatus有三个状态:通过按位或操作改变这个值
#define tmrSTATUS_IS_ACTIVE ( ( uint8_t ) 0x01 )
#define tmrSTATUS_IS_STATICALLY_ALLOCATED ( ( uint8_t ) 0x02 )
#define tmrSTATUS_IS_AUTORELOAD ( ( uint8_t ) 0x04 )
其中具有四个列表,当前定时器列表,溢出定时器列表,两个当前的活动列表,后面列表之间会判断进行切换
PRIVILEGED_DATA static List_t xActiveTimerList1;
PRIVILEGED_DATA static List_t xActiveTimerList2;
PRIVILEGED_DATA static List_t * pxCurrentTimerList;
PRIVILEGED_DATA static List_t * pxOverflowTimerList;
下面是原码分析:
#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 )
{
/* 到目前为止,状态为零,因为计时器不是静态创建的,也没有启动。可在prvInitializeNewTimer中设置自动重新加载位。*/
pxNewTimer->ucStatus = 0x00;
prvInitialiseNewTimer( pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, pxNewTimer );
}
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 ) PRIVILEGED_FUNCTION;
BaseType_t xTimerCreateTimerTask( void )
{
BaseType_t xReturn = pdFAIL;
/*如果configUSE_TIMERS设置为1,则在启动调度程序时调用此函数。检查计时器服务任务使用的基础结构是否已创建/初始化。如果已经创建了计时器,那么初始化将已经执行。 */
prvCheckForValidListAndQueue();
if( xTimerQueue != NULL )
{
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )//静态创建
{
StaticTask_t * pxTimerTaskTCBBuffer = NULL;
StackType_t * pxTimerTaskStackBuffer = NULL;
uint32_t ulTimerTaskStackSize;
vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );
xTimerTaskHandle = xTaskCreateStatic( prvTimerTask,
configTIMER_SERVICE_TASK_NAME,
ulTimerTaskStackSize,
NULL,
( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
pxTimerTaskStackBuffer,
pxTimerTaskTCBBuffer );
if( xTimerTaskHandle != NULL )
{
xReturn = pdPASS;
}
}
#else //动态创建
{
xReturn = xTaskCreate( prvTimerTask,
configTIMER_SERVICE_TASK_NAME,
configTIMER_TASK_STACK_DEPTH,
NULL,
( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
&xTimerTaskHandle );
}
#endif
}
else
{
mtCOVERAGE_TEST_MARKER();
}
configASSERT( xReturn );
return xReturn;
}
可以在这个函数中看到了创建一个定时器任务,我们进入这个函数看一下:
prvTimerTask
static portTASK_FUNCTION( prvTimerTask, pvParameters )
{
TickType_t xNextExpireTime;
BaseType_t xListWasEmpty;
/*只是为了避免编译器发出警告。 */
( void ) pvParameters;
#if ( configUSE_DAEMON_TASK_STARTUP_HOOK == 1 )
{
extern void vApplicationDaemonTaskStartupHook( void );
/*允许应用程序编写器在任务开始执行时在此任务的上下文中执行一些代码。如果应用程序包含初始化代码,则这一点非常有用,该代码将受益于在调度程序启动后执行 */
vApplicationDaemonTaskStartupHook();
}
#endif
for( ; ; )//循环
{
/*查询计时器列表,查看其中是否包含任何计时器,如果包含,则获取下一个计时器的过期时间 */
xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
/*如果计时器已过期,请处理它。否则,请阻止此任务
直到定时器到期或者接收到命令。 */
prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
/*清空命令队列,(准确来说是处理队列中的命令) */
prvProcessReceivedCommands();
}
}
可以看出这个任务其实就是在一个循环里面一直处理队列的命令
其中一共运行了三个函数:分别是prvGetNextExpireTime,prvProcessTimerOrBlockTask,prvProcessReceivedCommands三个函数;我们重点解析一下这三个函数的原码:
三、软件定时器原码解析
一共有三个函数我们一一介绍:
prvGetNextExpireTime
static TickType_t prvGetNextExpireTime( BaseType_t * const pxListWasEmpty )
{
TickType_t xNextExpireTime;
/* 计时器按过期时间顺序列出,列表的标题引用将首先过期的任务。获取最接近到期时间的计时器的到期时间。如果没有活动计时器,则只需将下一个过期时间设置为0。这将导致在刻度计数溢出时取消阻止此任务,此时计时器列表将被切换,并且可以重新评估下一个到期时间 */
//判断当前定时器列表内是否为空,若没有定时器,则返回pdTRUE,若有定时器,返回pdFALSE
*pxListWasEmpty = listLIST_IS_EMPTY( pxCurrentTimerList );
if( *pxListWasEmpty == pdFALSE )//存在定时器
{
xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );//获取定时器的定时值,存储在xNextExpireTime
}
else//不存在定时器,所以设置为0,计时器列表将被切换
{
/*确保在刻度计数滚动时取消阻止任务。*/
xNextExpireTime = ( TickType_t ) 0U;
}
return xNextExpireTime;
}
这个函数是用来获取当前定时器列表的首项,也就是定时器的超时时间。将这个返回值作为参数传入prvProcessTimerOrBlockTask这个函数中。
prvProcessTimerOrBlockTask
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime,
BaseType_t xListWasEmpty )
{
TickType_t xTimeNow;
BaseType_t xTimerListsWereSwitched;
vTaskSuspendAll();//挂起调度器
{
/*现在获取时间来评估计时器是否已过期。如果获取时间导致列表切换,则不要处理此计时器,因为切换列表时保留在列表中的任何计时器都将在prvSampleTimeNow()函数中进行处理 */
//判断是否有进行列表切换,如果发生列表切换,xTimerListsWereSwitched 为pdTRUE,否则为pdFALSE,并且获取当前的tick返回给xTimeNow,这个函数我们后面会介绍
xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
if( xTimerListsWereSwitched == pdFALSE )//并没有切换列表,说明定时器没有发生交换,也就是存在定时器
{
/* 刻度计数未溢出,判断计时器是否已过期 */
//xListWasEmpty 是传递进来的,定时器列表不为空,并且超时时间已经小于现在的时间了,说明定时器已经超时,需要处理定时器任务
if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )
{
( void ) xTaskResumeAll();
//立刻恢复调度器
prvProcessExpiredTimer( xNextExpireTime, xTimeNow );
//处理超时的定时器任务,这个函数我们后面会介绍
}
else//运行到这里有两种可能,可能是定时器列表空,也有可能是还没有超时
{
/*刻度计数尚未溢出,下一个过期时间还没有到。因此,这项任务应该等待下一个过期时间或接收命令(以先到者为准)。除非xNextExpireTime>xTimeNow,否则无法到达以下行,除非当前计时器列表为空。 */
if( xListWasEmpty != pdFALSE )
{
/*判断是哪个当前计时器列表为空-溢出列表是否也为空? */
xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );
}
//关键所在,这个就是将定时器任务阻塞的核心,这里队列等待消息用来处理,时间设置的是xNextExpireTime - xTimeNow,也就是在xNextExpireTime - xTimeNow这个时间段内,如果没有消息来我也不等了,我直接跳过重新运行,如果消息来了,那就处理。
vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );
if( xTaskResumeAll() == pdFALSE )
{
/*恢复任务失败则重新发起一次任务调度 */
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
( void ) xTaskResumeAll();
}
}
}
这个函数先处理定时器是否交换了定时器队列,判断是否超时,超时了则处理,如果没有超时,则在超时时间-当前时间内阻塞等待,如果没有消息到来则跳过恢复调度器重新获取一次定时器时间,循环进行。接下来我们看一下上面几个函数的原码:
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;//赋值给xLastTime
return xTimeNow;
}
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 ) );
traceTIMER_EXPIRED( pxTimer );
/*如果计时器是自动重新加载计时器,则计算下一个到期时间,并将计时器重新插入活动计时器列表中*/
if( ( pxTimer->ucStatus & tmrSTATUS_IS_AUTORELOAD ) != 0 )
{
/*计时器是使用相对于当前时间以外的任何时间的时间插入到列表中的。因此,它将被插入到相对于此任务认为现在的时间的正确列表中。 */
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//未设置自动重装
{
pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE;
mtCOVERAGE_TEST_MARKER();
}
//运行定时器回调函数
pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );
}
prvInsertTimerInActiveList
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 )//判断是否超时
{
/*从发出启动/重置计时器的命令到处理该命令之间是否经过了到期时间?*/
if( ( ( TickType_t ) ( xTimeNow - xCommandTime ) ) >= pxTimer->xTimerPeriodInTicks )
{
/*发出命令和处理命令之间的时间实际上超过了定时器周期 */
xProcessTimerNow = pdTRUE;
}
else//插入到pxOverflowTimerList这个列表
{
vListInsert( pxOverflowTimerList, &( pxTimer->xTimerListItem ) );
}
}
else
{
if( ( xTimeNow < xCommandTime ) && ( xNextExpiryTime >= xCommandTime ) )
{
/*如果自发出命令以来,刻度计数已溢出,但到期时间尚未溢出,则计时器必须已超过其到期时间,应立即进行处理。 */
xProcessTimerNow = pdTRUE;
}
else//插入到pxCurrentTimerList这个列表
{
vListInsert( pxCurrentTimerList, &( pxTimer->xTimerListItem ) );
}
}
return xProcessTimerNow;
}
xTimerGenericCommand
这个函数是对定时器队列发送指令
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 )
{
/*向计时器服务任务发送命令以启动xTimer计时器。 */
//组装消息体
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;
}
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 )
/*xMessage不必初始化,因为它是传出的,而不是传入的,除非xQueueReceive()返回pdTRUE,否则不会使用它。*/
{
#if ( INCLUDE_xTimerPendFunctionCall == 1 )
{
/*负命令是挂起的函数调用,而不是定时器命令 */
if( xMessage.xMessageID < ( BaseType_t ) 0 )
{
const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );
/* 计时器使用xCallbackParameters成员请求执行回调。检查回调是否为NULL。*/
configASSERT( pxCallback );
/* 运行回调函数 */
pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
/*正的命令是定时器命令,而不是挂起的函数调用 */
if( xMessage.xMessageID >= ( BaseType_t ) 0 )
{
/* 消息使用xTimerParameters成员处理软件计时器。 */
pxTimer = xMessage.u.xTimerParameters.pxTimer;
if( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) == pdFALSE ) //只有当NULL被传递到宏中时,强制转换才是多余的。
{
/* 移除定时器列表 */
( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
traceTIMER_COMMAND_RECEIVED( pxTimer, xMessage.xMessageID, xMessage.u.xTimerParameters.xMessageValue );
/*在这种情况下,不使用xTimerListsWereSwitched参数,但它必须存在于函数调用中。prvSampleTimeNow()必须在从xTimerQueue接收到消息后调用,因此优先级较高的任务不可能在消息队列中添加时间早于计时器守护进程任务的消息(因为它在设置xTimeNow值后抢占了计时器守护进程任务)。 */
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:
/* 开启或重启 */
pxTimer->ucStatus |= tmrSTATUS_IS_ACTIVE;
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->ucStatus & tmrSTATUS_IS_AUTORELOAD ) != 0 )//判断是否重装载值
{
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:
/* 计时器已从活动列表中删除。 */
pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE;
break;
case tmrCOMMAND_CHANGE_PERIOD:
case tmrCOMMAND_CHANGE_PERIOD_FROM_ISR:
pxTimer->ucStatus |= tmrSTATUS_IS_ACTIVE;
pxTimer->xTimerPeriodInTicks = xMessage.u.xTimerParameters.xMessageValue;
configASSERT( ( pxTimer->xTimerPeriodInTicks > 0 ) );
/*新时期并没有真正的参考,可以比旧时期更长也可以更短。因此,命令时间被设置为当前时间,由于周期不能为零,下一个到期时间只能在未来,这意味着(与上面的xTimerStart()情况不同)这里不需要处理失败情况 */
( void ) prvInsertTimerInActiveList( pxTimer, ( xTimeNow + pxTimer->xTimerPeriodInTicks ), xTimeNow, xTimeNow );
break;
case tmrCOMMAND_DELETE:
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
{
/* 计时器已经从活动列表中删除,如果内存是动态分配的,只需释放内存即可。 */
if( ( pxTimer->ucStatus & tmrSTATUS_IS_STATICALLY_ALLOCATED ) == ( uint8_t ) 0 )
{
vPortFree( pxTimer );
}
else
{
pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE;
}
}
#else
/*如果未启用动态分配,则不可能对内存进行动态分配。因此,无需释放内存,只需将计时器标记为“未激活”即可。 */
pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE;
}
#endif
break;
default:
break;
}
}
}
}
总结
这篇主要就是对软件定时器的介绍,主要是看原码,原码中函数一个嵌套又一个。也能看出来软件定时器实际上也是一个任务,因此我们使用软件定时器的时候,根据自己情况配置优先级,它的定时精度不如硬件定时器,因为它容易被中断打断或者被其他高优先级的任务抢占。