FreeRTOS学习笔记(五)——应用开发(三)

0x01 软件定时器

定时器概念: 定时器,是指从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器的周期与频率。

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

软件定时器回调函数的上下文是任务,下文所说的定时器均为软件定时器。软件定时器在被创建之后,当经过设定的时钟计数值后会触发用户定义的回调函数。定时精度与系统时钟的周期有关。

一般系统利用 SysTick 作为软件定时器的基础时钟,软件定时器的回调函数类似硬件的中断服务函数,所以,回调函数也要快进快出,而且回调函数中不能有任何阻塞任务运行的情况。

RTOS中的软件定时器支持单次模式和周期模式:

  • 单次模式:当用户创建了定时器并启动了定时器后,定时时间到了,只执行一次回调函数之后就将该定时器进入休眠状态,不再重新执行。
  • 周期模式:这个定时器会按照设置的定时时间循环执行回调函数,直到用户将定时器删除。

FreeRTOS 通过一个 prvTimerTask 任务(也叫守护任务 Daemon)管理软定时器,它是在启动调度器时自动创建的,为了满足用户定时需求。要使用软件定时器需要将FreeRTOSConfig.h中的宏定义configUSE_TIMERS设置为1。开启这个宏也需要开启configTIMER_TASK_PRIORITYconfigTIMER_QUEUE_LENGTHconfigTIMER_TASK_STACK_DEPTH这些宏,否则编译会报如下错误:

#if configUSE_TIMERS == 1

	#ifndef configTIMER_TASK_PRIORITY
		#error If configUSE_TIMERS is set to 1 then configTIMER_TASK_PRIORITY must also be defined.
	#endif /* configTIMER_TASK_PRIORITY */

	#ifndef configTIMER_QUEUE_LENGTH
		#error If configUSE_TIMERS is set to 1 then configTIMER_QUEUE_LENGTH must also be defined.
	#endif /* configTIMER_QUEUE_LENGTH */

	#ifndef configTIMER_TASK_STACK_DEPTH
		#error If configUSE_TIMERS is set to 1 then configTIMER_TASK_STACK_DEPTH must also be defined.
	#endif /* configTIMER_TASK_STACK_DEPTH */

#endif /* configUSE_TIMERS */
  • configTIMER_TASK_PRIORITY定义的是定时器任务的优先级,值可以是0~(configMAX_PRIORITIES-1),这个值定义的就是定时器任务prvTimerTask的优先级;

  • configTIMER_QUEUE_LENGTH定义的是定时器队列的长度,也就是能定义几个软件定时器;

  • configTIMER_TASK_STACK_DEPTH定义的是定时器任务的堆栈大小,图中定义的是256字,这个堆栈也是定时器回调函数会使用的堆栈,所以不能定义得太小。

这里使用预编译指令#if(依据),#error(提示信息)。这个可以学起来。如果报错没有加入这几个宏定义,可以自己加入:

	#define configTIMER_TASK_PRIORITY 		(2)
	#define configTIMER_QUEUE_LENGTH		10
	#define configTIMER_TASK_STACK_DEPTH 	256

加入后如果报错: L6218E: Undefined symbol vApplicationGetTimerTaskMemory (referred from timers.o).,可以检查自己的宏configSUPPORT_STATIC_ALLOCATION是否置位,需要更改为0:

应用场景

需要定时任务,但是硬件定时器受到硬件的限制,数量上不足以满足其要求,其缺陷是容易被其他中断打断,因为软件定时器执行的上下文环境为任务,所以软件定时器适用于对时间精度要求不高的任务,一些辅助型的任务

定时器精度

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

运作机制

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

PRIVILEGED_DATA static List_t xActiveTimerList1 = {
    
    0};
PRIVILEGED_DATA static List_t xActiveTimerList2 = {
    
    0};
PRIVILEGED_DATA static List_t *pxCurrentTimerList = NULL;
PRIVILEGED_DATA static List_t *pxOverflowTimerList = NULL;
  • pxCurrentTimerList:系统新创建并激活的定时器都会以超时时间升序的方式插入到pxCurrentTimerList 列表中。系统在定时器任务中扫描 pxCurrentTimerList 中的第一个定时器,看是否已超时,若已经超时了则调用软件定时器回调函数。否则将定时器任务挂起,因为定时时间是升序插入软件定时器列表的,列表中第一个定时器的定时时间都还没到的话,那后面的定时器定时时间自然没到。

  • pxOverflowTimerList 列表是在软件定时器溢出的时候使用,作用与 pxCurrentTimerList一致。

软件定时器可以采用消息队列进行通讯,利用“定时器命令队列”向软件定时器任务发送一些命令,任务在接收到命令就会去处理命令对应的程序,比如启动定时器,停止定时器等。

假如定时器任务处于阻塞状态,我们又需要马上再添加一个软件定时器的话,就是采用这种消息队列命令的方式进行添加,才能唤醒处于等待状态的定时器任务,并且在任务中将新添加的软件定时器添加到软件定时器列表中,所以,在定时器启动函数中,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,若大于则表示已经超时,定时器任务将会调用对应定时器的回调函数,否则将软件定时器任务挂起,直至下一个要唤醒的软件定时器时间到来或者接收到命令消息。执行完对应的回调函数后,会将其从软件定时器列表中删除,如果这个定时器是周期性的,则系统会根据Timer下一次唤醒时间重新将定时器添加到软件定时器列表中,也是根据时间来进行升序排序。

