分析FreeRTOS内核对象API的源码实现(六)

软件定时器:

1.软件定时器的基本概念

定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器的周期与频率。类似生活中的闹钟,我们可以设置闹钟每天什么时候响,还能设置响的次数,是响一次还是每天都响。

定时器有硬件定时器和软件定时器之分:

硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断(Systick中断)。硬件定时器的精度一般很高,可以达到纳秒级别,并且是中断触发方式。 软件定时器,软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,它实现的功能与硬件定时器也是类似的。

使用硬件定时器时,每次在定时时间到达之后就会自动触发一个中断,用户在中断中处理信息;而使用软件定时器时,需要我们在创建软件定时器时指定时间到达后要调用的函数(也称超时函数/回调函数,为了统一,下文均用回调函数描述),在回调函数中处理信息。

注意:软件定时器回调函数的上下文是任务,下文所说的定时器均为软件定时器。 软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。定时精度与系统时钟的周期有关。一般系统利用 SysTick 作为软件定时器的基础时钟,软件定时器的回调函数类似硬件的中断服务函数,所以,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况(软件定时器回调函数的上下文环境是任务),比如 vTaskDelay()以及 其它能阻 塞任务运 行的函数 ,两次触发 回调函数 的时间间 隔xTimerPeriodInTicks 叫定时器的定时周期。

FreeRTOS 操作系统提供软件定时器功能,软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务。FreeRTOS 软件定时器功能上支持:

 裁剪:能通过宏关闭软件定时器功能。

 软件定时器创建。

 软件定时器启动。

 软件定时器停止。

 软件定时器复位。

 软件定时器删除。

FreeRTOS 提供的软件定时器支持单次模式和周期模式,单次模式和周期模式的定时时间到之后都会调用软件定时器的回调函数,用户可以在回调函数中加入要执行的工程代码。

单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器进入休眠状态,不再重新执行。

周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除

如下图:

FreeRTOS 通过一个 prvTimerTask 任务(也叫守护任务 Daemon)管理软定时器,它是在启动调度器时自动创建的,为了满足用户定时需求。prvTimerTask 任务会在其执行期间检查用户启动的时间周期溢出的定时器,并调用其回调函数。只有设置 FreeRTOSConfig.h 中的宏定义 configUSE_TIMERS 设置为 1 ,将相关代码编译进来,才能正常使用软件定时器相关功能。

2.软件定时器应用场景

在很多应用中,我们需要一些定时器任务,硬件定时器受硬件的限制,数量上不足以满足用户的实际需求,无法提供更多的定时器,那么可以采用软件定时器来完成,由软件定时器代替硬件定时器任务。但需要注意的是软件定时器的精度是无法和硬件定时器相比的,而且在软件定时器的定时过程中是极有可能被其它中断所打断,因为软件定时器的执行上下文环境是任务。所以,软件定时器更适用于对时间精度要求不高的任务,一些辅助型的任务。

3.软件定时器的精度

在操作系统中,通常软件定时器以系统节拍周期为计时单位。系统节拍是系统的心跳节拍,表示系统时钟的频率,就类似人的心跳,1s 能跳动多少下,系统节拍配置为configTICK_RATE_HZ,该宏在 FreeRTOSConfig.h 中有定义,默认是 1000。那么系统的时钟节拍周期就为 1ms(1s跳动 1000 下,每一下就为 1ms)。软件定时器的所定时数值必须是这个节拍周期的整数倍,例如节拍周期是 10ms,那么上层软件定时器定时数值只能是10ms,20ms,100ms 等,而不能取值为 15ms。由于节拍定义了系统中定时器能够分辨的精确度,系统可以根据实际系统 CPU 的处理能力和实时性需求设置合适的数值,系统节拍周期的值越小,精度越高,但是系统开销也将越大,因为这代表在 1 秒中系统进入时钟中断的次数也就越多。

4.软件定时器的运作机制

软件定时器是可选的系统资源,在创建定时器的时候会分配一块内存空间。当用户创建并启动一个软件定时器时, FreeRTOS 会根据当前系统时间及用户设置的定时确定该定时器唤醒时间,并将该定时器控制块挂入软件定时器列表,FreeRTOS 中采用两个定时器列表维护软件定时器,pxCurrentTimerList 与 pxOverflowTimerList 是列表指针,在初始化的时候分别指向 xActiveTimerList1 与 xActiveTimerList2

软件定时器用到的链表:

c 复制代码
PRIVILEGED_DATA static List_t xActiveTimerList1; 
PRIVILEGED_DATA static List_t xActiveTimerList2; 
PRIVILEGED_DATA static List_t *pxCurrentTimerList; 
PRIVILEGED_DATA static List_t *pxOverflowTimerList;

pxCurrentTimerList:系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList 列表中。系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。否则将定时器任务挂起,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。 pxOverflowTimerList 列表是在软件定时器溢出的时候使用,作用与 pxCurrentTimerList一致。

同时,FreeRTOS 的软件定时器还有采用消息队列进行通信,利用"定时器命令队列"向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器等。假如定时器任务处于阻塞状态,我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中,所以,在定时器启动函数中,FreeRTOS 是采用队列的方式发送一个消息给软件定时器任务,任务被唤醒从而执行接收到的命令。

例如:系统当前时间 xTimeNow 值为 0,注意:xTimeNow 其实是一个局部变量,是根据 xTaskGetTickCount()函数获取的,实际它的值就是全局变量 xTickCount 的值,下文都采用它表示当前系统时间。在当前系统中已经创建并启动了 1 个定时器 Timer1;系统继续运行,当系统的时间 xTimeNow 为 20 的时候,用户创建并且启动一个定时时间为 100 的定时器 Timer2,此 时 Timer2 的 溢出 时间 xTicksToWait 就 为定 时时间 +系统当前时间(100+20=120),然后将 Timer2 按 xTicksToWait 升序插入软件定时器列表中;假设当前系统时间 xTimeNow 为 40 的时候,用户创建并且启动了一个定时时间为 50 的定时器Timer3,那么此时Timer3的溢出时间xTicksToWait 就 为 40+50=90,同样安装xTicksToWait 的数值升序插入软件定时器列表中

在定时器链表中插入过程如下图:

这里的底层逻辑其实跟我们将阻塞任务插入到阻塞链表是一样的

那么系统如何处理软件定时器列表?系统在不断运行,而 xTimeNow(xTickCount)随着 SysTick 的触发一直在增长(每一次硬件定时器中断来临,xTimeNow 变量会加 1),在软件定时器任务运行的时候会获取下一个要唤醒的定时器,比较当前系统时间xTimeNow 是否大于或等于下一个定时器唤醒时间 xTicksToWait,若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。

那么系统如何处理软件定时器列表?系统在不断运行,而 xTimeNow(xTickCount)随着 SysTick 的触发一直在增长(每一次硬件定时器中断来临,xTimeNow 变量会加 1),在软件定时器任务运行的时候会获取下一个要唤醒的定时器,比较当前系统时间xTimeNow 是否大于或等于下一个定时器唤醒时间 xTicksToWait,若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。上面图二为例,讲解软件定时器调用回调函数的过程:

在创建定 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 升序重新添加到软件定时器列表中。

使用软件定时器时候要注意以下几点:

 软件定时器的回调函数中应快进快出,绝对不允许使用任何可能引软件定时器起任务挂起或者阻塞的 API 接口,在回调函数中也绝对不允许出现死循环。

 软件定时器使用了系统的一个队列和一个任务资源,软件定时器任务的优先级默认为 configTIMER_TASK_PRIORITY,为了更好响应,该优先级应设置为所有任务中最高的优先级。

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

 定时器任务的堆栈大小默认为 configTIMER_TASK_STACK_DEPTH 个字节。

5.软件定时器控制块

软件定时器虽然不属于内核资源,但是也是 FreeRTOS 核心组成部分,是一个可以裁剪的功能模块,同样在系统中由一个控制块管理其相关信息,软件定时器的控制块中包含没 用 过 创 建 的 软 件定时器基本信息,在使用定时器前我们需要通过xTimerCreate()/xTimerCreateStatic()函数创建一个软件定时器,在函数中,FreeRTOS 将向系统管理的内存申请一块软件定时器控制块大小的内存用于保存定时器的信息,下面来看看软件定时器控制块的成员变量

c 复制代码
/* The definition of the timers themselves. */
typedef struct tmrTimerControl
{
	const char				*pcTimerName;		/*<< Text name.  This is not used by the kernel, it is included simply to make debugging easier. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
	ListItem_t				xTimerListItem;		/*<< Standard linked list item as used by all kernel features for event management. */
	TickType_t				xTimerPeriodInTicks;/*<< How quickly and often the timer expires. */
	UBaseType_t				uxAutoReload;		/*<< Set to pdTRUE if the timer should be automatically restarted once expired.  Set to pdFALSE if the timer is, in effect, a one-shot timer. */
	void 					*pvTimerID;			/*<< An ID to identify the timer.  This allows the timer to be identified when the same callback is used for multiple timers. */
	TimerCallbackFunction_t	pxCallbackFunction;	/*<< The function that will be called when the timer expires. */
	#if( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t			uxTimerNumber;		/*<< An ID assigned by trace tools such as FreeRTOS+Trace */
	#endif

	#if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
		uint8_t 			ucStaticallyAllocated; /*<< Set to pdTRUE if the timer was created statically so no attempt is made to free the memory again if the timer is later deleted. */
	#endif
} xTIMER;

/* The old xTIMER name is maintained above then typedefed to the new Timer_t
name below to enable the use of older kernel aware debuggers. */
typedef xTIMER Timer_t;

(1)软件定时器名字,这个名字一般用于调试的,RTOS 使用定时器是通过其句柄,并不是使用其名字。

(2)软件定时器节点,用于插入定时器链表

(3)软件定时器的周期,单位为系统节拍周期 (即 tick) ,pdMS_TO_TICKS()可以把时间单位从 ms转换为系统节拍周期。

(4)软件定时器是否自动重置,如果该值为 pdFalse,那么创建的软件定时器工作模式是单次模式,否则为周期模式。

