✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转FreeRTOS
💬保持学习、保持热爱、认真分享、一起进步!!!
目录
- 前言
- 一、软件定时器的简介
- 二、软件定时器的创建
- 1.软件定时器的创建
- 2.软件定时器的运行机制
- 三、定时器源码分析
- 1.通用发送命令函数xTimerGenericCommand()
- 2.守护任务的作用portTASK_FUNCTION()
- 1.prvGetNextExpireTimeh函数源码:
- 2.prvProcessTimerOrBlockTask函数源码:
- 3.处理命令函数prvProcessReceivedCommands()
- 四.总结
前言
本文将将讲解FreeRTOS最后一个应用-定时器,讲解完定时器,FreeRTOS的教程也就基本完结,当然还有一些中断、内存管理还需了解,之后还会继续更新的,本文要讲解的软件定时器总结起来就一句话-两条定时器链表和一条消息队列,一个守护任务,如果你前面的两天延时链表(任务的阻塞与唤醒)与队列理解很深刻,那软件定时就非常简单了。
如果前面的知识没有完全理解,还请看:
FreeRTOS-时间片与任务阻塞的实现
FreeRTOS-消息队列详解
一、软件定时器的简介
定时器的概念想必大家都知道,就像闹钟一样,时间一到就去做某些事情,定时器又分为硬件定时器和软件定时器,硬件定时器:就像STM32单片机都有几个定时器外设,而且功能也十分强大,可以输出PWM,输入捕获等高级功能,但每个定时器都可以进行定时,软件定时器:像FreeRTOS提供的定时器,软件定时器顾名思义是由写代码来实现,而且只能用来定时器。
硬件定时器与软件定时器的区别(重点):
1.硬件定时器是由外部晶振提供时钟,定时十分精准,而且精度一般都可以达到微秒级,而FreeRTOS软件定时器的运行基于一个守护任务,它可以被其他中断或优先级比它高的任务打断的,且软件定时器的精度是基于系统时钟SysTick的,一般达不到微秒级别的。
2.硬件定时器不仅具有定时功能,还有输出PWM,输入捕获高级功能,而软件定时器只有定时功能。
3.硬件定时器,当时间到达会触发一次中断,而软件定时器当时间到达会执行回调函数(在守护任务中)
既然硬件定时器精度高,功能这么多,还需要软件定时器干嘛?
原因就是硬件定时器是稀缺资源用一个少一个,而且使用了定时的高级功能之后一般就不能用来定时了,而实际开发中需要很多定时器(定时采集传感器数据、定时上传数据、、),而软件定时器如果在单片机内存足够的情况下可以创建无数个定时器(一个软件定时器只需一个定时器结构体的内存,当然要保证定时器能正常运转还需要两条定时器链表、一个队列)
所以软件定时器适合于对定时器精度要求不高的周期性任务
二、软件定时器的创建
1.软件定时器的创建
1.定时器结构体
1.pcTimerName: 定时器名字,只供调试作用。
2.xTimerListItem:定时器链表项,用于将定时器挂入定时器链表之中(等待超时)。
3.xTimerPeriodInTicks:定时器周期,单位为tick(系统节拍周期)。
定时:分为单次定时和周期定时,就像闹钟一样单次的定时,时间一到事情做完定时器就停止运行,而周期定时:假设每隔20s触发一次,是周期执行的。
4.pvTimerID:用于标识计时器的 ID,这允许在对多个定时器使用相同的回调函数时识别是那个定时器超时了。(其实就相当于在中断服务函数中,判断是那个中断来临了是一个道理)。
5.pxCallbackFunction:定时器的回调函数(或者叫超时函数),每个定时器都由用户指定一个回调函数(功能由用户实现),每当定时器超时,守护任务则会去调用该定时器的回调函数。
6.ucStatus:定时器的状态,如下图所示:
定时器是否活跃是什么意思呢?
并不是说定时器一被创建就开始定时,与硬件定时器一样需要一个启动的命令(当前任务通过消息队列发送给守护任务),则定时器才会被挂入定时器链表,则处于活跃态,假设一个定时器是单次定时,在一次定时之后,则定时器会被移出定时器链表,所以判断一个定时器是否在活跃态,就是判断定时器是否在定时器链表中(如果在说明该定时器在参与定时)。
2.定时器的创建
定时器的创建就非常简单,分配个内存,初始化一下,完了,以后只讲动态创建。
xTimerCreate:函数原型:
函数参数(基本是任务书结构体里面的成员):
1.pcTimerName:定时器名字
2.xTimerPeriodInTicks:定时周期
3.uxAutoReload:
pdTRUE:周期模式
pdFALSE:单次模式
4.pvTimerID:定时器ID,用于多个定时器共用同一个回调函数,判断那个定时器超时。
pxCallbackFunction:定时器回调函数。
函数返回值:
创建成功返回定时器句柄,即定时器结构体的地址。
定时器的创建没什么好讲的,主要是为定时器结构体分配内存,最后如果是第一次创建定时器需要初始化两条定时器链表(一条正常,一条超时),并创建一个消息队列,这些是软件定时器运行起来的基本配置,当然还有一个最重要的-守护任务。
前面一套流程走来,我们并没有看到守护任务的痕迹,其实早在启动调度器函数中,如果你将宏configUSE_TIMERS配置为1则默认使用定时器,则就会创建一个定时器的守护任务(任务创建跟我们之前讲的一样,没啥区别)。
2.软件定时器的运行机制
软件定时器到底是怎样运行的嘞,前面所讲的定时器运行所需的基础:两条定时器链表,一个消息队列、一个守护任务,他们之前是如何配合让多个定时器实现周期或单次定时的呢?
选择就让我们将整个定时器的运行机制完整的过一遍,后面讲解源代码的时候,自己代入理解起来更快。
定时器是基于systick中断,假设中断一次的时间为1ms,也就是一个tick。
简单来说:
假设有一个定时器它的周期为20个tick,我让它从当前时间开始定时,此时该定时器就会挂入定时器链表中,怎么挂入呢?,挂入链表中的定时器是以定时器超时时间升序来排序的,假设当前时间为100,此时将该定时器挂入链表中,超时时间是当前时间+周期=100+20=120,什么意思呢?,守护任务的作用就是,判断定时器是否超时,也就是定时器的超时120与当前系统的运行的时间进行对比,如果当前时间到了120(>=120),则守护任务会去调用该定时器的回调函数执行,这样就完成了一次定时。
1.如果该定时器为周期定时器,则重复上面动作,又以当前时间(120)+周期(20)=140,也就是说当系统时间到了140的时候,守护任务又再一次去调用定时器的回调函数,周而复始。
2.如果该定时器为单次定时器,则完成一次定时后,直接将定时器移出定时器链表,而标记定时器为不活跃态。
所以消息队列在里面起什么作用呢?
既然定时器的运行是靠守护任务,所以当我们需要去启动一个定时器的时候(肯定是在某个任务或者中断中去调用启动函数),而这个启动函数就是发送消息(命令)给消息队列,而守护任务会时刻去读队列,读取到启动定时器的命令之后,守护任务会立马将该定时器挂入定时器链表(定时器处于活跃态),当然除了有启动定时器的命令,还有停止定时、更改周期、删除定时器等命令,为了区分这些命令,每个命令都有一个命令ID,当守护任务读取队列消息时,会判断消息中的命令ID以区别执行不同的命令。也是为什么启动一个定时器函数需要一个阻塞时间的参数,本质就是往队列发送消息(当命令队列满,那肯定需要等待)。
为了保证定时的精度,守护任务的优先级一般为最高(当定时超时的时候能尽快去调用定时器的回调函数),但是也为了守护任务不占用太多CPU资源,当超时时间最小的定时器未到超时时间,且消息队列中也没有消息,则守护任务会进入阻塞状态直到超时时间最小的定时器超时,守护任务会被唤醒去调用该定时器的回调函数。
关于定时器的运行机制,就算我们讲出花来,也不能理解非常透彻,最好的方法还是直接分析定时器的源码,个人认为定时器的源码实现非常精彩!!!
阅读源码时重点关注:
1.定时器的守护任务有何作用?
2.如何通过消息队列让定时器执行相应的命令(启动、停止、更改周期、删除等操作)。
3.如何判断定时器是否到期,到期之后如何处理,以及定时器列表的切换。
声明关于定时器为何有两条定时器列表,以及什么时候进行列表切换?
定时器的两条列表,与任务的延时链表原理是如出一辙
看完延时的两条链表就知道了定时的两条链表的作用。
FreeRTOS-时间片与任务阻塞的实现
以下涉及到列表与链表是一个概念
三、定时器源码分析
1.通用发送命令函数xTimerGenericCommand()
前面讲过假设当我们要在某个任务或任务中去启动一个定时器,我们需要往定时器消息队列发送一个命令(即写一个消息到队列中),而守护任务则时刻读取队列消息,然后执行这些命令,因为定时器的命令不止一个,则每个消息都有一个命令ID,以便守护任务读取到消息之后判断需要执行什么命令。
下图则是定时器的启动、复位、停止、更改周期、删除的宏函数,它们最终都是调用了xTimerGenericCommand()函数。当然这是任务中调用的,还有中断中调用的这些函数(FromISR后缀),我就不提了。
- 1.xTimerGenericCommand()函数原型:
函数参数:
1.xTimer:定时器句柄
2. xCommandID:定时器命令ID
ID号-2~-1:直接调用用户指定的函数在守护任务执行前
ID号0~5:定时器的启动、复位、停止、更改周期、删除命令
ID号6~9:中断版的定时器的启动、复位、停止、更改周期命令
3.xOptionalValue:消息的value,新的的周期或者当前计数器的值(当前时间)
4.pxHigherPriorityTaskWoken:用于中断函数中,是否切换任务
5.xTicksToWait:阻塞时间,毕竟是写队列,队列满了不就写不进去,看你愿不愿意等。
- 2.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 ){/* 初始化消息结构体 *//* xCommandID:命令ID(开始、停止、删除定时器、更改周期) */xMessage.xMessageID = xCommandID;/* xOptionalValue:新的的周期或者当前计数器的值(当前时间) */xMessage.u.xTimerParameters.xMessageValue = xOptionalValue;/* 操作哪个定时器 */xMessage.u.xTimerParameters.pxTimer = xTimer;/* 如果xCommandID<6,说明在任务中发送命令 */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;}
/*-----------------------------------------------------------*/
进去函数就是创建一个定时器消息(结构体),并初始化。
如下图所示定时器消息结构体成员包含:1.命令ID,2.定时器参数结构体(两个员:xMessageValue,pxTimer)3.回调函数参数结构体(回调函数指针,两个函数参数)
回调函数参数结构体(回调函数指针,两个函数参数)
我们只要发送下图的命令,就可以让守护任务直接去调用该回调函数(这不是定时器的回调函数)
我们只需使用下图这个函数向队列发送命令即可
初始化定时器消息结构体后,将这个消息发送给队列,当然根据命令的大小,ID大于6的则是在中断中调用的函数,则需要使用中断版的发送消息函数。
总结:xTimerGenericCommand函数作用就是,将定时器消息打包发送给定时器消息队列等待守护任务读取消息并处理。
2.守护任务的作用portTASK_FUNCTION()
守护任务portTASK_FUNCTION函数的源码:
守护任务函数其实就是一个死循环,一直重复做三件事情:
1.获取当前定时器列表中最早超时的定时器的过期时间。
2.判断定时器是否超时,超时则处理(调用定时器的回调函数执行),否则阻塞任务知道队列接收到命令或者定时器超时(哪个先来就任务就先被唤醒)。
3.处理接收到的队列消息(命令),去读队列。
当然主要是做上面这些事,这些三个函数里面做一些其他的事情,让我们一个一个来分析。
- 1.获取当前定时器列表中最早超时的定时器的过期时间
1.prvGetNextExpireTimeh函数源码:
前提:定时器是按到期时间从小到大顺序排列在定时器列表中
所以定时器列表中第一个定时器是最早到期的。
到期时间(超时时间的含义):假设一个定时器的到期时间为100,系统时间从0开始,等系统时间tickcount运行到100后则称定时器超时了,需要调用一次回调函数。
获取定时器到期时间分两种情况:
1.当前定时器列表中不为空,那皆大欢喜,直接获取定时器到期时间(超时时间)即可。
2.当前定时器列表中为空,当前超时列表不一定为空,则设置到期时间为0,目的就是为了系统时间溢出时,任务将被唤醒->切换定时器列表(当前列表与超时列表互换在prvProcessTimerOrBlockTask()函数中执行)->又获取定时器的到期时间。
- 2.判断定时器是否超时,超时则处理(调用定时器的回调函数执行),否则阻塞任务知道队列接收到命令或者定时器超时(哪个先来就任务就先被唤醒)。
2.prvProcessTimerOrBlockTask函数源码:
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime,BaseType_t xListWasEmpty ){TickType_t xTimeNow;BaseType_t xTimerListsWereSwitched;vTaskSuspendAll();{/* 获得当前系统时间去比较是否有定时超时,如果在获取当前系统时间的时候发现,当前时间溢出,则当前定时器列表中的定时器都已超时直接处理,切换定时器列表是在prvSampleTimeNow()函数中进行的。 */xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );/* 如果当前时间(计数值)未溢出 */if( xTimerListsWereSwitched == pdFALSE ){/* xListWasEmpty == pdFALSE代表当前定时器列表不为空xNextExpireTime <= xTimeNow 说明列表中第一次定时器已超时。 */if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) ){( void ) xTaskResumeAll();/* 处理超时的定时器 */prvProcessExpiredTimer( xNextExpireTime, xTimeNow );}else{/* 前提条件是当前时间未溢出 *//* 代码执行到这里有两种情况:1.当前定时器列表为空(xListWasEmpty!=pdFALSE),xNextExpireTime==02.当前定时器列表不为空(xListWasEmpty == pdFALSE),但是xNextExpireTime>xTimeNow如果是第二种情况:说明定时器未超时,如果队列中无消息任务被阻塞(任务下一次被唤醒的时间就是第一次定时器的超时时间),当然定时器超时与队列接收到消息这两个事件,哪个先来则任务被唤醒。*/if( xListWasEmpty != pdFALSE ){/* 上面的第一种情况:当前定时器列表已为空,再判断一下超时定时器列表是否为空如果为空,且队列中无消息,则守护任务之间被挂起如果不为空,且队列中无消息,则守护任务被阻塞,唤醒时间为0(即xNextExpireTime==0)即在系统时间溢出时守护任务被唤醒。*/xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );}/* 根据上面两种情况让守护任务进入阻塞态 */vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );if( xTaskResumeAll() == pdFALSE ){/* Yield to wait for either a command to arrive, or the* block time to expire. If a command arrived between the* critical section being exited and this yield then the yield* will not cause the task to block. */portYIELD_WITHIN_API();}else{mtCOVERAGE_TEST_MARKER();}}}else{( void ) xTaskResumeAll();}}}
/*-----------------------------------------------------------*/
其实我在注释中讲解的比较清楚了
接下来还是来分析一下这个源码:
1.第一件事就是去获取当前的系统时间, 后面会与定时器的过期时间进行比较判断定时器是否超时,在该函数中还会判断当前系统时间是否超时,超时则需要切换列表
如果当前时间溢出,则切换列表 prvSwitchTimerLists
如果系统时间已经溢出,如果当前定时器列表不为空,则列表中的定时器全部过期了,需要进行处理,至于为啥不用我多讲了,其实超时列表中的定时器也可能过期了,但是不做处理而是等列表切换之后,超时列表变成了当前列表后面正常进行处理。
处理过期定时器prvProcessExpiredTimer函数:
处理过期定时器也分两种情况:
1.定时器为单次定时器:
则处理过程
(1).将定时器移出当前定时器列表,将定时器标记为无活跃态(相当于暂停了该定时器)。
(2).调用定时器的回调函数执行。
2.定时器为周期定时器:
处理过程:
(1).如果定时器的过期时间远小于当前时间,即便重新计算过期时间(新的过期时间=旧的过期时间+定时器周期)结果还小于当前时间,所以要搞个循环处理周期定时器(我这里说的小于不是说数值上(当前时间可能会溢出),我的意思是当前系统时间超过了定时器的过期时间很多(可能溢出))。
为什么会出现上面这种情况?
原因是守护任务是一个任务,可能会被更高优先级的任务或者中断打断,也就是说从发出命令去启动定时器到守护任务去处理这个命令这个时间了定时器可能早已经超时了
处理周期定时器超时:
最关键的就是prvInsertTimerInActiveList()函数
函数参数:
pxTimer:定时器句柄
xNextExpiryTime:下一次过期时间
xTimeNow:系统当前时间
xCommandTime:上一次过期时间
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 ){/* 从定时器的start/reset(开始或重置)命令发出到命令被守护任务处理这段时间内定时器的到期时间已过则需要处理超时的定时器 */if( ( ( TickType_t ) ( xTimeNow - xCommandTime ) ) >= pxTimer->xTimerPeriodInTicks ) /*lint !e961 MISRA exception as the casts are only redundant for some ports. */{/* 从发出命令到执行命令之间的时间处理实际超过定时器周期。*/xProcessTimerNow = pdTRUE;}else{vListInsert( pxOverflowTimerList, &( pxTimer->xTimerListItem ) );}}else{if( ( xTimeNow < xCommandTime ) && ( xNextExpiryTime >= xCommandTime ) ){/* 自命令发出以来,tick计数(系统时间)已经溢出但是定时器的过期时间还没有溢出,那么定时器一定已经过去了有效期及应立即处理。 */xProcessTimerNow = pdTRUE;}else{vListInsert( pxCurrentTimerList, &( pxTimer->xTimerListItem ) );}}return xProcessTimerNow;}
/*-----------------------------------------------------------*/
看我们传入了那些参数:
xNextExpiryTime:就等于xExpiredTime + pxTimer->xTimerPeriodInTicks(定时器周期)。
接下来在分析prvInsertTimerInActiveList()函数源码:
- 先看第一个分支xNextExpiryTime <= xTimeNow:
这里会有两种情况出现:
大前提:xCommandTime一定是小于xTimeNow
1.xNextExpiryTime 没有溢出,而xNextExpiryTime <= xTimeNow说明,当前系统时间大于定时器下一次过期时间,则定时器早已过期需要处理。
2.xNextExpiryTime 溢出,而xNextExpiryTime <= xTimeNow,但是并不满足( xTimeNow - xCommandTime ) ) >= pxTimer->xTimerPeriodInTicks这个条件,则它会走下面那个分支,即定时器下次过期时间还为到,但是下次过期时间已经溢出则需要插入超时定时器列表。
- 再看第二个分支xNextExpiryTime >xTimeNow:
大前提:xCommandTime一定是小于xTimeNow
同样有两种情况出现:
1.xTimeNow(系统时间)已经溢出,但是定时器的过期时间还没有溢出,而xNextExpiryTime >xTimeNow,且满足xTimeNow < xCommandTime ) && ( xNextExpiryTime >= xCommandTime )条件则,则定时器早已过期需要处理。
2.xTimeNow没有溢出,定时器的过期时间xNextExpiryTime 还没有溢出,这个更好理解大家都没有溢出,且xNextExpiryTime >xTimeNow,下次过期时间大于当前系统时间,则定时器还未过期则将它插入当前定时器列表。
到此最难理解的已经搞定了,后面涉及到处理定时器过期的函数prvProcessExpiredTimer()直接套用上面的概念。
回到之前的关于定时器列表切换前将当前列表中的定时器全部过期处理:
它传入了当前时间是最大值:
为啥说它是最大值呢?
因为当前时间变量是一个无符号数,你把-1存进去,存进去的是-1的补码,全是1。不理解请看:C语言深度解剖之数据到底在内存中如何存储
然后自己传入分析一下
战线拉的太远了,我们在回过来看prvProcessTimerOrBlockTask()函数的分析:
获取完当前系统时间,第二步就要去判断有定时器已经过期,前提是xListWasEmpty == pdFALSE代表当前定时器列表不为空, xNextExpireTime <= xTimeNow 说明列表中第一个定时器已过期,然后调用prvProcessExpiredTimer()函数处理过期的定时器,前面已经讲了prvProcessExpiredTimer()函数。
代码执行到这里有两种情况:
1.当前定时器列表为空(xListWasEmpty!=pdFALSE),xNextExpireTime==0
2.当前定时器列表不为空(xListWasEmpty == pdFALSE),但是xNextExpireTime>xTimeNow
如果是第二种情况:说明定时器未超时,如果队列中无消息任务被阻塞(任务下一次被唤醒的时间就是第一次定时器的超时时间),当然定时器超时与队列接收到消息这两个事件,哪个先来则任务被唤醒。
直接看注释吧,注释已经非常清晰了
接下来看看vQueueWaitForMessageRestricted()函数如何让任务进入阻塞态:
- 3.处理接收到的队列消息(命令),去读队列。**
3.处理命令函数prvProcessReceivedCommands()
prvProcessReceivedCommands()函数源码:
static void prvProcessReceivedCommands( void ){DaemonTaskMessage_t xMessage;Timer_t * pxTimer;BaseType_t xTimerListsWereSwitched;TickType_t xTimeNow;/* 读队列,判断是否消息 */while( xQueueReceive( xTimerQueue, &xMessage, tmrNO_DELAY ) != pdFAIL ) /*lint !e603 xMessage does not have to be initialised as it is passed out, not in, and it is not used unless xQueueReceive() returns pdTRUE. */{#if ( INCLUDE_xTimerPendFunctionCall == 1 ){/* 负命令是执行一次回调函数,而不是计时器命令。*/if( xMessage.xMessageID < ( BaseType_t ) 0 ){const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );/* 定时器使用 xCallbackParameters 成员来请求执行回调。 检查回调是否为空 */configASSERT( pxCallback );/* 调用函数 */pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );}else{mtCOVERAGE_TEST_MARKER();}}#endif /* INCLUDE_xTimerPendFunctionCall *//* xMessageID>0,为定时器命令 */if( xMessage.xMessageID >= ( BaseType_t ) 0 ){/* 这些消息使用 xTimerParameters 成员来处理软件定时器。*/pxTimer = xMessage.u.xTimerParameters.pxTimer;/* 判断定时器是否挂载在定时器链表中(正常链表或者超时链表) */if( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) == pdFALSE ) /*lint !e961. The cast is only redundant when NULL is passed into the macro. */{/* 如果定时器在定时器链表中,则将它从链表中移除 */( void ) uxListRemove( &( pxTimer->xTimerListItem ) );}else{mtCOVERAGE_TEST_MARKER();}traceTIMER_COMMAND_RECEIVED( pxTimer, xMessage.xMessageID, xMessage.u.xTimerParameters.xMessageValue );/* 在本例中,不使用 xTimerListsWereSwitch 参数,但它必须存在于函数调用中,获取当前时间,如果当前时间溢出则切换定时器链表 */xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );switch( xMessage.xMessageID ){/* 启动/复位定时器 */case tmrCOMMAND_START:case tmrCOMMAND_START_FROM_ISR:case tmrCOMMAND_RESET:case tmrCOMMAND_RESET_FROM_ISR:/* Start or restart a timer. */pxTimer->ucStatus |= tmrSTATUS_IS_ACTIVE;if( prvInsertTimerInActiveList( pxTimer, xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, xTimeNow, xMessage.u.xTimerParameters.xMessageValue ) != pdFALSE ){/* 定时器在添加至定时器链表之前就已经超时了,应立即处理(调用回调函数)。*/if( ( pxTimer->ucStatus & tmrSTATUS_IS_AUTORELOAD ) != 0 ){/* 如果是周期定时器 */prvReloadTimer( pxTimer, xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, xTimeNow );}else{/* 如果是单次定时器,则将定时器的状态改为不活跃态 */pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE;}traceTIMER_EXPIRED( pxTimer );/* 调用定时的回调函数 */pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );}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;/* 此时xMessageValue变量的值就代表定时器新的周期 */pxTimer->xTimerPeriodInTicks = xMessage.u.xTimerParameters.xMessageValue;configASSERT( ( pxTimer->xTimerPeriodInTicks > 0 ) );/* 新的周期可以比旧的周期长也可以比旧的周期短,有一点特别注意,该定时器的起始时间就是系统运行时间,所以该定时器必定是在将来超时,不会像上面tmrCOMMAND_START的情况一样定时器在添加至定时器链表之前就已经超时了*/( 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 /* if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) */{/* 定时器是静态创建的,内存无法释放的 只需改变定时器的状态即可 */pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE;}#endif /* configSUPPORT_DYNAMIC_ALLOCATION */break;default:/* Don't expect to get here. */break;}}}}
/*-----------------------------------------------------------*/
prvProcessReceivedCommands()函数非常简单,看注释吧,唯一注意的是
当我们去调用xTimerReset(),xTimerStart(),它会自动获取当前时间,以当前调用函数的时间作为基准,开始定时。
四.总结
一句话基础不牢地动山摇!!!!,有哪里不理解的在评论区问我,一起讨论讨论。