使用软件定时器需要注意:

在这里插入图片描述

软件定时器控制模块

typedef struct tmrTimerControl
{
    
    
	const char				*pcTimerName;		//软件定时器名字,用于调试的,一般通过其句柄来使用
	ListItem_t				xTimerListItem;		//软件定时器列表项,用于插入定时器列表
	TickType_t				xTimerPeriodInTicks;//软件定时器的周期
	UBaseType_t				uxAutoReload;		//pdFALSE为单次,pdTRUE为周期
	void 					*pvTimerID;			//软件定时器ID,在回调中根据ID来执行任务
	TimerCallbackFunction_t	pxCallbackFunction;	//回调函数
	#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; //标记定时器使用的内存,删除时判断是否需要释放内存。
	#endif
} xTIMER;

函数接口

软件定时器的很多 API 函数通过一个名字叫“定时器命令队列”的队列来给定时器守护任务发送命令。该定时器命令队列由 RTOS 内核提供,且应用程序不能够直接访问,其消息队列的长度由宏 configTIMER_QUEUE_LENGTH 定义。

xTimerCreate()

创建软件定时器函数,软件定时器在创建成功后是处于休眠状态的,可以使用 xTimerStart()、xTimerReset()、 xTimerStartFromISR() 、 xTimerResetFromISR() 、 xTimerChangePeriod() 和 xTimerChangePeriodFromISR()这些函数将其状态转换为活跃态。

#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

	TimerHandle_t xTimerCreate(	const char * const pcTimerName,			//软件定时器名字
								const TickType_t xTimerPeriodInTicks,	//软件定时器周期
								const UBaseType_t uxAutoReload,			//设置周期or单次模式
								void * const pvTimerID,					//软件定时器ID
								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 )
			{
    
    
				/* 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 */
  • xTimerPeriodInTicks:软件定时器的周期,单位为系统节拍周期(即 tick)。使用pdMS_TO_TICKS()可以把时间单位从 ms 转换为系统节拍周期。如果软件定时器的周期为100个 tick,那么只需要简单的设置 xTimerPeriod 的值为 100 即可。如果软件定时器的周期为 500ms,那么 xTimerPeriod 应设置为 pdMS_TO_TICKS(500)。宏 pdMS_TO_TICKS()只有当 configTICK_RATE_HZ 配置成小于或者等于 1000HZ 时才可以使用。

prvInitialiseNewTimer()

初始化一个软件定时器。

static void prvInitialiseNewTimer(	const char * const pcTimerName,			/*lint !e971 Unqualified char types are allowed for strings and single characters only. */
									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 ) );
		traceTIMER_CREATE( pxNewTimer );
	}
}

在 prvCheckForValidListAndQueue()函数中系统将初始化软件定时器列表与创建软件定时器消息队列,也叫“定时器命令队列”,因为在使用软件定时器的时候,用户是无法直接控制软件定时器的,必须通过“定时器命令队列”向软件定时器发送一个命令,软件定时器任务被唤醒就去执行对应的命令操作。其中调用了xTimerQueue = xQueueCreate( ( UBaseType_t ) configTIMER_QUEUE_LENGTH, sizeof( DaemonTaskMessage_t ) );

xTimerStart()

软件定时器启动函数,上面只是创建,处于休眠状态,需要使用这个函数来使其开始工作。系统开始运行时,会自动创建一个任务prvTimerTask,如果没有运行中的定时器,则会进入等待阻塞的状态,启动函数就是通过“定时器命令队列”向定时器任务发送一个启动命令,定时器任务获得命令就解除阻塞,然后执行启动软件定时器命令。

#define xTimerStart( xTimer, xTicksToWait ) xTimerGenericCommand( ( xTimer ),	//定时器句柄
											tmrCOMMAND_START, 					///启动命令
											( xTaskGetTickCount() ), 			//获取当前系统时间
											NULL, 								//中断发送命令才有用,其他时NULL
											( xTicksToWait ) )					//超时阻塞时间

xTicksToWait:用户指定超时阻塞时间,单位为系统节拍周期(即 tick)。调用xTimerStart()的任务将被锁定在阻塞态,在软件定时器把启动的命令成功发送到定时器命令队列之前。如果在 FreeRTOS 调度器开启之前调用 xTimerStart(),形参将不起作用。命令有如下:

