在freeRTOS中,除了信号量、队列、互斥量这些通信方式,其实还提供了比较轻量的两种方式,这两种为事件和通知。为何说他们两种通信方式是轻量化的方式呢?这两种方式如何实现的?

事件

学习事件之前,先思考几个问题,什么是事件?事件和消息队列的差异在哪?事件在任务之间同步时可以进行阻塞吗?如果可以阻塞,那么阻塞是如何实现的?

定义

先来回答第一个问题,事件是任务之间进行通信的一种机制,主要用于任务之间的同步,相比对信号量的同步操作,事件具有以下优势。

  • 占用的内存空间很小,信号量其实是依附在队列结构体中实现的,但是事件是一个单独的结构体,由uxEventBits和xTasksWaitingForBits组成;

    typedef struct xEventGroupDefinition
    {
    	EventBits_t uxEventBits;
    	List_t xTasksWaitingForBits;		/*< List of tasks waiting for a bit to be set. */
    } EventGroup_t;
    
    
  • 事件可以实现一对多,多对多的同步(逻辑与和逻辑或操作),而信号量一般用于一对一的同步;

  • 事件仅仅是用于同步,不能进行消息的传输,只与任务关链,事件相互独立;

  • 事件没有排队这一说,而信号量支持累计,支持事件等待超时机制;

uxEventBits

在这里插入图片描述

uxEventBits划分为两部分,其中0-23位为事件组的标志位,1代表事件发生,0代表事件没有发生;其余的高位定位于控制位,BIT24定义退出时是否清除事件位,BIT25定义事件unlock的原因,BIT26代表是否为逻辑与,也就是等待所有的事件发生,BIT31代表是否在使用中。

接口函数

通过接口函数,要搞清楚事件是如何实现逻辑与和逻辑或的。

在使用事件之前,与其他通信方式一样,需要先创建事件,创建事件的本质操作是向内存中申请struct xEventGroupDefinition大小的内存空间。申请成功之后,对uxEventBits和xTasksWaitingForBits成员变量进行初始化。

1. 置位函数
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet )
{
ListItem_t *pxListItem, *pxNext;
ListItem_t const *pxListEnd;
List_t *pxList;
EventBits_t uxBitsToClear = 0, uxBitsWaitedFor, uxControlBits;
EventGroup_t *pxEventBits = ( EventGroup_t * ) xEventGroup;
BaseType_t xMatchFound = pdFALSE;

    //获取事件组等待链表
	pxList = &( pxEventBits->xTasksWaitingForBits );
	pxListEnd = listGET_END_MARKER( pxList );   /* 获取等待链表的尾结点 */
	vTaskSuspendAll();
	{
        /* 获取等待链表的头结点 */
		pxListItem = listGET_HEAD_ENTRY( pxList );
		/* 设置事件标志位 */
		pxEventBits->uxEventBits |= uxBitsToSet;
		/* 检查该标志位设置后,是否有任务条件满足 */
		while( pxListItem != pxListEnd )
		{	/* 获取该结点等待的位 */
			pxNext = listGET_NEXT( pxListItem );
			uxBitsWaitedFor = listGET_LIST_ITEM_VALUE( pxListItem );
			xMatchFound = pdFALSE;
			/* 对uxBitsWaitedFor进行分割 */
			uxControlBits = uxBitsWaitedFor & eventEVENT_BITS_CONTROL_BYTES; /* 获取32位的高8位 */
			uxBitsWaitedFor &= ~eventEVENT_BITS_CONTROL_BYTES; 				 /*获取32位的低24位 */
			/* 逻辑或 */	
			if( ( uxControlBits & eventWAIT_FOR_ALL_BITS ) == ( EventBits_t ) 0 )
			{
				/* 有单个位满足要求 */
				if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) != ( EventBits_t ) 0 )
				{
					xMatchFound = pdTRUE;  /* 已经匹配到 */
				}
			}  /* 逻辑与,所有位都要满足要求 */
			else if( ( uxBitsWaitedFor & pxEventBits->uxEventBits ) == uxBitsWaitedFor )
			{
				/* All bits are set. */
				xMatchFound = pdTRUE;
			}
			else
			{
				/* Need all bits to be set, but not all the bits were set. */
			}
			if( xMatchFound != pdFALSE )	/* 已经匹配到 */
			{
				/* The bits match.  Should the bits be cleared on exit?  是否要清除*/
				if( ( uxControlBits & eventCLEAR_EVENTS_ON_EXIT_BIT ) != ( EventBits_t ) 0 )
				{
					uxBitsToClear |= uxBitsWaitedFor;
				}  /* 将满足事件条件的任务从等待链表移除,添加到就绪链表 */
				(void)xTaskRemoveFromUnorderedEventList( pxListItem, pxEventBits->uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET );
			}
			pxListItem = pxNext;  /* 下一个链表结点 */
		}
		pxEventBits->uxEventBits &= ~uxBitsToClear;  /* 在事件链表编译结束时,清除事件组置位 */
	}
	( void ) xTaskResumeAll();

	return pxEventBits->uxEventBits;
}