(5)软件定时器 ID,该变量为指针类型。该 ID 典型的用法是当一个回调函数分配给一个或者多个软件定时器时,在回调函数里面根据 ID 号来处理不同的软件定时器(可以理解为指令,不同的ID对应不同的指令,回调函数会根据这些指令执行不同的代码逻辑)。

(6)软件定时器的回调函数,当定时时间到达的时候就会调用这个函数。

(7)标记定时器使用的内存,删除时判断是否需要释放内存。

6.软件定时器函数接口讲解

软件定时器的功能是在定时器任务(也叫定时器守护任务,Daemon)中实现的。软件定时器的很多 API 函数并不是直接操作内核链表,而是通过一个名为「定时器命令队列」的队列,把命令消息发送给定时器守护任务。该队列由 RTOS 内核提供,应用程序不能直接访问,队列长度由宏 configTIMER_QUEUE_LENGTH 定义。

可以借用 SysTick 中断的模型来理解这种「命令 + 队列」的设计:

  • SysTick 中断到来后,由 xPortSysTickHandler() 中断服务函数统一处理,它的核心是调用 xTaskIncrementTick() 来更新 tick 时基、唤醒阻塞任务、申请任务切换。也就是说,中断入口是固定的,处理逻辑也比较单一。
  • 软件定时器守护任务可以类比成 xTaskIncrementTick(),是软件定时器(类比SysTick 硬件计数器归零 )相关的统一处理入口。但守护任务要处理的操作有很多种:启动、停止、复位、删除、周期重载等,逻辑各不相同,而且主要是在软件定时器中是不能调用阻塞该任务的API函数,但是守护任务可以
  • 因此,软件定时器 API 不能像 SysTick 那样「固定入口直接调用固定函数」,而是把不同操作封装成不同的命令 ID,通过定时器命令队列发给守护任务。守护任务收到消息后,再根据命令 ID 分派到对应的处理程序。

一句话总结:SysTick 是「固定入口 + 固定逻辑直接调用」,软件定时器守护任务是「固定入口 + 命令队列 + 按 ID 分派」。

下面就讲解一些常用的软件定时器函数接口:

(1)软件定时器创建函数 xTimerCreate()

软件定时器与 FreeRTOS 内核其他资源一样,需要创建才允许使用的,FreeRTOS 为我们提供了两种创建方式,一种是动态创建软件定时器 xTimerCreate(),另一种是静态创建方式 xTimerCreateStatic(),因为创建过程基本差不多,所以在这里我们只讲解动态创建方式。

xTimerCreate()用于创建一个软件定时器,并返回一个句柄。要想使用该函数函数必须在 头 文 件 FreeRTOSConfig.h 中 把 宏 configUSE_TIMERS 和configSUPPORT_DYNAMIC_ALLOCATION 均 定 义 为 1(configSUPPORT_DYNAMIC_ALLOCATION 在 FreeRTOS.h 中默认定义为 1),并且需要把 FreeRTOS/source/times.c 这个 C 文件添加到工程中。 每一个软件定时器只需要 很少的 RAM 空间来保存其的状态。如果使用函 数xTimeCreate()来创建一个软件定时器,那么需要的 RAM 是动态分配的。如果使用函数xTimeCreateStatic()来创建一个事件组,那么需要的 RAM 是静态分配的 软件定时器在创建成功后是处于休眠状态的,可以使用 xTimerStart()、xTimerReset()、

xTimerStartFromISR() 、 xTimerResetFromISR() 、 xTimerChangePeriod() 和xTimerChangePeriodFromISR()这些函数将其状态转换为活跃态。

xTimerCreate()源码如下:

c 复制代码
#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 ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
	{
	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 )
			{
				/* Timers can be created statically or dynamically, so note this
				timer was created dynamically in case the timer is later
				deleted. */
				pxNewTimer->ucStaticallyAllocated = pdFALSE;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */
		}

		return pxNewTimer;
	}

#endif /* configSUPPORT_STATIC_ALLOCATION */

参数说明:

1.软件定时器名字,文本形式,纯粹是为了调试,FreeRTOS 使用定时器是通过其句柄,而不是使用其名字

2.软件定时器的周期,单位为系统节拍周期(即 tick)。使用pdMS_TO_TICKS()可以把时间单位从 ms 转换为系统节拍周期。如果软件定时器的周期为100个 tick,那么只需要简单的设置 xTimerPeriod 的值为 100 即可。如果软件定时器的周期为 500ms,那么 xTimerPeriod 应设置为 pdMS_TO_TICKS(500)。宏 pdMS_TO_TICKS()只有当 configTICK_RATE_HZ 配置成小于或者等于 1000HZ 时才可以使用。

3.如果 uxAutoReload 设置为 pdTRUE,那么软件定时器的工作模式就 是 周 期模 式 ,一 直 会以 用 户指 定 的 xTimerPeriod 周 期 去 执行 回 调函 数 。如 果uxAutoReload 设置为 pdFALSE,那么软件定时器就在用户指定的 xTimerPeriod 周期下运行一次后就进入休眠态。

4.软件定时器 ID,该变量为指针类型。该 ID 典型的用法是当一个回调函数分配给一个或者多个软件定时器时,在回调函数里面根据 ID 号来处理不同的软件定时器。

5.软件定时器的回调函数,当定时时间到达的时候就会调用这个函数,该函数需要用户自己实现。

这里的代码逻辑与我们创建任务的代码逻辑非常相似。

6.定义一个软件定时器的控制块

7.给这个定时器控制块分配内存(因为软件定时器只有控制块,而我们任务除了控制块还有栈空间要分配内存)

8.将分配好的定时器控制块和要设置该控制块的参数传入prvInitialiseNewTimer()函数

prvInitialiseNewTimer()函数源码如下:

c 复制代码
static void prvInitialiseNewTimer(	const char * const pcTimerName,
									const TickType_t xTimerPeriodInTicks,
									const UBaseType_t uxAutoReload,
									void * const pvTimerID,
									TimerCallbackFunction_t pxCallbackFunction,
									Timer_t *pxNewTimer ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
{
	/* 0 is not a valid value for xTimerPeriodInTicks. */
	configASSERT( ( xTimerPeriodInTicks > 0 ) );

	if( pxNewTimer != NULL )
	{
		/* Ensure the infrastructure used by the timer service task has been
		created/initialised. */
		prvCheckForValidListAndQueue();

		/* Initialise the timer structure members using the function
		parameters. */
		pxNewTimer->pcTimerName = pcTimerName;
		pxNewTimer->xTimerPeriodInTicks = xTimerPeriodInTicks;
		pxNewTimer->uxAutoReload = uxAutoReload;
		pxNewTimer->pvTimerID = pvTimerID;
		pxNewTimer->pxCallbackFunction = pxCallbackFunction;
		vListInitialiseItem( &( pxNewTimer->xTimerListItem ) );
		traceTIMER_CREATE( pxNewTimer );
	}
}

(1)调用prvCheckForValidListAndQueue()函数,源码如下:

c 复制代码
static void prvCheckForValidListAndQueue( void )
{
	/* Check that the list from which active timers are referenced, and the
	queue used to communicate with the timer service, have been
	initialised. */
	taskENTER_CRITICAL();
	{
		if( xTimerQueue == NULL )
		{
			vListInitialise( &xActiveTimerList1 );
			vListInitialise( &xActiveTimerList2 );
			pxCurrentTimerList = &xActiveTimerList1;
			pxOverflowTimerList = &xActiveTimerList2;

			#if( configSUPPORT_STATIC_ALLOCATION == 1 )
			{
				/* The timer queue is allocated statically in case
				configSUPPORT_DYNAMIC_ALLOCATION is 0. */
				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

			#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();
}

(1.1)判断xTimerQueue(定时器队列)是否为空,如果为空说明还没创建定时器队列,走if()语句分支

(1.2)初始化软件定时器链表,这里与初始化延迟函数链表底层代码逻辑一样

(1.3)创建定时器队列,并将句柄传给xTimerQueue,传入xQueueCreate()函数的参数宏定义如下:

c 复制代码
//软件定时器队列长度
#define configTIMER_QUEUE_LENGTH		        10

单个消息空间大小为DaemonTaskMessage_t类型结构体的大小,该结构体类型定义如下:

c 复制代码
typedef struct tmrTimerParameters
{
	TickType_t			xMessageValue;		/*<< An optional value used by a subset of commands, for example, when changing the period of a timer. */
	Timer_t *			pxTimer;			/*<< The timer to which the command will be applied. */
} TimerParameter_t;


typedef struct tmrCallbackParameters
{
	PendedFunction_t	pxCallbackFunction;	/* << The callback function to execute. */
	void *pvParameter1;						/* << The value that will be used as the callback functions first parameter. */
	uint32_t ulParameter2;					/* << The value that will be used as the callback functions second parameter. */
} CallbackParameters_t;

/* The structure that contains the two message types, along with an identifier
that is used to determine which message type is valid. */
typedef struct tmrTimerQueueMessage
{
	BaseType_t			xMessageID;			/*<< The command being sent to the timer service task. */
	union
	{
		TimerParameter_t xTimerParameters;

		/* Don't include xCallbackParameters if it is not going to be used as
		it makes the structure (and therefore the timer queue) larger. */
		#if ( INCLUDE_xTimerPendFunctionCall == 1 )
			CallbackParameters_t xCallbackParameters;
		#endif /* INCLUDE_xTimerPendFunctionCall */
	} u;
} DaemonTaskMessage_t;

(1.4)该函数干三件事:

  1. 遍历全局注册表 xQueueRegistryconfigQUEUE_REGISTRY_SIZE(你的工程里是 10 格);
  2. 找到第一个空槽(pcQueueName == NULL)→ 把"句柄 + 名字指针"存进去 → break;
  3. 登记完调一个 trace 宏(traceQUEUE_REGISTRY_ADD,Tracealyzer 用)。

用途:调试可视化。 Keil 的 RTOS 感知视图、Tracealyzer、pcQueueGetName() 靠这张表把 0x2000xxxx显示成"UART_RX_Q、按键队列"这种人能读的名字。内核调度逻辑从不查这张表------不登记,系统照跑。所以无需在意

所以prvCheckForValidListAndQueue()函数的作用就是初始化定时器两条链表和创建一个定时器队列

(2)初始化定时器控制块的各种参数

(3)初始化该定时器控制块的节点

9.返回设置好的定时器控制块(也就是该定时器的句柄)

(2)软件定时器启动函数

1.xTimerStart()

前面说过,软件定时器在创建完成的时候是处于休眠状态的,需要用 FreeRTOS 的相关函数将软件定时器活动起来,而 xTimerStart()函数就是可以让处于休眠的定时器开始工作。 我们知道,在系统开始运行的时候,系统会帮我们自动创建一个软件定时器任务(prvTimerTask),在这个任务中,如果暂时没有运行中的定时器,任务会进入阻塞态等待命令,而我们的启动函数就是通过"定时器命令队列"向定时器任务发送一个启动命令,定时器任务获得命令就解除阻塞,然后执行启动软件定时器命令。下面来看看 xTimerStart()是怎么让定时器工作的吧

xTimerStart()函数宏定义如下:

c 复制代码
#define xTimerStart( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START, ( xTaskGetTickCount() ), NULL, ( xTicksToWait ) )

xTimerGenericCommand()函数源码如下:

c 复制代码
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 );

	/* Send a message to the timer service task to perform a particular action
	on a particular timer definition. */
	if( xTimerQueue != NULL )
	{
		/* Send a command to the timer service task to start the xTimer timer. */
		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 );
		}

		traceTIMER_COMMAND_SEND( xTimer, xCommandID, xOptionalValue, xReturn );
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	return xReturn;
}

参数说明:

TimerHandle_t xTimer: 要操作的软件定时器句柄

const BaseType_t xCommandID: tmrCOMMAND_START 是软件定时器启动命令,因为现在是要将软件定时器启动,该命令在 timers.h 中有定义。xCommandID 参数可以指定多个命令。

软件定时器操作支持的命令宏定义如下:

c 复制代码
#define tmrCOMMAND_EXECUTE_CALLBACK_FROM_ISR 	( ( BaseType_t ) -2 )
#define tmrCOMMAND_EXECUTE_CALLBACK				( ( BaseType_t ) -1 )
#define tmrCOMMAND_START_DONT_TRACE				( ( BaseType_t ) 0 )
#define tmrCOMMAND_START					    ( ( BaseType_t ) 1 )
#define tmrCOMMAND_RESET						( ( BaseType_t ) 2 )
#define tmrCOMMAND_STOP							( ( BaseType_t ) 3 )
#define tmrCOMMAND_CHANGE_PERIOD				( ( BaseType_t ) 4 )
#define tmrCOMMAND_DELETE						( ( BaseType_t ) 5 )

#define tmrFIRST_FROM_ISR_COMMAND				( ( BaseType_t ) 6 )
#define tmrCOMMAND_START_FROM_ISR				( ( BaseType_t ) 6 )
#define tmrCOMMAND_RESET_FROM_ISR				( ( BaseType_t ) 7 )
#define tmrCOMMAND_STOP_FROM_ISR				( ( BaseType_t ) 8 )
#define tmrCOMMAND_CHANGE_PERIOD_FROM_ISR		( ( BaseType_t ) 9 )

const TickType_t xOptionalValue: 调用xTaskGetTickCount()函数,获取当前系统时间xOptionalValue。

BaseType_t * const pxHigherPriorityTaskWoken: pxHigherPriorityTaskWoken 为 NULL(即"申请任务切换回执清单"),该参数在中断中发送命令才起作用。

const TickType_t xTicksToWait: 用户指定超时阻塞时间,单位为系统节拍周期(即 tick)。调用xTimerStart()的任务将可能会锁定在阻塞态(因为该函数就是通过队列发送指令和消息给守护任务,所以如果队列满了,那该任务就会锁定在阻塞态),在软件定时器把启动的命令成功发送到定时器命令队列之前。如果在 FreeRTOS 调度器开启之前调用 xTimerStart(),形参将不起作用。

1.系统打算通过"定时器命令队列"发送命令给定时器守护任务,需要先判断一下"定时器命令队列"是否存在,只有存在队列才允许发送命令。

2.要发送的命令基本信息,包括命令、命令的数值(比如可以表示当前系统时间、要修改的定时器周期等)以及要处理的软件定时器句柄等。

3.根据用户指定的 xCommandID 参数,判断命令是在哪个上下文环境发出的,如果是在任务中发出的,则执行(4) 、(5) 代码,否则就执行(6)。

c 复制代码
#define tmrFIRST_FROM_ISR_COMMAND				( ( BaseType_t ) 6 )

#define taskSCHEDULER_RUNNING		( ( BaseType_t ) 2 )

4.判断xTaskGetSchedulerState()函数(获取调度器状态函数)的返回值,该函数源码如下:

c 复制代码
#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )

	BaseType_t xTaskGetSchedulerState( void )
	{
	BaseType_t xReturn;

		if( xSchedulerRunning == pdFALSE )
		{
			xReturn = taskSCHEDULER_NOT_STARTED;
		}
		else
		{
			if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
			{
				xReturn = taskSCHEDULER_RUNNING;
			}
			else
			{
				xReturn = taskSCHEDULER_SUSPENDED;
			}
		}

		return xReturn;
	}

#endif /* ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) ) */

该函数判断调度器的状态,如果调度器没开启就返回taskSCHEDULER_NOT_STARTED(1),如果调度器开启了并且能正常运行就返回taskSCHEDULER_RUNNING(2),如果调度器开启了但被挂起了就返回taskSCHEDULER_SUSPENDED(0)

5、6:所以这里就是判断如果调度器开启了并能正常运行就调用xReturn = xQueueSendToBack( xTimerQueue, &xMessage, xTicksToWait )

,如果调度器没开启或者被挂起就调用xReturn = xQueueSendToBack( xTimerQueue, &xMessage, tmrNO_DELAY )也就是指定阻塞时间为0,因为在调度器没开启和被挂起期间,指定的阻塞时间是失效的,任务并不会切换

7.如果在中断上下文执行,就调用xQueueSendToBackFromISR( xTimerQueue, &xMessage, pxHigherPriorityTaskWoken );

那调度器还没开启是什么情况呢?如下:

c 复制代码
int main(void)
{
	 xTimer = xTimerCreate("T1", pdMS_TO_TICKS(1000), pdTRUE, 0, cb);  // ← 队列在这一步已建好
	 xTimerStart(xTimer, 0);          // ← 调度器没跑,但队列入队成功
	 vTaskStartScheduler();           // ← 这时才创建守护任务 "Tmr Svc"
	 /* 守护任务启动后从队列取出 START 命令,定时器才真正开始计时 */
}

这里需要把一个执行顺序捋清楚:

  • vTaskStartScheduler() 启动调度器时,会创建软件定时器守护任务 prvTimerTask(也就是任务名 "Tmr Svc")。
  • 守护任务创建的过程中,会调用 prvCheckForValidListAndQueue(),把软件定时器两条链表和「定时器命令队列」准备好。
  • 而 xTimerCreate() 内部同样会调用 prvCheckForValidListAndQueue()。也就是说,只要我们手动调用一次 xTimerCreate(),定时器命令队列就会被提前创建出来。

因此,如果想在 vTaskStartScheduler() 之前就往定时器命令队列发送消息,就需要先手动调用 xTimerCreate(),让队列先被创建好。

补充:

  • 此时队列虽然已经存在,消息可以正常入队,但守护任务 prvTimerTask 还没有被创建,因为它也要等到 vTaskStartScheduler() 启动调度器时才会被创建。
  • 所以在这段时间里,命令可以成功写入队列,但并不会有守护任务立刻从队列里取出并处理这些命令;这些命令会先暂存在队列里,等调度器启动、守护任务创建并开始运行后,再被取出处理。

小总结:

  • 软件定时器是数据对象,没有 TCB/栈/调度;它只记录"周期、到期时间、回调"。
  • xTimerStart() 把"启动命令"投进定时器命令队列;守护任务取出命令,把定时器按到期时间插入活动链表,倒计时开始(对已在运行的定时器再 Start = 重新计时)。
  • 到时后,守护任务从链表里发现它:周期模式重装下一次到期时间、单次模式摘出链表;然后在守护任务上下文调用回调。
  • 因此:回调里不能阻塞、不能长时间占用(会拖住守护任务,所有定时器一起遭殃);回调也没有"自己的栈"。

2.xTimerStartFromISR()

当 然 除 在任 务启 动 软件 定 时器 之外 , 还有 在 中断 中启 动 软件 定 时器 的函 数xTimerStartFromISR()。xTimerStartFromISR()是函数 xTimerStart()的中断版本,用于启动一个先前由函数 xTimerCreate() / xTimerCreateStatic()创建的软件定时器。

该函数说明如下:

该函数宏定义如下:

c 复制代码
#define xTimerStartFromISR( xTimer, pxHigherPriorityTaskWoken ) xTimerGenericCommand( ( xTimer ), tmrCOMMAND_START_FROM_ISR, ( xTaskGetTickCountFromISR()

可以看到本质调用的还是xTimerGenericCommand()函数,只是传入的参数ID(tmrCOMMAND_START_FROM_ISR)不同,这在上面我们了解过了

(3)软件定时器停止函数

1.xTimerStop()

xTimerStop() 用于停止一个已经启动的软件定时器,该函数的实现也是通过"定时器命令队列"发送一个停止命令给软件定时器任务,从而唤醒软件定时器任务去将定时器停止。要想使函数 xTimerStop()必须在头文件 FreeRTOSConfig.h 中把宏 configUSE_TIMERS定义为 1。

xTimerStop()函数说明如下:

2.xTimerStopFromISR()

xTimerStopFromISR()是函数 xTimerStop()的中断版本,用于停止一个正在运行的软件定时器,让其进入休眠态,实现过程也是通过"定时器命令队列"向软件定时器任务发送停止命令。

xTimerStopFromISR()函数说明如下:

它们本质都是调用xTimerGenericCommand(),然后向守护任务发送不同指令,所以就不分析源码了

(4)软件定时器任务

我们知道,软件定时器回调函数运行的上下文环境是任务,那么软件定时器任务是在干什么的呢?如何创建的呢?下面跟我一步步来分析软件定时器的工作过程。 软件定时器任务是在系统开始调度(vTaskStartScheduler()函数)的时候就被创建的,前提是将宏定 义 configUSE_TIMERS 开启,如下图:

在xTimerCreateTimerTask()函数里面就是创建了一个软件定时器任务,就跟我们创建任务一样,支持动态与静态创建,我们暂时看动态创建的即可

1.xTimerCreateTimerTask()函数(创建守护任务函数)源码如下:

c 复制代码
BaseType_t xTimerCreateTimerTask( void )
{
BaseType_t xReturn = pdFAIL;

	/* This function is called when the scheduler is started if
	configUSE_TIMERS is set to 1.  Check that the infrastructure used by the
	timer service task has been created/initialised.  If timers have already
	been created then the initialisation will already have been performed. */
	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,
													"Tmr Svc",
													ulTimerTaskStackSize,
													NULL,
													( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
													pxTimerTaskStackBuffer,
													pxTimerTaskTCBBuffer );

			if( xTimerTaskHandle != NULL )
			{
				xReturn = pdPASS;
			}
		}
		#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;
}