#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 )

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 )
	{
    
    
		//要发送的命令信息,包含命令、命令的数值、处理定时器的句柄
		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
			{
    
    
                // 如果调度器还未运行,发送就行了,不需要阻塞,tmrNO_DELAY=0
				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;
}

xTimerStartFromISR()

在中断中启动软件定时器。

在这里插入图片描述

在这里插入图片描述

也就是说其使用的代码如下:

if ( xTimerStartFromISR( xBacklightTimer, &xHigherPriorityTaskWoken ) != pdPASS ) {
    
     
        /* 软件定时器开启命令没有成功执行 */ 
} 

        /*...*/
if ( xHigherPriorityTaskWoken != pdFALSE ) {
    
     
        /* 执行上下文切换 */ 
}

xTimerStop()

xTimerStop() 用于停止一个已经启动的软件定时器,该函数的实现也是通过“定时器 命令队列”发送一个停止命令给软件定时器任务,从而唤醒软件定时器任务去将定时器停止。

在这里插入图片描述

xTimerStopFromISR()

中断版本的停止。BaseType_t xTimerStopFromISR(TimerHandle_t xTimer,BaseType_t *pxHigherPriorityTaskWoken);
在这里插入图片描述

xTimerDelete()

在这里插入图片描述

从软件定时器删除函数 xTimerDelete()的原型可以看出,删除一个软件定时器也是在软件定时器任务中删除,调用 xTimerDelete()将删除软件定时器的命令发送给软件定时器任务,软件定时器任务在接收到删除的命令之后就进行删除操作。

软件定时器任务创建以及执行原理

软件定时器任务是在系统开始调度(vTaskStartScheduler()函数)的时候就被创建的:

#if ( configUSE_TIMERS == 1 )
{
    
    
    if( xReturn == pdPASS )
    {
    
    
        xReturn = xTimerCreateTimerTask();
    }
    else
    {
    
    
        mtCOVERAGE_TEST_MARKER();
    }
}
#endif /* configUSE_TIMERS */

调用了函数xTimerCreateTimerTask,这个函数中使用了任务创建函数:

xReturn = xTaskCreate(	prvTimerTask,
                        configTIMER_SERVICE_TASK_NAME,
                        configTIMER_TASK_STACK_DEPTH,
                        NULL,
                        ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
                        &xTimerTaskHandle );

在仔细看看其中的任务函数prvTimerTask

static void prvTimerTask( void *pvParameters )
{
    
    
TickType_t xNextExpireTime;
BaseType_t xListWasEmpty;

	/* Just to avoid compiler warnings. */
	( void ) pvParameters;

	for( ;; )
	{
    
    
		/*  获取下一个要到期的软件定时器的时间 */
        //只需获取软件定时器列表中的第一个定时器到期时间就是下一个要到期的时间。
		xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );

		/* 处理定时器或者将任务阻塞到下一个到期的软件定时器时间 */
		prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );

		/*读取“定时器命令队列”,处理相应命令*/
		prvProcessReceivedCommands();
	}
}
  • prvProcessTimerOrBlockTask : 处理定时器或者将任务阻塞到下一个到期的软件定时器时间,因 为系统时间节拍随着系统的运行可能会溢出,那么就需要处理溢出的情况,如果没有溢出,那么就等待下一个定时器到期时间的到来。该函数每次调用都会记录节拍值, 下一次调用,通过比较相邻两次调用的值判断节拍计数器是否溢出过。当节拍计数器溢出,需要处理掉当前定时器列表上的定时器(因为这条定时器列表上的定时器都已经溢出了),然后切换定时器列表。
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime, BaseType_t xListWasEmpty )
{
    
    
TickType_t xTimeNow;
BaseType_t xTimerListsWereSwitched;

	vTaskSuspendAll();
	{
    
    
		/* 获取当前系统时间节拍并判断系统节拍计数是否溢出 , 如果是,那么就处理当前列表上的定时器,并切换定时器列表*/
		xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
        // 系统节拍计数器没有溢出
		if( xTimerListsWereSwitched == pdFALSE )
		{
    
    
			/* 判断是否有定时器是否到期,定时器列表非空并且定时器的时间已比当前时间小,说明定时器到期了 */
			if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) )
			{
    
    	
                //恢复调度器
				( void ) xTaskResumeAll();
                //执行相应定时器的回调函数
                //对于需要自动重载的定时器, 更新下一次溢出时间, 插回列表
				prvProcessExpiredTimer( xNextExpireTime, xTimeNow );
			}
			else
			{
    
    
				/* 当前定时器列表中没有定时器*/
				if( xListWasEmpty != pdFALSE )
				{
    
    
					/* 发生这种情况的可能是系统节拍计数器溢出了,定时器被添加到溢出列表中,所以判断定时器溢出列表上是否有定时器*/
					xListWasEmpty = listLIST_IS_EMPTY( pxOverflowTimerList );
				}
				// 定时器定时时间还没到,将当前任务挂起,
                // 直到定时器到期才唤醒或者收到命令的时候唤醒
                // 阻塞时间为下一个定时器到期时间节拍减去当前系统时间节拍
				vQueueWaitForMessageRestricted( xTimerQueue, ( xNextExpireTime - xTimeNow ), xListWasEmpty );
				//  恢复调度器
				if( xTaskResumeAll() == pdFALSE )
				{
    
    
					// 任务切换
					portYIELD_WITHIN_API();
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
		else
		{
    
    
			( void ) xTaskResumeAll();
		}
	}
}

获取当前系统时间节拍并判断系统节拍计数是否溢出,如果已经溢出则处理当前表上的定时器,并切换定时器列表,函数为prvSampleTimeNow:

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;
}