置位函数的流程大致为首先是将要置的位写入uxEventBits,写入后判断xEventGroup中等待链表上的任务有没有条件符合解除等待的任务,这个过程会遍历整个等待链表中的每一个结点,如果有任务,则将任务从等待链表移除,然后加入到延时链表中。

注意在这个过程中会设置xItemValue的数值,具体如下:

newValue = uxEventBits | eventUNBLOCKED_DUE_TO_BIT_SET | taskEVENT_LIST_ITEM_VALUE_IN_USE

对应的中断版本的函数为xEventGroupSetBitsFromISR,让守护任务完成事件组的置位;

2. 等待事件函数

入口参数:

  • xEventGroup : 事件句柄;
  • uxBitsToWaitFor : 需要等待哪些位是1;
  • xClearOnExit : 是否需要清除uxBitsToWaitFor所指的位;
  • xWaitForAllBits: 是等待uxBitsToWaitFor的所有位还是某一位;
  • xTicksToWait: 等待的超时时间;
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait )
{
EventGroup_t *pxEventBits = ( EventGroup_t * ) xEventGroup;
EventBits_t uxReturn, uxControlBits = 0;
BaseType_t xWaitConditionMet, xAlreadyYielded;
BaseType_t xTimeoutOccurred = pdFALSE;

	vTaskSuspendAll();
	{
		const EventBits_t uxCurrentEventBits = pxEventBits->uxEventBits;   /* 当前事件组的状态 */
		/* Check to see if the wait condition is already met or not. */
		xWaitConditionMet = prvTestWaitCondition( uxCurrentEventBits, uxBitsToWaitFor, xWaitForAllBits );
		if( xWaitConditionMet != pdFALSE )  /* 满足置位 */
		{
			uxReturn = uxCurrentEventBits; 		/* 需要用户在收到标志位后判断返回数值 */
			xTicksToWait = ( TickType_t ) 0;
			/* Clear the wait bits if requested to do so. 需要清除置位 */
			if( xClearOnExit != pdFALSE )
			{
				pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
			}
		} /* 不满足置位,且等待时间为0 */
		else if( xTicksToWait == ( TickType_t ) 0 )
		{
			uxReturn = uxCurrentEventBits;   /* 直接返回,同样需要用户判断返回数值 */
		}
		else  /* 不满足置位,且等待时间不为0 */
		{
			if( xClearOnExit != pdFALSE )			/* 需要清除置位 */
			{
				uxControlBits |= eventCLEAR_EVENTS_ON_EXIT_BIT;
			}
			if( xWaitForAllBits != pdFALSE )  /* 逻辑与 */
			{
				uxControlBits |= eventWAIT_FOR_ALL_BITS;
			}
            /* 添加到等待链表中  */
			vTaskPlaceOnUnorderedEventList( &( pxEventBits->xTasksWaitingForBits ), ( uxBitsToWaitFor | uxControlBits ), xTicksToWait );
			uxReturn = 0;
		}
	}
	xAlreadyYielded = xTaskResumeAll();		
	if( xTicksToWait != ( TickType_t ) 0 )  //阻塞的时间不为0
	{
		if( xAlreadyYielded == pdFALSE )
		{
			portYIELD_WITHIN_API();/* 可能会发生任务切换 */
		}
		uxReturn = uxTaskResetEventItemValue();  /* 阻塞延时已过,从新调度 */
		if( ( uxReturn & eventUNBLOCKED_DUE_TO_BIT_SET ) == ( EventBits_t ) 0 )
		{
			taskENTER_CRITICAL();  
			{
				/* The task timed out, just return the current event bit value. */
				uxReturn = pxEventBits->uxEventBits;
                /* 再次判断是否发生了调度 */
				if( prvTestWaitCondition( uxReturn, uxBitsToWaitFor, xWaitForAllBits ) != pdFALSE )
				{
					if( xClearOnExit != pdFALSE )  /* 清除标志位,并返回 */
					{
						pxEventBits->uxEventBits &= ~uxBitsToWaitFor;
					}
				}
			}
			taskEXIT_CRITICAL();
			xTimeoutOccurred = pdFALSE;
		}
		/* 返回事件所有标志 */
		uxReturn &= ~eventEVENT_BITS_CONTROL_BYTES;
	}
	return uxReturn;
}
  • 检查事件组中等待的事件是否满足

    static BaseType_t prvTestWaitCondition( const EventBits_t uxCurrentEventBits, const EventBits_t uxBitsToWaitFor, const BaseType_t xWaitForAllBits )
    {
    BaseType_t xWaitConditionMet = pdFALSE;
    	if( xWaitForAllBits == pdFALSE )  /* 逻辑或 */
    	{
    		if( ( uxCurrentEventBits & uxBitsToWaitFor ) != ( EventBits_t ) 0 )
    		{
    			xWaitConditionMet = pdTRUE;
    		}
    	}
    	else
    	{   /* 逻辑与 */
    		if( ( uxCurrentEventBits & uxBitsToWaitFor ) == uxBitsToWaitFor )
    		{
    			xWaitConditionMet = pdTRUE;
    		}
    	}
    	return xWaitConditionMet;
    }
    
  • vTaskPlaceOnUnorderedEventList函数首先会设置TCB->xEventListItem->xItemValue数值,然后将任务TCB->xEventListItem挂载到事件的等待链表的末尾,最后将任务添加到延时等待链表中。

    • xItemValue的数值

      uxBitsToWaitFor | uxControlBits | taskEVENT_LIST_ITEM_VALUE_IN_USE
      

      uxControlBits位会依据是否等待所有位和退出时是否清除,进行eventCLEAR_EVENTS_ON_EXIT_BIT/eventWAIT_FOR_ALL_BITS的与操作。

    • 设置xItemValue数值的目的在于告诉置位函数,我这个任务在等待什么事件,

  • 对应的中断版本为xEventGroupClearBitsFromISR,通过守护任务调用xEventGroupWaitBits函数;