可以看到这个函数与我们上面说的一样,调用了prvCheckForValidListAndQueue()函数来把软件定时器两条链表和「定时器命令队列」准备好,后面就创建守护任务(优先级最高)

2.接着我们来看看执行在守护任务栈上的prvTimerTask()函数:

prvTimerTask()源码如下:

c 复制代码
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 )
	{
		extern void vApplicationDaemonTaskStartupHook( void );

		/* Allow the application writer to execute some code in the context of
		this task at the point the task starts executing.  This is useful if the
		application includes initialisation code that would benefit from
		executing after the scheduler has been started. */
		vApplicationDaemonTaskStartupHook();
	}
	#endif /* configUSE_DAEMON_TASK_STARTUP_HOOK */

	for( ;; )
	{
		/* Query the timers list to see if it contains any timers, and if so,
		obtain the time at which the next timer will expire. */
		xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );

		/* If a timer has expired, process it.  Otherwise, block this task
		until either a timer does expire, or a command is received. */
		prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );

		/* Empty the command queue. */
		prvProcessReceivedCommands();
	}
}

if()语句不用管,我们直接看for(;;)循环的代码逻辑:

1.获取下一个要到期的软件定时器的时间

2.处理定时器或者将任务阻塞到下一个到期的软件定时器时间

而我们第一次启动定时器在经过这两个函数都是走空分支,所以先不用分析,我们先分析3

(2.3).读取"定时器命令队列",处理相应命令

prvProcessReceivedCommands()函数源码如下:

c 复制代码
static void	prvProcessReceivedCommands( void )
{
DaemonTaskMessage_t xMessage;
Timer_t *pxTimer;
BaseType_t xTimerListsWereSwitched, xResult;
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 )
		{
			/* Negative commands are pended function calls rather than timer
			commands. */
			if( xMessage.xMessageID < ( BaseType_t ) 0 )
			{
				const CallbackParameters_t * const pxCallback = &( xMessage.u.xCallbackParameters );

				/* The timer uses the xCallbackParameters member to request a
				callback be executed.  Check the callback is not NULL. */
				configASSERT( pxCallback );

				/* Call the function. */
				pxCallback->pxCallbackFunction( pxCallback->pvParameter1, pxCallback->ulParameter2 );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* INCLUDE_xTimerPendFunctionCall */

		/* Commands that are positive are timer commands rather than pended
		function calls. */
		if( xMessage.xMessageID >= ( BaseType_t ) 0 )
		{
			/* The messages uses the xTimerParameters member to work on a
			software timer. */
			pxTimer = xMessage.u.xTimerParameters.pxTimer;

			if( listIS_CONTAINED_WITHIN( NULL, &( pxTimer->xTimerListItem ) ) == pdFALSE )
			{
				/* The timer is in a list, remove it. */
				( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			traceTIMER_COMMAND_RECEIVED( pxTimer, xMessage.xMessageID, xMessage.u.xTimerParameters.xMessageValue );

			/* In this case the xTimerListsWereSwitched parameter is not used, but
			it must be present in the function call.  prvSampleTimeNow() must be
			called after the message is received from xTimerQueue so there is no
			possibility of a higher priority task adding a message to the message
			queue with a time that is ahead of the timer daemon task (because it
			pre-empted the timer daemon task after the xTimeNow value was set). */
			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 :
					/* Start or restart a timer. */
					if( prvInsertTimerInActiveList( pxTimer,  xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks, xTimeNow, xMessage.u.xTimerParameters.xMessageValue ) != pdFALSE )
					{
						/* The timer expired before it was added to the active
						timer list.  Process it now. */
						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 :
					/* The timer has already been removed from the active list.
					There is nothing to do here. */
					break;

				case tmrCOMMAND_CHANGE_PERIOD :
				case tmrCOMMAND_CHANGE_PERIOD_FROM_ISR :
					pxTimer->xTimerPeriodInTicks = xMessage.u.xTimerParameters.xMessageValue;
					configASSERT( ( pxTimer->xTimerPeriodInTicks > 0 ) );

					/* The new period does not really have a reference, and can
					be longer or shorter than the old one.  The command time is
					therefore set to the current time, and as the period cannot
					be zero the next expiry time can only be in the future,
					meaning (unlike for the xTimerStart() case above) there is
					no fail case that needs to be handled here. */
					( void ) prvInsertTimerInActiveList( pxTimer, ( xTimeNow + pxTimer->xTimerPeriodInTicks ), xTimeNow, xTimeNow );
					break;

				case tmrCOMMAND_DELETE :
					/* The timer has already been removed from the active list,
					just free up the memory if the memory was dynamically
					allocated. */
					#if( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 0 ) )
					{
						/* The timer can only have been allocated dynamically -
						free it again. */
						vPortFree( pxTimer );
					}
					#elif( ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) && ( configSUPPORT_STATIC_ALLOCATION == 1 ) )
					{
						/* The timer could have been allocated statically or
						dynamically, so check before attempting to free the
						memory. */
						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;
			}
		}
	}
}

(1)判断是否成功从定时器队列上接受到数据,如果接收到数据,就执行while()循环处理每一个数据(注意这里阻塞时间要设置0)

(2)#if ( INCLUDE_xTimerPendFunctionCall == 1 ) 是一条件编译语句:当该宏定义为 1 时,表示允许使用「定时器回调函数投递」功能,此时才会编译并执行下面这个 if 分支。

(3)这个分支处理的并不是普通的定时器命令,而是「在守护任务上下文中执行一个回调函数」的请求。它的处理过程是:

守护任务先从定时器命令队列中接收一条 DaemonTaskMessage_t 类型的消息,并把它拷贝到局部变量 xMessage 中;然后从这条消息里取出要执行的回调函数地址,再把 xMessage 的另外两个参数成员作为实参传入该回调函数并调用它。

我们可以结合 DaemonTaskMessage_t 结构体来理解:

c 复制代码
typedef struct tmrTimerParameters
{
	TickType_t			xMessageValue;		/*<< An optional value used by a subset of commands, for example, when changing the period of a timer. */
	Timer_t *			pxTimer;			/*<< The timer to which the command will be applied. */
} TimerParameter_t;


typedef struct tmrCallbackParameters
{
	PendedFunction_t	pxCallbackFunction;	/* << The callback function to execute. */
	void *pvParameter1;						/* << The value that will be used as the callback functions first parameter. */
	uint32_t ulParameter2;					/* << The value that will be used as the callback functions second parameter. */
} CallbackParameters_t;

/* The structure that contains the two message types, along with an identifier
that is used to determine which message type is valid. */
typedef struct tmrTimerQueueMessage
{
	BaseType_t			xMessageID;			/*<< The command being sent to the timer service task. */
	union
	{
		TimerParameter_t xTimerParameters;

		/* Don't include xCallbackParameters if it is not going to be used as
		it makes the structure (and therefore the timer queue) larger. */
		#if ( INCLUDE_xTimerPendFunctionCall == 1 )
			CallbackParameters_t xCallbackParameters;
		#endif /* INCLUDE_xTimerPendFunctionCall */
	} u;
} DaemonTaskMessage_t;

可以看到,TimerParameter_t xTimerParameters 和 CallbackParameters_t xCallbackParameters 位于同一个 union(联合体)中,两者共用同一块内存地址,也就是说它们是一组互斥使用的成员------同一时刻只有其中一个有效,具体使用哪一个由 xMessageID 决定:

  • 当这条消息用于处理定时器命令 时,使用 xTimerParameters,它保存发送命令时的系统时刻 tick,以及要操作的定时器句柄;
  • 当这条消息用于执行回调函数 时,使用 xCallbackParameters,它保存要执行的回调函数地址,以及要传入该回调函数的两个参数。

这也正是前面代码先通过 xMessage.xMessageID 的正负来分流的原因:负数表示「执行回调函数」,非负数表示「处理定时器命令」。

我们在学习「在中断中向事件组写入或读出事件标志位」时,底层用到的就是这个机制------中断服务函数无法直接操作事件组,而是通过 xTimerPendFunctionCallFromISR() 向守护任务投递一个回调函数,由守护任务在任务上下文中完成真正的操作。

(4)如果ID号>=0,就是走处理定时器命令的分支,即该if()语句分支

(5)获取本次要操控的定时器句柄

(6)listIS_CONTAINED_WITHIN()该函数宏定义如下:

c 复制代码
#define listIS_CONTAINED_WITHIN( pxList, pxListItem ) ( ( BaseType_t ) ( ( pxListItem )->pvContainer == ( void * ) ( pxList ) ) )

判断该定时器的定时器节点是否插在哪个链表上,如果有那就移除

(7)该函数内部源码如下:

c 复制代码
static TickType_t prvSampleTimeNow( BaseType_t * const pxTimerListsWereSwitched )
{
TickType_t xTimeNow;
PRIVILEGED_DATA static TickType_t xLastTime = ( TickType_t ) 0U; /*lint !e956 Variable is only accessible to one task. */

	xTimeNow = xTaskGetTickCount();

	if( xTimeNow < xLastTime )
	{
		prvSwitchTimerLists();
		*pxTimerListsWereSwitched = pdTRUE;
	}
	else
	{
		*pxTimerListsWereSwitched = pdFALSE;
	}

	xLastTime = xTimeNow;

	return xTimeNow;
}

1.xLastTime 是 static,程序启动时初始化为 0,之后每次调用都保存本次采样值。

2.获取当前系统tick时刻,赋值给xTimeNow

3、4:判断当前时刻与上一次调用该函数时的xLastTime时刻,如果小于说明,系统tick时刻发生了溢出,说明当前待激活链表的全部定时器都超时了,要马上调用prvSwitchTimerLists()函数,将该链表上的全部定时器唤醒,执行它们的回调函数,并且交换 pxCurrentTimerList 和 pxOverflowTimerList 两个指针,让原来的溢出链表成为新的当前链表,最后*pxTimerListsWereSwitched赋值为pdTURE

5.本次 xTimeNow < xLastTime 说明 tick 回绕,调用 prvSwitchTimerLists() 并置 *pxTimerListsWereSwitched =pdTRUE;否则置 pdFALSE。

6、7:返回调用该函数的系统tick时刻

(8)根据拷贝来的xMessage.xMessageID,看看要执行的是哪个指令,如果是(8)红框内的指令就执行下面的代码

(9)prvInsertTimerInActiveList()函数(将定时器插入到定时器链表函数),该函数内部如下

c 复制代码
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 )
	{
		/* Has the expiry time elapsed between the command to start/reset a
		timer was issued, and the time the command was processed? */
		if( ( ( TickType_t ) ( xTimeNow - xCommandTime ) ) >= pxTimer->xTimerPeriodInTicks ) /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
		{
			/* The time between a command being issued and the command being
			processed actually exceeds the timers period.  */
			xProcessTimerNow = pdTRUE;
		}
		else
		{
			vListInsert( pxOverflowTimerList, &( pxTimer->xTimerListItem ) );
		}
	}
	else
	{
		if( ( xTimeNow < xCommandTime ) && ( xNextExpiryTime >= xCommandTime ) )
		{
			/* If, since the command was issued, the tick count has overflowed
			but the expiry time has not, then the timer must have already passed
			its expiry time and should be processed immediately. */
			xProcessTimerNow = pdTRUE;
		}
		else
		{
			vListInsert( pxCurrentTimerList, &( pxTimer->xTimerListItem ) );
		}
	}

	return xProcessTimerNow;
}

我们先分析传进来的参数:

pxTimer:要操作的定时器句柄

xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks:调用发送定时器指令函数时的系统时刻+该定时器要休眠的时间tick

xTimeNow:当前执行守护任务的系统时刻tick

xMessage.u.xTimerParameters.xMessageValue:调用发送定时器指令函数时的系统时刻

1、2:这两个函数先初始化定时器节点,使用的宏定义如下:

c 复制代码
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue )  ( ( pxListItem )->xItemValue = ( xValue ) )