接下来是读取定时器队列的处理函数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 ) );
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}

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

			/* 判断节拍计数器是否溢出过,如果有就处理并切换定时器列表 */
            //  因为下面的操作可能有新定时器项插入确保定时器列表对应
			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 );
						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 :
					/* 如果命令是停止定时器,那就将定时器移除, 在开始的时候已经从定时器列表移除,在此不需要操作*/
					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 );
					}
					#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
					break;

				default	:
					/* Don't expect to get here. */
					break;
			}
		}
	}
}

软件定时器实验

创建一个单次任务以及一个周期任务:

在这里插入图片描述

TimerHandle_t Timer_Handle1 = NULL;
TimerHandle_t Timer_Handle2 = NULL;

static void Timer_Handle_1_callback(void)
{
    
    
		TickType_t tick_num1;
		
		static uint32_t TmrCb_Count1 = 0;
		
		TmrCb_Count1++;
	
		tick_num1 = xTaskGetTickCount();
	
		printf("Timer1_callback go %d \r\n", TmrCb_Count1); 
		printf("time is =%d\r\n", tick_num1);
}
static void Timer_Handle_2_callback(void)
{
    
    
		TickType_t tick_num2;
		
		static uint32_t TmrCb_Count2 = 0;
		
		TmrCb_Count2++;
	
		tick_num2 = xTaskGetTickCount();
	
		printf("Timer2_callback go %d \r\n", TmrCb_Count2); 
		printf("time is =%d \r\n", tick_num2);
}

void MX_FREERTOS_Init(void) {
    
    
Timer_Handle1 = xTimerCreate((const char*)"Timer1",
                                                            (TickType_t)1000,
                                                            (UBaseType_t)pdTRUE,
                                                            (void*)1,							//ID		
                                                            				(TimerCallbackFunction_t)Timer_Handle_1_callback);
if(Timer_Handle1!=NULL)
{
    
    
        xTimerStart(Timer_Handle1,0);
}

Timer_Handle2 = xTimerCreate((const char*)"Timer2",
                                                            (TickType_t)10000,
                                                            (UBaseType_t)pdFALSE,
                                                            (void*)2,								//ID
                                                            (TimerCallbackFunction_t)Timer_Handle_2_callback);
if(Timer_Handle2!=NULL)
{
    
    
        xTimerStart(Timer_Handle2,0);
}
}

如果使用了软件定时器,则其他定义的线程无法执行,不知道为什么,暂时无解(优先级设置为相同或者比软件定时器高都不行)。

0x02 任务通知

任务通知即每个任务都有一个32位的通知值,在大多数情况下,任务通知可以替代二值信号量、计数量、事件组、长度为1的队列。使用任务通知比通过信号量等 ICP 通信方式解除阻塞的任务要快 45%,并且更加省 RAM 内存空间(使用 GCC 编译器,-o2 优化级别),任务通知的使用无需创建队列。若想要实现任务通知,则需要在FreeRTOSConfig.h中将宏定义configUSE_TASK_NOTIFICATIONS设置为1。

RTOS发送通知给任务具有以下几种方式:

在这里插入图片描述

但是他也是具有以下限制:

  • 只能有一个任务接收通知消息,因为必须指定接收通知的任务。
  • 只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态。

运作机制

任务通知的数据结构已包含在任务控制块中,只要任务存在,任务通知数据结构就已经创建完毕,可以直接使用。任务通知可以在任务中向指定任务发送通知,也可以在中断中向指定任务发送通知,FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue就是这个通知值。只有在任务中可以 等待通知,不允许在中断中等待通知。如果等待任务没有接收到消息,任务会根据用户指定的阻塞超时时间进入阻塞状态,获得通知后则会从阻塞态中解除。

数据结构

typedef struct tskTaskControlBlock
{
    
    
    ...
    #if( configUSE_TASK_NOTIFICATIONS == 1 )
        // 任务通知的值,可以保存一个 32 位整数或指针值
		volatile uint32_t ulNotifiedValue;
    	// 任务通知状态,用于标识任务是否在等待通知
		volatile uint8_t ucNotifyState;
	#endif
    ...
    
} tskTCB;

发送任务通知函数接口

xTaskGenericNotify()

发送任务通知函数,发送通知给对应的任务,其实就是修改这个任务的变量,更改其标志。在任务中发送通知的 API

函 数 , 如 xTaskNotifyGive() 、 xTaskNotify() 、 xTaskNotifyAndQuery() ,都 是 以xTaskGenericNotify()为原型的,只不过指定的发送方式不同而已。

#if( configUSE_TASK_NOTIFICATIONS == 1 )

BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify,  //被通知的任务句柄,指定通知的任务
                              uint32_t ulValue, 			//发送的通知值
                         	 eNotifyAction eAction, 		//枚举类型,指明更新通知值的方式
                              uint32_t *pulPreviousNotificationValue ) // 任务原本的通知值返回
{
    
    
    TCB_t * pxTCB;
    BaseType_t xReturn = pdPASS;
    uint8_t ucOriginalNotifyState;

    configASSERT( xTaskToNotify );
    pxTCB = ( TCB_t * ) xTaskToNotify;

    taskENTER_CRITICAL();
    {
    
    
        if( pulPreviousNotificationValue != NULL )
        {
    
    
            // 回传未被更新的任务通知值
            *pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
        }
		// 获取任务通知的状态,看看任务是否在等待通知,方便在发送通知后恢复任务
        ucOriginalNotifyState = pxTCB->ucNotifyState;
		// 不管状态是怎么样的,反正现在发送通知,任务就收到任务通知
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
		//  指定更新任务通知的方式
        switch( eAction )
        {
    
    
            // 通知值按位或上 ulValue,使用这种方法可以在某些场景下代替事件组,执行速度更快
            case eSetBits	:
                pxTCB->ulNotifiedValue |= ulValue;
                break;
			//  被通知任务的通知值增加 1,这种发送通知方式,参数 ulValue 未使用
            case eIncrement	:
                ( pxTCB->ulNotifiedValue )++;
                break;
			// 将被通知任务的通知值设置为 ulValue。无论任务是否还有通知,都覆盖当前任务通知值。
            // 可以替代xQueueoverwrite()函数,执行速度更快。
            case eSetValueWithOverwrite	:
                pxTCB->ulNotifiedValue = ulValue;
                break;
			// 如果被通知任务当前没有通知,则被通知任务的通知值设置为 ulValue;
            // 在某些场景下替代长度为 1 的 xQueuesend(),但速度更快。
            case eSetValueWithoutOverwrite :
                if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
                {
    
    
                    pxTCB->ulNotifiedValue = ulValue;
                }
                else
                {
    
    
                    /* 如果被通知任务还没取走上一个通知,本次发送通知,任务又接收到了一个通知,则这次通知值丢弃,
                    在这种情况下,函数调用失败并返回 pdFALSE。*/
                    xReturn = pdFAIL;
                }
                break;

            case eNoAction:
                /*  发送通知但不更新通知值,这意味着参数 ulValue 未使用。 */
                break;
        }

        traceTASK_NOTIFY();

        /* 如果被通知任务由于等待任务通知而挂起 */
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
    
    
            //  唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中
            ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
            prvAddTaskToReadyList( pxTCB );

            
            configASSERT( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) == NULL );
            
			/* 刚刚唤醒的任务优先级比当前任务高 */
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
    
    
                /* 任务切换 */
                taskYIELD_IF_USING_PREEMPTION();
            }
            else
            {
    
    
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
    
    
            mtCOVERAGE_TEST_MARKER();
        }
    }
    taskEXIT_CRITICAL();

    return xReturn;
}

#endif /* configUSE_TASK_NOTIFICATIONS */

xTaskNotifyGive()

#define xTaskNotifyGive( xTaskToNotify ) xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )

即向一个任务发送通知,并将对方的任务通知值加 1。该函数可以作为二值信号量和计数信号量的一种轻量型的实现,速度更快,在这种情况下对象任务在等待任务通知的时候应该是使用函数 ulTaskNotifyTake() 而不是 xTaskNotifyWait() 。该函数不能在中断中使用,需要带有中断保护的函数vTaskNotifyGiveFromISR()来代替。

static void prvTask1( void *pvParameters )
{
    for ( ;; ) {
        /* 向 prvTask2()发送一个任务通知,让其退出阻塞状态 */
        xTaskNotifyGive( xTask2 );

        /* 阻塞在 prvTask2()的任务通知上
        如果没有收到通知,则一直等待*/
        ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
    }
}

static void prvTask2( void *pvParameters )
{
    for ( ;; ) {
        /* 阻塞在 prvTask1()的任务通知上
        如果没有收到通知,则一直等待*/
        ulTaskNotifyTake( pdTRUE, portMAX_DELAY );

        /* 向 prvTask1()发送一个任务通知,让其退出阻塞状态 */
        xTaskNotifyGive( xTask1 );
	}
}

vTaskNotifyGiveFromISR()

vTaskNotifyGiveFromISR()是 vTaskNotifyGive()的中断保护版本。用于在中断中向指定任务发送任务通知,并更新对方的任务通知值(加 1 操作),在某些场景中可以替代信号量操作,因为这两个通知都是不带有通知值的

在这里插入图片描述

从上面的函数说明我们大概知道 vTaskNotifyGiveFromISR()函数作用,每次调用该函数都会增加任务的通知值,任务通过接收函数返回值是否大于零,判断是否获取到了通知,任务通知值初始化为 0,(如果与信号量做对比)则对应为信号量无效。当中断调用vTaskNotifyGiveFromISR()通知函数给任务的时候,任务的通知值增加,使其大于零,使其表示的通知值变为有效,任务获取有效的通知值将会被恢复。

#if( configUSE_TASK_NOTIFICATIONS == 1 )