通知

定义

任务的控制块中提供了一个32位的通知值,该数值可以代替二值信号量、计数信号量、事件组和长度为1的队列。但是任务通知存在必须指定接收通知的任务,只有等待通知的任务可以阻塞,发送通知的任务不会阻塞。

typedef struct tskTaskControlBlock
{
    /* 省略部分函数 */
	#if( configUSE_TASK_NOTIFICATIONS == 1 )
	volatile uint32_t ulNotifiedValue;
	volatile uint8_t ucNotifyState;  /* 用于比奥是任务是否在等待通知 */
	#endif
    /* 省略部分函数 */
}   
关键代码

任务通知主要围绕ulNotifiedValue展开,在发送通知方面包含7个函数,接收侧有2个函数。函数被划分为中断版本和任务版本。发送测除了通用的xTaskNotify函数,还设计了专门用于二值信号量和计数信号量的收发函数。
在这里插入图片描述

发送通知的函数
  • xTaskGenericNotify 通用

    BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue )
    	{
    	TCB_t * pxTCB;
    	BaseType_t xReturn = pdPASS;
    	uint8_t ucOriginalNotifyState;
    		pxTCB = ( TCB_t * ) xTaskToNotify;
    		taskENTER_CRITICAL();
    		{
    			if( pulPreviousNotificationValue != NULL )
    			{	/* 保存任务原本的通知数值 */
    				*pulPreviousNotificationValue = pxTCB->ulNotifiedValue;
    			}
    			ucOriginalNotifyState = pxTCB->ucNotifyState;  			/* 获取任务的通知状态 */
    			pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED; 		/* 更新状态为收到任务通知 */
    			switch( eAction )
    			{
    				case eSetBits	:  	/* 新值 | 旧值 可代替事件组 */
    					pxTCB->ulNotifiedValue |= ulValue;
    					break;
    				case eIncrement	:	
    					( pxTCB->ulNotifiedValue )++;	/* 通知数值++ 可代替信号量通信	*/
    					break;
    				case eSetValueWithOverwrite	:		/* 覆盖写入,可代替xQueueoverwrite*/
    					pxTCB->ulNotifiedValue = ulValue;
    					break;
    				case eSetValueWithoutOverwrite :
    					if( ucOriginalNotifyState != taskNOTIFICATION_RECEIVED )
    					{
    						pxTCB->ulNotifiedValue = ulValue;  /* 代替长度为1的队列 */
    					}
    					else
    					{	
    						/* The value could not be written to the task. 上一次通知的任务没有取走 */
    						xReturn = pdFAIL;
    					}
    					break;
    				case eNoAction:
    					/* The task is being notified without its notify value being
    					updated. */
    					break;
    			}
    			if( ucOriginalNotifyState == taskWAITING_NOTIFICATION ) /* 如果任务在等待通知 */
    			{	/* 将任务解除等待,加入就绪链表 */
    				( void ) uxListRemove( &( pxTCB->xStateListItem ) );
    				prvAddTaskToReadyList( pxTCB );
    				if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )  /* 高优先级进行一次切换 */
    				{
    					taskYIELD_IF_USING_PREEMPTION();
    				}
    			}
    		}
    		taskEXIT_CRITICAL();
    		return xReturn;
    	}
    
    • 入口参数xTaskToNotify代表被通知的任务句柄、ulValue代表通知的数值、eAction指明更新通知值的方式、pulPreviousNotificationValue任务原本的通知数值;
    • 发送任务通知就是根据eAction的数值对pxTCB->ulNotifiedValue做不同的操作,然后检查是否等待通知,如果在等待通知,就解除等待,加入就绪链表,进行调度;
  • xTaskNotifyGive 二值信号量和计数信号量的代替

    #define xTaskNotifyGive( xTaskToNotify ) xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )
    
  • vTaskNotifyGiveFromISR 任务通知中断版本,这个只是对ulNotifiedValue进行加加;

    void vTaskNotifyGiveFromISR( TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken )
    	{
    	TCB_t * pxTCB;
    	uint8_t ucOriginalNotifyState;
    	UBaseType_t uxSavedInterruptStatus;
    		portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
    		pxTCB = ( TCB_t * ) xTaskToNotify;
    		uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
    		{
    			ucOriginalNotifyState = pxTCB->ucNotifyState;
    			pxTCB->ucNotifyState = taskNOTIFICATION_RECEIVED;
    			( pxTCB->ulNotifiedValue )++;
    			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;
    					}
    				}
    			}
    		}
    		portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
    	}
    
    
    • pxHigherPriorityTaskWoken在使用之前要初始化为pdFALSE;
  • xTaskNotify 用于向指定任务发送一个通知数值,可以指定通知方式

    #define xTaskNotify( xTaskToNotify, ulValue, eAction ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL )
    
  • xTaskNotifyFromISR xTaskNotify的中断版本

    #define xTaskNotifyFromISR( xTaskToNotify, ulValue, eAction, pxHigherPriorityTaskWoken ) xTaskGenericNotifyFromISR( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL, ( pxHigherPriorityTaskWoken ) )
    
  • xTaskGenericNotifyFromISR 在中断中发送通知的函数;

    BaseType_t xTaskGenericNotifyFromISR( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue, BaseType_t *pxHigherPriorityTaskWoken )
    
  • xTaskNotifyAndQuery 带上次通知值

    #define xTaskNotifyAndQuery( xTaskToNotify, ulValue, eAction, pulPreviousNotifyValue ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), ( pulPreviousNotifyValue ) )
    
  • xTaskNotifyAndQueryFromISR 中断版本的带上一次通知值

    #define xTaskNotifyAndQueryFromISR( xTaskToNotify, ulValue, eAction, pulPreviousNotificationValue, pxHigherPriorityTaskWoken ) xTaskGenericNotifyFromISR( ( xTaskToNotify ), ( ulValue ), ( eAction ), ( pulPreviousNotificationValue ), ( pxHigherPriorityTaskWoken ) )
    