#define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner ) ( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) )

它们的作用是:把定时器节点的 xItemValue 设置为「调用发送定时器指令函数时的系统 tick + 该定时器要休眠的 tick 数」,也就是该定时器被唤醒的系统时刻;同时让节点的 pvOwner 指向这个定时器的句柄。

  1. 如果 xNextExpiryTime <= xTimeNow,即当前守护任务拿到的系统 tick 已经不小于定时器设置的唤醒时刻,说明定时器可能已经超时了。但此时还不能直接下结论,因为唤醒时刻可能落在 tick 计数溢出后的区间,所以需要进一步判断。

  2. 接着执行 ( TickType_t ) ( xTimeNow - xCommandTime ) &gt;= pxTimer->xTimerPeriodInTicks:用当前守护任务时刻 xTimeNow 减去命令发出时刻 xCommandTime,如果差值已经大于等于定时器的周期,说明从发出启动/复位命令到守护任务现在处理命令,系统已经整整走过了一个定时周期,因此该定时器的唤醒时刻一定不大于当前 xTimeNow。在第 3 点的前提下,可以确定定时器已经超时,于是把 xProcessTimerNow 置为 pdTRUE,表示需要马上处理该定时器的回调函数。

  3. 如果不满足第 4 点的"整周期"条件,说明定时器的唤醒时刻虽然已不大于当前 tick,但还没有晚到整整一个周期,它实际位于 tick 计数溢出后的时间段内,于是把该定时器节点插入 pxOverflowTimerList(溢出定时器链表)。注意这里调用的是 vListInsert(),会按 xItemValue 升序排序插入,这也是为什么进入本函数时要先把唤醒时刻写入节点 xItemValue。

  4. 回到外层判断,如果 xNextExpiryTime > xTimeNow,说明唤醒时刻还在未来,此时进入另一个分支,但仍需进一步判断是否存在"命令发出后 tick 已经溢出,而唤醒时刻还没有溢出"导致的超时。

  5. 若 ( xTimeNow &lt; xCommandTime ) &amp;&amp; ( xNextExpiryTime &gt;= xCommandTime ),说明当前守护任务时刻 xTimeNow 已经小于命令发出时刻 xCommandTime,即系统 tick 已经发生了溢出;而定时器的唤醒时刻仍然大于等于 xCommandTime,还没有溢出。综合起来就是:命令发出后系统 tick 已经绕了一圈,但定时器的唤醒时刻还停留在旧的时间段,因此该定时器实际上已经超时,应把 xProcessTimerNow 置为 pdTRUE,立即处理回调函数。

  6. 如果第 7 点的溢出条件也不成立,说明该定时器确实没有超时,并且唤醒时刻与当前守护任务时刻 xTimeNow 处于同一个 tick 计数周期内,于是调用 vListInsert() 将该定时器节点插入 pxCurrentTimerList(当前定时器链表),等待后续到点处理。

(10)如果系统能执行到这里,说明prvInsertTimerInActiveList()函数返回的结果是pdTURE,即要执行该定时器的回调函数,这里是将该定时器句柄作为参数传入到该定时器控制块保存的回调函数(补充:为什么要将该定时器的句柄作为参数传入呢?还记得在学习定时器结构体时有个成员*pvTimerID吗?我说过一个回调函数可以服务多个定时器,所以执行回调的时候,回调需要知道"这是哪个定时器到期"了)

(11)如果该定时器被设置为周期模式(uxAutoReload == pdTRUE),就会进入这个分支:再次调用 xTimerGenericCommand() 函数,重新向定时器命令队列投递一条 tmrCOMMAND_START_DONT_TRACE 命令。这里传入的时刻参数就是本次超时对应的唤醒 tick 时刻,也就是 xMessage.u.xTimerParameters.xMessageValue + pxTimer->xTimerPeriodInTicks。守护任务后面再处理这条命令时,会在这个"本次唤醒时刻"的基础上继续加上一个定时周期,因此该定时器下一次被唤醒的时刻 = 这次唤醒的时刻 + 要休眠的时间。

(12)该分支的执行的指令是更改该定时器的休眠时间,所以xMessage.u.xTimerParameters传入的是要更改该定时器的休眠时间,然后调用prvInsertTimerInActiveList()函数根据新到期时刻插入当前链表或溢出链表(跨回绕时进溢出链表),这里的返回值被 (void) 忽略,因为周期 > 0,新到期时刻在本轮检查中不会走"立即处理"分支。

(13)该分支的指令是删除该定时器,所以这里直接就释放该定时器控制块的空间,前面已经将该定时器的节点从所属链表移除,所以这里直接释放空间不影响系统

该分支是停止该定时器,前面已经将该定时器的节点从所属链表移除,所以这里什么都不用做

现在我们回到prvTimerTask()函数,看看它是如何处理软件定时器

(2.1)prvGetNextExpireTime()函数(获取下一个到期的软件定时器的唤醒时刻)

prvGetNextExpireTime()函数源码如下:

c 复制代码
static TickType_t prvGetNextExpireTime( BaseType_t * const pxListWasEmpty )
{
TickType_t xNextExpireTime;

	/* Timers are listed in expiry time order, with the head of the list
	referencing the task that will expire first.  Obtain the time at which
	the timer with the nearest expiry time will expire.  If there are no
	active timers then just set the next expire time to 0.  That will cause
	this task to unblock when the tick count overflows, at which point the
	timer lists will be switched and the next expiry time can be
	re-assessed.  */
	*pxListWasEmpty = listLIST_IS_EMPTY( pxCurrentTimerList );
	if( *pxListWasEmpty == pdFALSE )
	{
		xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );
	}
	else
	{
		/* Ensure the task unblocks when the tick count rolls over. */
		xNextExpireTime = ( TickType_t ) 0U;
	}

	return xNextExpireTime;
}

(1)listLIST_IS_EMPTY该函数的宏定义如下:

c 复制代码
#define listLIST_IS_EMPTY( pxList )	( ( BaseType_t ) ( ( pxList )->uxNumberOfItems == ( UBaseType_t ) 0 ) )

判断pxCurrentTimerList链表是否有定时器,有pxListWasEmpty==pdFALSE,没有pxListWasEmpty==pdTURE

(2)如果pxCurrentTimerList链表有定时器,就走该分支,然后获取该定时器节点的xItemValue(也就是唤醒时刻),赋值给xNextExpireTime

(3)这是该链表没有定时器时走的分支,注意:0 不是"没有时间",而是 tick 计数回绕到 0 的那一刻 的哨兵值,TickType_t 是 32 位无符号,0 - xTimeNow 会回绕成 2³² - xTimeNow,表示"距离 tick 回绕还剩多少 tick"。当前链表空、溢出链表非空时,守护任务就用这个超时睡到回绕点,醒来后切换到溢出链表。

(4)返回xNextExpireTime

所以该函数的作用就是:判断pxCurrentTimerList链表是否有定时器,如果有定时器就返回该定时器的唤醒时刻和判断该pxCurrentTimerList链表是否有定时器的结果

(2.2)prvProcessTimerOrBlockTask()函数(处理定时器或者阻塞该守护任务函数)