void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken )
{
    
    
    TCB_t * pxTCB;
    uint8_t ucOriginalNotifyState;
    UBaseType_t uxSavedInterruptStatus;

    configASSERT( xTaskToNotify );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

    pxTCB = ( TCB_t * ) xTaskToNotify;
	// 进入中断
    uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    {
    
    	
        //保存任务通知的原始状态
        //看看任务是否在等待通知,方便在发送通知后恢复任务
        ucOriginalNotifyState = pxTCB->ucNotifyState;
        //不管状态是怎么样的,反正现在发送通知,任务就收到任务通知
        pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;

        /*  通知值自加,类似于信号量的释放*/
        ( pxTCB->ulNotifiedValue )++;

        traceTASK_NOTIFY_GIVE_FROM_ISR();

        /*  如果任务在阻塞等待通知 */
        if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
        {
    
    
            //如果任务调度器运行中
            if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
            {
    
    
                //唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中
                ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
                prvAddTaskToReadyList( pxTCB );
            }
            else
            {
    
    
                /* 调度器处于挂起状态,中断依然正常发生,但是不能直接操作就绪列表 
                将任务加入到就绪挂起列表,任务调度恢复后会移动到就绪列表*/
                vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
            }

            // 如果刚刚唤醒的任务优先级比当前任务高,则设置上下文切换标识,等退出函数后手动切换上下文,
            // 或者在系统节拍中断服务程序中自动切换上下文
            if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
            {
    
    
                /* 设置返回参数,表示需要任务切换,在退出中断前进行任务切换 */
                if( pxHigherPriorityTaskWoken != NULL )
                {
    
    
                    *pxHigherPriorityTaskWoken = pdTRUE;
                }
                else
                {
    
    
                    /* 设置自动切换标志*/
                    xYieldPending = pdTRUE;
                }
            }
            else
            {
    
    
                mtCOVERAGE_TEST_MARKER();
            }
        }
    }
    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
}

#endif /* configUSE_TASK_NOTIFICATIONS */

xTaskNotify()

FreeRTOS 每个任务都有一个 32 位的变量用于实现任务通知,在任务创建的时候初始化为 0。这个 32 位的通知值在任务控制块 TCB 里面定义,xTaskNotify()用于在任务中直接向另外一个任务发送一个事件,接收到该任务通知的任务有可能解锁

如果你想使用任务通知来实现二值信号量和计数信号量,那么应该使用更加简单的函数 xTaskNotifyGive() ,而不是使用 xTaskNotify(),xTaskNotify()函数在发送任务通知的时候会指定一个通知值,并且用户可以指定通知值发送的方式

该函数不能在中断里面使用,而是使用具体中断保护功能的版本函数xTaskNotifyFromISR()

#define xTaskNotify( xTaskToNotify, ulValue, eAction ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL )

在这里插入图片描述

eAction取值:

在这里插入图片描述
在这里插入图片描述

xTaskNotifyFromISR()

xTaskNotifyFromISR()是 xTaskNotify()的中断保护版本,真正起作用的函数是中断发送任务通知通用函数xTaskGenericNotifyFromISR(),而 xTaskNotifyFromISR()是一个宏定义,用于在中断中向指定的任务发送一个任务通知,该任务通知是带有通知值并且用户可以指定通知的发送方式,不返回上一个任务在的通知值。

#define xTaskNotifyFromISR( xTaskToNotify, ulValue, eAction, pxHigherPriorityTaskWoken ) xTaskGenericNotifyFromISR( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL, ( pxHigherPriorityTaskWoken ) )

在这里插入图片描述

xTaskGenericNotifyFromISR()

xTaskGenericNotifyFromISR() 是一个在中断中发送任务通知的通用函数,xTaskNotifyFromISR()、xTaskNotifyAndQueryFromISR()等函数都是以其为基础,采用宏定义的方式实现。