获取任务通知

获取任务通知函数有两个,分别是ulTaskNotifyTake和xTaskNotifyWait。ulTaskNotifyTake是代替信号量设计,与xTaskNotifyGive()、vTaskNotifyGiveFromISR()成对使用;xTaskNotifyWait是通用版本。

  • ulTaskNotifyTake

    uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait )
    	{
    	uint32_t ulReturn;
    		taskENTER_CRITICAL();
    		{
    			if( pxCurrentTCB->ulNotifiedValue == 0UL )  /* 通知数值为0,则阻塞任务 */
    			{
    				pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
    				if( xTicksToWait > ( TickType_t ) 0 )
    				{
    					prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
    					portYIELD_WITHIN_API();
    				}
    			}
    		}
    		taskEXIT_CRITICAL();
    		taskENTER_CRITICAL(); /*1.获取到了任务 2.阻塞超时 */
    		{
    			ulReturn = pxCurrentTCB->ulNotifiedValue;
    			if( ulReturn != 0UL )
    			{
    				if( xClearCountOnExit != pdFALSE )
    				{
    					pxCurrentTCB->ulNotifiedValue = 0UL;  /* 类似于二值信号量 */
    				}
    				else
    				{
    					pxCurrentTCB->ulNotifiedValue = ulReturn - 1;  /* 类似于计数信号量 */
    				}
    			}
    			pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
    		}
    		taskEXIT_CRITICAL();
    		return ulReturn;
    	}
    
  • xTaskNotifyWait 获取任务通知

    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 )
    			{
    				pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnEntry;
    				pxCurrentTCB->ucNotifyState = taskWAITING_NOTIFICATION;
    				if( xTicksToWait > ( TickType_t ) 0 )
    				{
    					prvAddCurrentTaskToDelayedList( xTicksToWait, pdTRUE );
    					portYIELD_WITHIN_API();  /* 调度切换 */
    				}
    			}
    		}
    		taskEXIT_CRITICAL();
    		/* 再一次回到该函数 */
    		taskENTER_CRITICAL();
    		{
    			if( pulNotificationValue != NULL )
    			{
    				*pulNotificationValue = pxCurrentTCB->ulNotifiedValue;  /* 返回当前任务通知值 */
    			}
    			if( pxCurrentTCB->ucNotifyState == taskWAITING_NOTIFICATION )
    			{
    				xReturn = pdFALSE;		/* 阻塞超时 */
    			}
    			else
    			{
    				pxCurrentTCB->ulNotifiedValue &= ~ulBitsToClearOnExit;  /* 将通知值的某些位清零 */
    				xReturn = pdTRUE;
    			}
    			pxCurrentTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
    		}
    		taskEXIT_CRITICAL();
    		return xReturn;
    	}
    

总结

现在来回答下开始的问题,为什么说他们两种是轻量化的方式,这个轻量主要指的是内存空间的占用,事件通过两个成员变量实现,而通知更加简单,通过任务的TCB中的成员变量实现,相比于消息队列中申请的内存空间,是轻量化的。再来看如何实现的,以及在使用的过程中有什么注意的地方。实现的话,事件通过一个24位的事件组来完成,可以用于一对多或多对多,一个任务可以等待多个事件,多个任务可以同时向这个事件写入数值。通知的话使用一个变量实现,可以完成信号量和互斥量的操作,对这个变量进行加加就是计数信号量等,但是有一点,任务通知仅仅是对于任务来讲的,通常是一对一的实现,任务的接受者可以阻塞,但是任务的发送者无法阻塞。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