c 复制代码
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime, BaseType_t xListWasEmpty )
{
TickType_t xTimeNow;
BaseType_t xTimerListsWereSwitched;

	vTaskSuspendAll();
	{
		/* Obtain the time now to make an assessment as to whether the timer
		has expired or not.  If obtaining the time causes the lists to switch
		then don't process this timer as any timers that remained in the list
		when the lists were switched will have been processed within the
		prvSampleTimeNow() function. */
		xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
		if( xTimerListsWereSwitched == pdFALSE )
		{
			/* The tick count has not overflowed, has the timer expired? */
			if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )
			{
				( void ) xTaskResumeAll();
				prvProcessExpiredTimer( xNextExpireTime, xTimeNow );
			}
			else
			{
				/* The tick count has not overflowed, and the next expire
				time has not been reached yet.  This task should therefore
				block to wait for the next expire time or a command to be
				received - whichever comes first.  The following line cannot
				be reached unless xNextExpireTime > xTimeNow, except in the
				case when the current timer list is empty. */
				if( xListWasEmpty != pdFALSE )
				{
					/* The current timer list is empty - is the overflow list
					also empty? */
					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();
		}
	}
}

参数说明:

调用该函数传入的两个参数是调用prvGetNextExpireTime()得到的

xNextExpireTime:准备处理的定时器的唤醒时刻

xListWasEmpty:pxCurrentTimerList是否为空的结果

(1)挂起调度器(vTaskSuspendAll())。这里真正要防的,并不是其他 API 干扰内核对定时器的操作------因为任务/ISR 只投命令,链表操作只在守护任务,此刻正在运行的就是守护任务本身,并不存在"其他任务同时操作定时器链表"的竞争问题。它真正要防的是:从「采样当前系统 tick」到「判断定时器是否超时」这段过程被任务切换打断。

假设没有先挂起调度器,守护任务调用 xTaskGetTickCount() 采样到当前时刻后,还没来得及用这个时刻与最近定时器的唤醒时刻比较,就可能被切换走。等守护任务再次被调度回来时,系统 tick 已经又向前走了若干拍(比如几毫秒),但代码里拿到的仍是切换前的旧 tick 值。用这个旧 tick 去判断定时器是否超时,结果必然不准:可能切换前明明还没超时,切换期间刚好到达唤醒时刻,等切回来时实际上已经超时了;可由于仍然使用旧 tick,代码会得出"还没超时"的错误结论,导致本应被唤醒的定时器没有及时唤醒。

(2)prvSampleTimeNow()函数(采样当前系统时刻函数)

这个函数我们遇到过了

c 复制代码
static TickType_t prvSampleTimeNow( BaseType_t * const pxTimerListsWereSwitched )
{
TickType_t xTimeNow;
PRIVILEGED_DATA static TickType_t xLastTime = ( TickType_t ) 0U; /*lint !e956 Variable is only accessible to one task. */

	xTimeNow = xTaskGetTickCount();

	if( xTimeNow < xLastTime )
	{
		prvSwitchTimerLists();
		*pxTimerListsWereSwitched = pdTRUE;
	}
	else
	{
		*pxTimerListsWereSwitched = pdFALSE;
	}

	xLastTime = xTimeNow;

	return xTimeNow;
}

(1)在读取"定时器命令队列"的时候已经调用过该函数了,所以这里不会再执行,而xLastTime的值为上次调用该函数的系统tick时刻

(2)获取当前调用该函数的时刻

(3) 比较上次调用和这次调用的时刻,如果上次时刻>这次时刻,说明系统tick已经绕一圈了,要马上处理pxCurrentTimerList链表上的定时器,因为该链表已经是旧的链表了,所以要调用prvSwitchTimerLists()函数

prvSwitchTimerLists()函数(交换定时器待激活链表)源码如下:

c 复制代码
static void prvSwitchTimerLists( void )
{
TickType_t xNextExpireTime, xReloadTime;
List_t *pxTemp;
Timer_t *pxTimer;
BaseType_t xResult;

	/* The tick count has overflowed.  The timer lists must be switched.
	If there are any timers still referenced from the current timer list
	then they must have expired and should be processed before the lists
	are switched. */
	while( listLIST_IS_EMPTY( pxCurrentTimerList ) == pdFALSE )
	{
		xNextExpireTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxCurrentTimerList );

		/* Remove the timer from the list. */
		pxTimer = ( Timer_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );
		( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
		traceTIMER_EXPIRED( pxTimer );

		/* Execute its callback, then send a command to restart the timer if
		it is an auto-reload timer.  It cannot be restarted here as the lists
		have not yet been switched. */
		pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );

		if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )
		{
			/* Calculate the reload value, and if the reload value results in
			the timer going into the same timer list then it has already expired
			and the timer should be re-inserted into the current list so it is
			processed again within this loop.  Otherwise a command should be sent
			to restart the timer to ensure it is only inserted into a list after
			the lists have been swapped. */
			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.判断这个pxCurrentTimerList链表是否有定时器,如果有等于pdFALSE,进入该循环

2.获取该链表上的定时器节点的xItemValue值(也就是该定时器被唤醒的时刻)

3.获取该链表上的定时器的句柄

4.将该定时器的节点从所属链表移除

5.执行该定时器的回调函数,因为现在还待在该链表上的定时器都已经超时了,必须马上执行这些回调函数(注意该回调函数执行是在调度器挂起期间执行的,所以定时器回调必须极短、不能调用阻塞 API。)

7.如果该定时器的工作模式是周期模式(uxAutoReload == pdTRUE),那么执行完回调函数后,还需要把该定时器重新插入到定时器待激活链表中。注意:在正常唤醒的情况下,下一次唤醒时刻固然是"本次唤醒时刻 + 定时周期";但这里处理的是 tick 溢出导致的超时唤醒,所以计算基础要使用该定时器原本约定的唤醒时刻 xNextExpireTime,而不是当前系统时刻。

8.于是先计算该定时器的下一次唤醒时刻:

c 复制代码
xReloadTime = xNextExpireTime + pxTimer->xTimerPeriodInTicks;

也就是"原本约定的唤醒时刻 + 要休眠的时间"。

9.如果 xReloadTime > xNextExpireTime,说明加上一个定时周期后没有发生 tick 回绕,下一次唤醒时刻仍然落在旧的定时器待激活链表所覆盖的时间段内。此时直接将 xReloadTime 写入该定时器节点的 xItemValue,让节点的 pvOwner 指向该定时器控制块,然后重新插入旧的定时器待激活链表 pxCurrentTimerList。因为外层的 while 循环会一直遍历完旧链表、逐个唤醒其中的定时器,所以这个定时器随后还会被再次取出并唤醒,直到它的唤醒时刻落到新链表的时间段为止。

10.反之,如果 xReloadTime <= xNextExpireTime(即加上一个定时周期后 tick 发生了回绕),说明下一次唤醒时刻已经属于新的定时器待激活链表所覆盖的时间段。这时不能直接插回旧链表,而是调用 xTimerGenericCommand() 函数,向定时器命令队列发送 tmrCOMMAND_START_DONT_TRACE 指令。这样等后面链表交换完成后,守护任务再处理这条命令,才会把这个定时器插入到新的定时器待激活链表中。

11.当把旧链表中的定时器全部唤醒、并完成上述重插或重新投递命令后,就交换 pxCurrentTimerList 和 pxOverflowTimerList 两个指针,让原来的溢出链表成为新的当前链表。

回到prvSampleTimeNow()函数(采样当前系统时刻函数):

(4)将*pxTimerListsWereSwitched 赋值为pdTRUE,告诉后面的函数,交换了新的定时器待激活链表

(5)如果当前的系统tick时刻>上次调用该函数的时刻,说明系统tick并没有绕一圈,将*pxTimerListsWereSwitched 赋值为pdFALSE

(6)返回当前的系统tick时刻

回到prvProcessTimerOrBlockTask()函数(处理定时器或者阻塞该守护任务函数):

3.如果xTimerListsWereSwitched == pdFALSE,说明没有发送定时器待激活链表交换,也没有唤醒当前定时器待激活链表的定时器,执行if()语句分支(注意:我们传进去的是xTimerListsWereSwitched变量的地址,所以在prvSampleTimeNow()函数时修改了xTimerListsWereSwitched的值,所以我们通过它的地址就能知道它的值)

4.如果定时器待激活链表非空,同时最近要唤醒的定时器时刻<当前系统时刻,说明最近的定时器已经超时了,恢复调度器(因为采样时间+已经判断完该定时器超时了,后面就要执行该定时器的回调函数,所以无需挂起调度器了),调用prvProcessExpiredTimer()函数

prvProcessExpiredTimer()函数(处理超时定时器函数)源码如下:

c 复制代码
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 );

	/* Remove the timer from the list of active timers.  A check has already
	been performed to ensure the list is not empty. */
	( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
	traceTIMER_EXPIRED( pxTimer );

	/* If the timer is an auto reload timer then calculate the next
	expiry time and re-insert the timer in the list of active timers. */
	if( pxTimer->uxAutoReload == ( UBaseType_t ) pdTRUE )
	{
		/* The timer is inserted into a list using a time relative to anything
		other than the current time.  It will therefore be inserted into the
		correct list relative to the time this task thinks it is now. */
		if( prvInsertTimerInActiveList( pxTimer, ( xNextExpireTime + pxTimer->xTimerPeriodInTicks ), xTimeNow, xNextExpireTime ) != pdFALSE )
		{
			/* The timer expired before it was added to the active timer
			list.  Reload it now.  */
			xResult = xTimerGenericCommand( pxTimer, tmrCOMMAND_START_DONT_TRACE, xNextExpireTime, NULL, tmrNO_DELAY );
			configASSERT( xResult );
			( void ) xResult;
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	/* Call the timer callback. */
	pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer );
}

(1)获取已经超时的定时器句柄

(2)将该定时器的节点从所属链表移除

(3)再判断该定时器是否选择周期模式

(4)(5):如果该定时器选择周期模式,就把它的下一次唤醒时刻传入 prvInsertTimerInActiveList() 函数。如果该函数返回 pdTRUE,说明该定时器的下一次唤醒时刻也已经超时了------因为执行到这里之前,守护任务也可能本来就错过了多个周期,所以这里要调用 xTimerGenericCommand() 函数,向命令队列发送对该定时器的启动指令,等后面 prvProcessReceivedCommands() 函数处理这条命令时,会再次执行一次该定时器的回调函数;如果该函数没有返回 pdTRUE(未超时),则在 prvInsertTimerInActiveList() 内部已经根据唤醒时刻把该定时器挂回对应的定时器待激活链表,等待下次到期唤醒。

(6)执行该定时器的回调函数

回到prvProcessTimerOrBlockTask()函数(处理定时器或者阻塞该守护任务函数)

5.反之,这个定时器没有超时,走else分支

6.如果当前定时器待激活链表为空,再判断一下溢出定时器待激活链表是否为空,如果为空xListWasEmptypdTURE,如果溢出定时器待激活链表有定时器xListWasEmptypdFALSE

7.执行到这说明当前定时器待激活链表是没有定时器要唤醒的或者没有定时器,那就调用vQueueWaitForMessageRestricted()函数,将守护任务阻塞

vQueueWaitForMessageRestricted()函数源码如下:

c 复制代码
#if ( configUSE_TIMERS == 1 )

	void vQueueWaitForMessageRestricted( QueueHandle_t xQueue, TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely )
	{
	Queue_t * const pxQueue = ( Queue_t * ) xQueue;

		/* This function should not be called by application code hence the
		'Restricted' in its name.  It is not part of the public API.  It is
		designed for use by kernel code, and has special calling requirements.
		It can result in vListInsert() being called on a list that can only
		possibly ever have one item in it, so the list will be fast, but even
		so it should be called with the scheduler locked and not from a critical
		section. */

		/* Only do anything if there are no messages in the queue.  This function
		will not actually cause the task to block, just place it on a blocked
		list.  It will not block until the scheduler is unlocked - at which
		time a yield will be performed.  If an item is added to the queue while
		the queue is locked, and the calling task blocks on the queue, then the
		calling task will be immediately unblocked when the queue is unlocked. */
		prvLockQueue( pxQueue );
		if( pxQueue->uxMessagesWaiting == ( UBaseType_t ) 0U )
		{
			/* There is nothing in the queue, block for the specified period. */
			vTaskPlaceOnEventListRestricted( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait, xWaitIndefinitely );
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
		prvUnlockQueue( pxQueue );
	}

#endif /* configUSE_TIMERS */

(1):将定时器命令队列上锁,防止在操作该队列时,又有别的API函数操作该队列

(2):如果该命令队列为空,就调用vTaskPlaceOnEventListRestricted()函数,传入的参数有:命令队列的等待从队列接受信息链表、当前系统时刻距离最近定时器要唤醒的时刻还差多少时刻、定时器的两个待激活链表状态

vTaskPlaceOnEventListRestricted()函数源码如下:

c 复制代码
#if( configUSE_TIMERS == 1 )

	void vTaskPlaceOnEventListRestricted( List_t * const pxEventList, TickType_t xTicksToWait, const BaseType_t xWaitIndefinitely )
	{
		configASSERT( pxEventList );

		/* This function should not be called by application code hence the
		'Restricted' in its name.  It is not part of the public API.  It is
		designed for use by kernel code, and has special calling requirements -
		it should be called with the scheduler suspended. */


		/* Place the event list item of the TCB in the appropriate event list.
		In this case it is assume that this is the only task that is going to
		be waiting on this event list, so the faster vListInsertEnd() function
		can be used in place of vListInsert. */
		vListInsertEnd( pxEventList, &( pxCurrentTCB->xEventListItem ) );

		/* If the task should block indefinitely then set the block time to a
		value that will be recognised as an indefinite delay inside the
		prvAddCurrentTaskToDelayedList() function. */
		if( xWaitIndefinitely != pdFALSE )
		{
			xTicksToWait = portMAX_DELAY;
		}

		traceTASK_DELAY_UNTIL( ( xTickCount + xTicksToWait ) );
		prvAddCurrentTaskToDelayedList( xTicksToWait, xWaitIndefinitely );
	}

#endif /* configUSE_TIMERS */

(1)将守护任务的事件节点插入到定时器命令队列的等待接受信息链表

(2)(3):这里分几种情况阻塞:

情况一:如果定时器的两个待激活链表都为空,xTicksToWait等于portMAX_DELAY,xWaitIndefinitely等于pdTURE,那么守护任务的状态节点会被插到暂停链表,永久等待直到有消息发送给命令队列

情况二:定时器溢出待激活链表中有定时器。此时 xTicksToWait = xNextExpireTime - xTimeNow,也就是从现在走到 tick 计数回绕到 0,还剩多少个 tick。也就是说,该守护任务会一直阻塞到系统 tick 回绕到 0 的那一刻,然后再继续执行。

具体又分为两种情形:

  • 如果在执行到 xQueueReceive() 函数之前,刚刚好有消息发送到定时器命令队列,那么守护任务可以进入while()循环:然后调用 prvSampleTimeNow() 采样当前系统时刻,该函数检测到 tick 已经回绕后,会交换 pxCurrentTimerList 和 pxOverflowTimerList 两个指针,让原来的溢出待激活链表成为新的当前待激活链表,接着再接着处理新收到的定时器命令。
  • 如果没有消息发送到队列,继续往下执行到 prvProcessTimerOrBlockTask() 函数;该函数内部同样会先调用 prvSampleTimeNow() 采样当前系统时刻,检测到 tick 回绕后交换 pxCurrentTimerList 和 pxOverflowTimerList 两个指针,让原来的溢出待激活链表成为新的当前待激活链表。

(8)恢复调度器,在挂起期间如果有申请任务切换则在xTaskResumeAll()统一执行,如果没有在xTaskResumeAll()执行,则调用portYIELD_WITHIN_API()函数"补上一刀",因为守护任务阻塞自己并没有申请任务切换

(9)如果定时器的两个待激活链表交换了,说明无需再处理旧的待激活链表和阻塞该守护任务,而是看看命令队列有没有消息,没有消息就去新的待激活链表处理定时器或者阻塞守护任务

总结:

适用范围:首次创建定时器并向命令队列发送启动指令,且当前链表和溢出链表都为空。(运行中再 xTimerStart 时链表不一定空,会走正常分支,不完全等同下面的流程。)

(1)首次启动:命令驱动路径

  1. 两个链表为空,前两个函数走空分支
  • prvGetNextExpireTime():当前链表为空,返回 xNextExpireTime = 0(tick 回绕哨兵),xListWasEmpty = pdTRUE。
  • prvProcessTimerOrBlockTask():当前链表为空,到期条件不成立,进入阻塞分支;但命令队列里已经有 START 命令,vQueueWaitForMessageRestricted() 检查到 uxMessagesWaiting != 0直接返回,不阻塞。恢复调度器后返回。
  1. 进入命令队列处理
  • prvProcessReceivedCommands() 用while (xQueueReceive(xTimerQueue, &xMessage, 0) != pdFAIL)逐条处理,直到队列为空。
  • 根据 xMessage.xMessageID 分流:负数是 pend function call(在守护任务上下文执行回调);非负数是定时器命令。注意 0 也是合法命令(tmrCOMMAND_START_DONT_TRACE),所以判断是 >=0,不是 > 0。
  1. 处理 START / RESET 命令
  • 先把该定时器节点从它所在的链表摘下(如果 pvContainer != NULL)。
  • 调用 prvSampleTimeNow() 采样当前 tick;这里也可能发现 tick 回绕,若发现会先切换链表再继续处理命令。
  • 调用prvInsertTimerInActiveList(pxTimer, xMessageValue + period, xTimeNow, xMessageValue):
  • 候选到期时刻 = 命令发出时的 tick + 周期;
  • 同时用 xMessageValue 作为命令时刻,处理 tick 回绕的两种情况。
  1. 根据是否已过期走不同分支
  • 未过期:根据候选到期时刻落在当前 tick 周期还是下一个周期,插入 pxCurrentTimerList 或 pxOverflowTimerList。prvInsertTimerInActiveList() 返回pdFALSE,本轮不执行回调,也不投新命令。
  • 已过期(返回 pdTRUE,函数内部没有插入链表):立即执行该定时器的回调函数。
  1. 已过期且周期模式:补课
  • 回调之后,如果 uxAutoReload == pdTRUE,向命令队列投一条 tmrCOMMAND_START_DONT_TRACE,其 value = 这次错过的到期时刻 V + P,不是 now + P。
  • 下一次处理这条命令时,候选到期时刻变成 (V + P) + P = V + 2P;每补一轮前进一个周期,直到候选 > now,才插入链表并停止。
  • 接收命令是在 while 循环里;守护任务自己投的补课命令也会被同一次 prvProcessReceivedCommands() 收回来继续处理,直到定时器挂上链表、队列清空。
  • 因为周期 P > 0(创建和改周期都有 configASSERT(period > 0)),这个追赶是有限次的,不会死循环。
  • 正常插入链表的定时器(未过期)不产生补课命令,所以 while 不会再次处理它。
  1. 处理完命令后回到循环顶部
  • prvProcessReceivedCommands() 返回后,回到 prvTimerTask 的 for(;😉 顶部,重新执行 prvGetNextExpireTime()。

(2)prvGetNextExpireTime:看表

  • 当前链表有定时器:链表按到期时刻升序,取队头的 xItemValue 赋给 xNextExpireTime,*pxListWasEmpty = pdFALSE。
  • 当前链表无定时器:xNextExpireTime = 0,*pxListWasEmpty = pdTRUE。
  • 0 不是"没有时间",而是 tick 计数回绕到 0 的时刻 的哨兵值。
  • 当前链表空、溢出链表非空时,后面用 0 - xTimeNow(32 位无符号回绕)算出"距离 tick 回绕还剩多少 tick",守护任务睡到回绕点,醒来后切换链表、处理原来的溢出链表。
  • 如果两条链表都空,xListWasEmpty 保持 pdTRUE,守护任务无限期等待命令,不会因 tick 回绕而醒。

(3)prvProcessTimerOrBlockTask:处理到期或阻塞

  1. 先采样并检查回绕
  • 函数开头 vTaskSuspendAll() 挂起调度器,然后调用 prvSampleTimeNow() 采样当前 tick。
  • 注意:采样时调度器已经挂起。挂起期间 SysTick 仍会来,但 tick 只记进 uxPendedTicks,xTickCount 不前进。
  • 真正没有保护的是 prvGetNextExpireTime() 返回到 vTaskSuspendAll() 执行之前这一段;从上一次采样到现在,tick 可能已经绕了一圈。所以这次采样若发现 xTimeNow < xLastTime,说明回绕。
  1. 发生回绕
  • prvSampleTimeNow() 内部调用 prvSwitchTimerLists():先处理当前链表上已经到期的定时器(执行回调;周期定时器可能重插或投补课命令),再交换 pxCurrentTimerList 与pxOverflowTimerList。
  • 此时 xTimerListsWereSwitched = pdTRUE。prvProcessTimerOrBlockTask() 不需要再处理之前 prvGetNextExpireTime() 得到的旧到期时刻,也不阻塞;它恢复调度器后直接返回。
  • 随后 prvProcessReceivedCommands() 清命令队列;下一轮 for(;😉 重新 prvGetNextExpireTime(),这时读到的就是原来的溢出链表(它已经变成当前链表)。
  1. 没有发生回绕:正常处理
  • 队头已到期(xListWasEmpty == pdFALSE && xNextExpireTime <= xTimeNow):
  • xTaskResumeAll() 恢复调度器,然后调用 prvProcessExpiredTimer() 处理队头这一个定时器。
  • 函数内部:摘下队头节点 → 若是周期模式,先调用 prvInsertTimerInActiveList(nextExpiry, ...):
  • 返回 pdFALSE → 直接插回当前/溢出链表,正常重装,不投命令;
  • 返回 pdTRUE → 下一次到期也已经过了,投 START_DONT_TRACE 补课命令,交给后续 prvProcessReceivedCommands() 处理(可能连续补多个周期)。
  • 最后执行该定时器的回调函数。
  • 注意:prvProcessTimerOrBlockTask() 每次调用只处理一个到期定时器;下一轮 for(;😉 才会处理下一个,循环在外层,不在这个函数内部。
  • 队头未到期 / 当前链表为空:进入阻塞分支,调用vQueueWaitForMessageRestricted(xTimerQueue, xNextExpireTime - xTimeNow, xListWasEmpty)。
  • vQueueWaitForMessageRestricted() 会先检查命令队列:如果队列非空,就直接返回,不会阻塞;只有在队列为空时,才会把守护任务挂到等待接收链表和延时链表上。真正让出 CPU 的动作发生在返回到 prvProcessTimerOrBlockTask() 之后,由 xTaskResumeAll() / portYIELD_WITHIN_API() 统一执行。
  • 三种情况(假设命令队列为空):
当前链表 溢出链表 xWaitIndefinitely 等待行为
非空、队头未到期 --- pdFALSE 限时等到队头到期(或命令先到)
空 非空 pdFALSE 限时等到 tick 回绕(0 - xTimeNow),或命令先到
空 空 pdTRUE 无限期等待,只有命令能唤醒

一句话总结:

守护任务的 for(;😉 就是"① 看表 → ② 处理到期或阻塞等待(最近的到期 / 命令 / tick 回绕)→ ③ 清命令队列"。定时器只提供"到期时刻 + 回调",真正睡/醒的是 Tmr Svc。

上面讲的只是守护任务的执行流程。实际上,在我们启动调度器时,FreeRTOS 会自动帮我们创建守护任务和定时器命令队列,因此应用层无需关心这些底层机制,只需要在主函数中做好三件事:创建定时器、实现回调函数,然后向命令队列发送该定时器的句柄和对应的操作指令即可。

实验:

本次实验目的,创建两个定时器,两个定时器都选择周期模式,然后创建两个回调函数,一个回调函数向队列发送消息,一个回调从队列接收消息,代码如下:

c 复制代码
#include "stm32f4xx.h"
#include "./led/bsp_led.h"
#include "bsp_debug_usart.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "event_groups.h"

TaskHandle_t APP_TaskHandel=NULL;
TaskHandle_t TaskA_Handle=NULL;
QueueHandle_t Queue1_Handle=NULL;
SemaphoreHandle_t Semaphore_Text1Handel=NULL;
SemaphoreHandle_t Semaphore_Text2Handel=NULL;
/*创建互斥量句柄*/
SemaphoreHandle_t Semaphore_MutexHandel=NULL;

/*创建事件组句柄*/
EventGroupHandle_t EventGroup_Text=NULL;

TimerHandle_t TimerA_Text=NULL;
TimerHandle_t TimerB_Text=NULL;

#define Event_Key1		(1<<0)
#define Event_Key2		(1<<1)
#define Event_USART		(1<<2)

static void Hadware_Init(void);
void APP_CreatTask(void* parameter);
void TaskA(void* parameter);
// void TaskB(void* parameter);
// void TaskC(void* parameter);
// void TaskD(void* parameter);

/*定时器回调函数*/
void TimerA_CallBack(TimerHandle_t xTimer);
void TimerB_CallBack(TimerHandle_t xTimer);


void Delay(uint32_t time);

uint8_t	Flag1;
uint8_t	Flag2;

int main(void)
{
  Hadware_Init();

  /*创建应用任务*/
  xTaskCreate(APP_CreatTask,"APP_Task",128,NULL,2,&APP_TaskHandel);

  if(APP_TaskHandel!=NULL)
  {
	printf("任务创建成功\r\n");
  }
  else
  {
	printf("任务创建失败\r\n");
  }

  /*开启任务调度*/
  vTaskStartScheduler();

  while(1)
  {

  }
}

static void Hadware_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	Debug_USART_Config();

	Key_Init();
  
}

/*创建应用任务*/
void APP_CreatTask(void* parameter)
{
	xTaskCreate(TaskA,"TaskA",128,NULL,1,&TaskA_Handle);

	Queue1_Handle=xQueueCreate(100,2);

	Semaphore_Text1Handel=xSemaphoreCreateBinary();
	/*创建完二值信号量,信号量默认为0,所以要释放一个*/
	xSemaphoreGive(Semaphore_Text1Handel);

	/*创建计数型信号量*/
	Semaphore_Text2Handel=xSemaphoreCreateCounting(2,2);

	/*创建普通互斥量*/
	Semaphore_MutexHandel=xSemaphoreCreateMutex();

	/*创建事件组*/
	EventGroup_Text=xEventGroupCreate();

	/*创建软件定时器*/
	TimerA_Text=xTimerCreate("TimerA",200,pdTRUE,(void *)1,TimerA_CallBack);
	TimerB_Text=xTimerCreate("TimerB",300,pdTRUE,(void *)2,TimerB_CallBack);

	vTaskDelete(APP_TaskHandel);
}

/*创建任务A*/
void TaskA(void* parameter)
{
	EventBits_t i=0;
	i=xTimerStart(TimerA_Text,0);
	if(i==pdPASS)
	{
		printf("定时器A启动成功\r\n");
	}
	xTimerStart(TimerB_Text,0);
	if(i==pdPASS)
	{
		printf("定时器B启动成功\r\n");
	}
	while(1)
	{
		printf("等待定时器\r\n");
		vTaskDelay(50);		
	}
}

void TimerA_CallBack(TimerHandle_t xTimer)
{
	uint8_t i=0;
	uint16_t data=0;
	for(i=0;i<5;i++)
	{
		data++;
		xQueueSend(Queue1_Handle,&data,0);
		printf("软件定时器A,接收消息:0x%x\r\n",data);
	}
}

void TimerB_CallBack(TimerHandle_t xTimer)
{
	uint8_t i=0;
	uint16_t data=0;
	for(i=0;i<5;i++)
	{
		xQueueReceive(Queue1_Handle,&data,0);
		printf("软件定时器B,接收消息:0x%x\r\n",data);
	}
}

因为回调函数不能出现有阻塞守护任务的情况,所以我们向队列发送消息和接受消息等待的时间都选择0,实验结果如下:

可以看到,串口先输出四次"等待定时器",随后守护任务被唤醒并激活定时器 A,执行定时器 A 的回调函数;之后又输出两次"等待定时器",守护任务再次被唤醒并激活定时器 B,执行定时器 B 的回调函数。

这是因为:TaskA 每轮循环打印一次"等待定时器"并调用 vTaskDelay(50),大约每 50ms 输出一次;而定时器 A 的周期是 200ms,定时器 B 的周期是 300ms。因此大约每 200ms(即输出 4 次"等待定时器")触发一次定时器 A,每 300ms(即再输出 2 次"等待定时器")触发一次定时器 B,如上图所示。

需要补充的是:本实验仅做演示,不能一直运行下去,否则会导致队列被写满。因为定时器 A 每 200ms 会向队列发送 5 条消息,而定时器 B 每 300ms 才从队列接收 5 条消息,发送速率高于接收速率,持续运行下去消息队列空间最终会被占满。

相关推荐
comyccccc1 小时前
PID扩展板布局布线全解析:高效设计六大优势学习笔记
单片机·嵌入式硬件
三佛科技-187366133971 小时前
分享智能感应开关开发案例
单片机·嵌入式硬件
Zw-awa1 小时前
机器周期与共地:时钟基准和电压参考基准
stm32
Brilliantwxx2 小时前
【STM32】RTC 完结篇:日期操作、闹钟中断与附录(时间戳 / 闹钟 / NTP)
服务器·c语言·stm32·单片机·嵌入式硬件·实时音视频
深圳老胡2 小时前
STM32C5 应用阶段开发难点解析:从选型到量产的实战经验
笔记·stm32·单片机·代码规范
李永奉2 小时前
杰理可视化SDK开发-【BUG】AC7006F8芯片开启一拖二和双麦通话语音算法后,设置触摸音在唤醒语音助手和通话时触摸音没作用问题
单片机·嵌入式硬件·mcu·物联网·电脑·bug
学逆向的2 小时前
C 悬空指针
c语言·开发语言·网络安全
·present·15 小时前
CSAPP(lecture 4_01)
c语言·c++
南山电子nscn17 小时前
中科芯CETC汽车电池管理系统BMS进口芯片国产型号替代指南
单片机·汽车·bms