#if( configUSE_TASK_NOTIFICATIONS == 1 )

	BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify, 			// 指定接收通知的任务句柄
                                         uint32_t ulValue, 						// 用于更新接收任务通知值,具体如何更新由形参 eAction 决定
                                         eNotifyAction eAction, 				// 任务通知值更新方式。
                                         uint32_t *pulPreviousNotificationValue, // 用于保存任务上一个通知值
                                         BaseType_t *pxHigherPriorityTaskWoken )	//*pxHigherPriorityTaskWoken 在使用之前必须先初始化为 pdFALSE,其具体作用同上
	{
    
    
        TCB_t * pxTCB;
        uint8_t ucOriginalNotifyState;
        BaseType_t xReturn = pdPASS;
        UBaseType_t uxSavedInterruptStatus;

		configASSERT( xTaskToNotify );
        
		portASSERT_IF_INTERRUPT_PRIORITY_INVALID();

		pxTCB = ( TCB_t * ) xTaskToNotify;
		// 进入中断临界区
		uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
		{
    
    
			if( pulPreviousNotificationValue != NULL )
			{
    
    
                // 回传未被更新的任务通知值
				*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
			}
			
            // 保存任务通知的原始状态,
            // 看看任务是否在等待通知,方便在发送通知后恢复任务
			ucOriginalNotifyState = pxTCB->ucNotifyState;
            //  不管状态是怎么样的,反正现在发送通知,任务就收到任务通知
			pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
			//  指定更新任务通知的方式
			switch( eAction )
			{
    
    
                // 通知值按位或上 ulValue。
                // 使用这种方法可以某些场景下代替事件组,但执行速度更快。
                case eSetBits	:
					pxTCB->ulNotifiedValue |= ulValue;
					break;
				// 被通知任务的通知值增加 1,这种发送通知方式,参数 ulValue 未使用
                // 在某些场景下可以代替信号量,执行速度更快
				case eIncrement	:
					( pxTCB->ulNotifiedValue )++;
					break;
				// 将被通知任务的通知值设置为 ulValue。无论任务是否还有通知,
                // 都覆盖当前任务通知值。使用这种方法,
                // 可以在某些场景下代替 xQueueoverwrite()函数,但执行速度更快。
				case eSetValueWithOverwrite	:
					pxTCB->ulNotifiedValue = ulValue;
					break;
				// 采用不覆盖发送任务通知的方式
				case eSetValueWithoutOverwrite :
                    // 如果被通知任务当前没有通知,则被通知任务的通知值设置为 ulValue;
                    // 在某些场景下替代长度为 1 的 xQueuesend(),但速度更快。
					if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
					{
    
    
						pxTCB->ulNotifiedValue = ulValue;
					}
					else
					{
    
    
						/* 如果被通知任务还没取走上一个通知,本次发送通知,
                        任务又接收到了一个通知,则这次通知值丢弃,
                        在这种情况下,函数调用失败并返回 pdFALSE。
                        */
						xReturn = pdFAIL;
					}
					break;

				case eNoAction :
					/* 退出 */
					break;
			}

			traceTASK_NOTIFY_FROM_ISR();

			/*  如果任务在阻塞等待通知 */
			if( ucOriginalNotifyState == taskWAITING_NOTIFICATION )
			{
    
    
                // 如果任务调度器运行中,表示可用操作就绪级列表
				if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
				{
    
    
                    // 唤醒任务,将任务从阻塞列表中移除,添加到就绪列表中
					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
					prvAddTaskToReadyList( pxTCB );
				}
				else
				{
    
    
					/*  调度器处于挂起状态,中断依然正常发生,但是不能直接操作就绪列表,
                    将任务加入到就绪挂起列表,任务调度恢复后会移动到就绪列表*/
					vListInsertEnd( &( xPendingReadyList ), &( pxTCB->xEventListItem ) );
				}
				
                // 如果刚刚唤醒的任务优先级比当前任务高,
                // 则设置上下文切换标识,等退出函数后手动切换上下文,
                // 或者自动切换上下文
				if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
				{
    
    
					if( pxHigherPriorityTaskWoken != NULL )
					{
    
    
                        // 设置返回参数,表示需要任务切换,在退出中断前进行任务切换
						*pxHigherPriorityTaskWoken = pdTRUE;
					}
					else
					{
    
    
						/* 设置自动切换标志,等高优先级任务释放 CPU 使用权 */
						xYieldPending = pdTRUE;
					}
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
		}
        // 离开中断临界区
		portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );

		return xReturn;
	}

#endif /* configUSE_TASK_NOTIFICATIONS */

xTaskNotifyAndQuery()

xTaskNotifyAndQuery()与 xTaskNotify()很像,都是调用通用的任务通知发送函数xTaskGenericNotify() 来 实 现 通 知 的 发 送 , 不同 的是多了一个附加的参数pulPreviousNotifyValue 用于回传接收任务的上一个通知值.

#define xTaskNotifyAndQuery( xTaskToNotify, \
							ulValue, 		\
                            eAction, 		\
                            pulPreviousNotifyValue ) \
        xTaskGenericNotify( ( xTaskToNotify ), \
							( ulValue ), \
							( eAction ), \
							( pulPreviousNotifyValue ) )

在这里插入图片描述

xTaskNotifyAndQueryFromISR()

xTaskNotifyAndQueryFromISR()是 xTaskNotifyAndQuery ()的中断版本,用于向指定的任务发送一个任务通知,并返回对象任务的上一个通知值,该函数也是一个宏定义,真正实现发送通知的是 xTaskGenericNotifyFromISR()

在这里插入图片描述

最后的形参xHigherPriorityTaskWoken可以这样使用:

xTaskNotifyAndQueryFromISR( xTask1Handle, 
                            ( 1UL << 8UL ), 
                            eSetBits, 
                            &ulPreviousValue, 
                            &xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );

接收任务通知函数接口

ulTaskNotifyTake() 和xTaskNotifyWait ()为两个接收任务通知函数的API,前者是为代替二值信号量和计数信号量而专门设计的,它和发送通知API 函数 xTaskNotifyGive()、vTaskNotifyGiveFromISR()配合使用;后者是全功能版的等待通知,可以根据不同的参数实现轻量级二值信号量、计数信号量、事件组和长度为 1 的队列。

所有的获取任务通知 API 函数都带有指定阻塞超时时间参数,当任务因为等待通知而进入阻塞时,用来指定任务的阻塞时间,这些超时机制与 FreeRTOS 的消息队列、信号量、事件等的超时机制一致。

ulTaskNotifyTake()

ulTaskNotifyTake()作为二值信号量和计数信号量的一种轻量级实现,速度更快。对于这个函数,任务通知值为 0,对应信号量无效,如果任务设置了阻塞等待,任务被阻塞挂起。当其他任务或中断发送了通知值使其不为 0 后,通知变为有效,等待通知的任务将获取到通知,并且在退出时候根据用户传递的第一个参数xClearCountOnExit选择清零通知值或者执行减一操作。

xTaskNotifyTake()在退出的时候处理任务的通知值的时候有两种方法,一种是在函数退出时将通知值清零,这种方法适用于实现二值信号量;另外一种是在函数退出时将通知值减 1,这种方法适用于实现计数信号量。

当一个任务使用其自身的任务通知值作为二值信号量或者计数信号量时,其他任务应该使用函数 xTaskNotifyGive()或者 xTaskNotify( ( xTaskToNotify ), ( 0 ), eIncrement )来向其发送信号量。

在这里插入图片描述

#if( configUSE_TASK_NOTIFICATIONS == 1 )

	uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, 
                              TickType_t xTicksToWait )
	{
    
    
		uint32_t ulReturn;

		taskENTER_CRITICAL();			// 进入中断临界区
		{
    
    
			// 如果通知值为 0 ,阻塞任务
            // 默认初始化通知值为 0, 说明没有未读通知
			if( pxCurrentTCB->ulNotifiedValue == 0UL )
			{
    
    
				/* 标记任务状态 : 等待消息通知 */
				pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
				// 用户指定超时时间了,那就进入等待状态
				if( xTicksToWait > ( TickType_t ) 0 )
				{
    
    
                    // 根据用户指定超时时间将任务添加到延时列表
					prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
					traceTASK_NOTIFY_TAKE_BLOCK();

					// 切换任务
					portYIELD_WITHIN_API();
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
		}
		taskEXIT_CRITICAL();
		//  到这里说明其它任务或中断向这个任务发送了通知,或者任务阻塞超时,现在继续处理
		taskENTER_CRITICAL();
		{
    
    
            // 获取任务通知值
			traceTASK_NOTIFY_TAKE();
			ulReturn = pxCurrentTCB->ulNotifiedValue;
			// 看看任务通知是否有效,有效则返回
			if( ulReturn != 0UL )
			{
    
    
                // 是否需要清除通知
				if( xClearCountOnExit != pdFALSE )
				{
    
    
					pxCurrentTCB->ulNotifiedValue = 0UL;
				}
				else
				{
    
    
                    //不清除,就减一
					pxCurrentTCB->ulNotifiedValue = ulReturn - ( uint32_t ) 1;
				}
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
			// 恢复任务通知状态变量
			pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
		}
		taskEXIT_CRITICAL();

		return ulReturn;
	}

#endif /* configUSE_TASK_NOTIFICATIONS */

与获取二值信号量和获取计数信号量的函数相比,ulTaskNotifyTake()函数少了很多调用子函数开销、少了很多判断、少了事件列表处理、少了队列上锁与解锁处理等等,因此ulTaskNotifyTake()函数相对效率很高。

xTaskNotifyWait()

xTaskNotifyWait()函数用于实现全功能版的等待任务通知,根据用户指定的参数的不同,可以灵活的用于实现轻量级的消息队列队列、二值信号量、计数信号量和事件组功能,并带有超时等待。

在这里插入图片描述

#if( configUSE_TASK_NOTIFICATIONS == 1 )

	BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry, 
                               uint32_t ulBitsToClearOnExit, 
                               uint32_t *pulNotificationValue, 
                               TickType_t xTicksToWait )
	{
    
    
		BaseType_t xReturn;
		// 进入临界段
		taskENTER_CRITICAL();
		{
    
    
			/* 只有任务当前没有收到任务通知,才会将任务阻塞 */
			if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED )
			{
    
    
				/* 使用任务通知值之前,根据用户指定参数 ulBitsToClearOnEntryClear
				将通知值的某些或全部位清零 */
				pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;

				/* 设置任务状态标识:等待通知 */
				pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
				
                // 挂起任务等待通知或者进入阻塞态
				if( xTicksToWait > ( TickType_t ) 0 )
				{
    
    
                    //  根据用户指定超时时间将任务添加到延时列表
					prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
					traceTASK_NOTIFY_WAIT_BLOCK();

					/* 任务切换*/
					portYIELD_WITHIN_API();
				}
				else
				{
    
    
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
		}
		taskEXIT_CRITICAL();
		// 程序能执行到这里说明其它任务或中断向这个任务发送了通知或者任务阻塞超时,现在继续处理
		taskENTER_CRITICAL();
		{
    
    
			traceTASK_NOTIFY_WAIT();

			if( pulNotificationValue != NULL )
			{
    
    
				/* 返回当前通知值,通过指针参数传递*/
				*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;
			}

			/*  判断是否是因为任务阻塞超时,因为如果有
			任务发送了通知的话,任务通知状态会被改变 */
			if( pxCurrentTCB->ucNotifyState != taskNOTIFICATION_RECEIVED )
			{
    
    
				/* 没有收到任务通知,是阻塞超时 */
				xReturn = pdFALSE;
			}
			else
			{
    
    
				/* 收到任务值,先将参数 ulBitsToClearOnExit 取反后与通知值位做按位与运算
				在退出函数前,将通知值的某些或者全部位清零. */
				pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;
				xReturn = pdTRUE;
			}
			// 重新设置任务通知状态
			pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
		}
		taskEXIT_CRITICAL();

		return xReturn;
	}

#endif /* configUSE_TASK_NOTIFICATIONS */

可以通过任务通知值传递到形参中,通过其值得到各个位值进而使用不同的处理函数。

猜你喜欢

转载自blog.csdn.net/Alkaid2000/article/details/131109651
今日推荐