FreeRTOS的任务间通信

文章目录

  • [4 FreeRTOS任务间通信](#4 FreeRTOS任务间通信)
    • [4.1 队列](#4.1 队列)
      • [4.1.1 队列的使用](#4.1.1 队列的使用)
      • [4.1.2 队列的创建,删除,复位](#4.1.2 队列的创建,删除,复位)
      • [4.1.3 队列的发送,接收,查询](#4.1.3 队列的发送,接收,查询)
    • [4.2 邮箱(mailbox)](#4.2 邮箱(mailbox))
      • [4.2.1 任务中读写邮箱](#4.2.1 任务中读写邮箱)
      • [4.2.2 中断中读写邮箱](#4.2.2 中断中读写邮箱)
    • [4.3 队列集](#4.3 队列集)
      • [4.3.1 队列集的创建](#4.3.1 队列集的创建)
      • [4.3.2 队列集读写使用](#4.3.2 队列集读写使用)
    • [4.4 信号量](#4.4 信号量)
      • [4.4.1 信号量基础](#4.4.1 信号量基础)
      • [4.4.2 give,take操作](#4.4.2 give,take操作)
      • [4.4.3 信号量的使用](#4.4.3 信号量的使用)
    • [4.5 互斥量](#4.5 互斥量)
      • [4.5.1 互斥量与信号量实现互斥异同点](#4.5.1 互斥量与信号量实现互斥异同点)
      • [4.5.2 优先级反转](#4.5.2 优先级反转)
      • [4.5.3 优先级继承](#4.5.3 优先级继承)
      • [4.5.4 互斥量的创建,删除](#4.5.4 互斥量的创建,删除)
    • [4.6 事件组(事件标志组)](#4.6 事件组(事件标志组))
      • [4.6.1 事件位](#4.6.1 事件位)
      • [4.6.2 事件组](#4.6.2 事件组)
      • [4.6.3 事件组的工作流程](#4.6.3 事件组的工作流程)
      • [4.6.5 事件组控制块](#4.6.5 事件组控制块)
      • [4.6.6 事件组实验](#4.6.6 事件组实验)

4 FreeRTOS任务间通信

​ 为了实现FreeRTOS任务之间的同步和临界资源互斥的问题,我们需要进行任务之间的通信,针对于任务之间通信,FreeRTOS一般有如下方法:

  • 队列:先进先出的一种结构(队列集)
  • 邮箱:特殊的一种队列
  • 信号量(semaphoe)
  • 互斥量(mutex):类似于锁的一种机制。
  • 事件组(event group):事件的组合
  • 任务通知(task notification)
  • StreamBuffer流媒体存储

4.1 队列

4.1.1 队列的使用

​ 队列是一种先进先出的数据结构,一般来说,写数据一般写在尾部,读数据一般读头部的数据。类似于Linux中的消息队列。

  • 队列中可以包含若干项的数据,数据的个数称为队列的长度。
  • 每个数据的大小是固定的,在建立队列的时候就已经确定了队列的长度和每个数据的大小。
  • 将新的数据放到头部时,并不会覆盖掉原来的数据,队列的本质是循环buffer。
  • 在队列中没有数据的时候,消费者(接收方)是处于阻塞态的。
  • 队列的长度N时,元素的角标是0~N-1。

​ 队列传输数据可以选择两种方法:(但是要注意单片机的地址都是物理地址)

  • 拷贝:就是值传递,传递一个值过去,另一个task也会生成一个该值的局部变量的副本,两个task之间的这个值是不会互相干扰的。
  • 引用:就是地址传递,需要注意的是,由于没有MMU的缘故,单片机传递的是物理地址而不是虚拟地址,所以也就是说两个任务此时此刻用的是同一个地址里面的值,传地址的时候一定要注意临界资源的冲突。

4.1.2 队列的创建,删除,复位

​ 队列的创建分为动态创建内存和静态创建内存两个方式:

c 复制代码
/*
    @brief  动态分配内存创建队列函数
    @retval 返回创建成功的队列句柄,如果返回NULL则表示因内存不足创建失败
    @param  uxQueueLength:队列深度
            uxItemSize:队列中数据单元的长度,以字节为单位
*/
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
 
/*
    @brief  静态分配内存创建队列函数
    @retval 返回创建成功的队列句柄,如果返回NULL则表示因内存不足创建失败
    @param  uxQueueLength:队列深度
    			 uxItemSize:队列中数据单元的长度,以字节为单位
    			 pucQueueStorageBuffer:队列栈空间数组
   			   pxQueueBuffer:指向StaticQueue_t类型的用于保存队列数据结构的变量
 */
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,
								 UBaseType_t uxItemSize,
								 uint8_t *pucQueueStorageBuffer,
								 StaticQueue_t *pxQueueBuffer);
 
/*example:创建一个深度为5,队列单元占uint16_t大小队列*/
QueueHandle_t QueueHandleTest;
QueueHandleTest = xQueueCreate(5, sizeof(uint16_t));
c 复制代码
/**
  * @brief  删除队列
  * @retval None
  * @param  pxQueueToDelete:要删除的队列句柄
*/
void vQueueDelete(QueueHandle_t pxQueueToDelete);

/**
  * @brief  将队列重置为其原始空状态
  * @retval pdPASS(从FreeRTOS V7.2.0之后)
  * @param  xQueue:要复位的队列句柄
*/
BaseType_t xQueueReset(QueueHandle_t xQueue);

4.1.3 队列的发送,接收,查询

​ 队列发送(写队列)

c 复制代码
/*
    @brief  向队列后方发送数据(FIFO先入先出)
    @retval pdPASS:数据发送成功,errQUEUE_FULL:队列满无法写入
    @param  xQueue:要写入数据的队列句柄
            pvItemToQueue:要写入的数据
            xTicksToWait:阻塞超时时间,单位为节拍数,portMAXDELAY表示无限等待
*/
BaseType_t xQueueSend(QueueHandle_t xQueue,
					  				const void * pvItemToQueue,
					  				TickType_t xTicksToWait);
 
/*
   @brief  向队列后方发送数据(FIFO先入先出),与xQueueSend()函数一致
*/
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
							const void * pvItemToQueue,
							TickType_t xTicksToWait);
 
/*
    @brief  向队列前方发送数据(LIFO后入先出)
*/
BaseType_t xQueueSendToFront(QueueHandle_t xQueue,
							 const void * pvItemToQueue,
							 TickType_t xTicksToWait);
 
/*
   @brief  以下三个函数为上述三个函数的中断安全版本
   @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
*/
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
							 const void *pvItemToQueue,
							 BaseType_t *pxHigherPriorityTaskWoken);
 
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
								   const void *pvItemToQueue,
								   BaseType_t *pxHigherPriorityTaskWoken)
 
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
									const void *pvItemToQueue,
									BaseType_t *pxHigherPriorityTaskWoken);

​ 队列接收(读队列)

c 复制代码
/* 
    @brief  从队列头部接收数据单元,接收的数据同时会从队列中删除
    @retval pdPASS:数据接收成功,errQUEUE_FULL:队列空无读取到任何数据
   	@param  xQueue:被读队列句柄
    			  pvBuffer:接收缓存指针
  			    xTicksToWait:阻塞超时时间,单位为节拍数
*/
BaseType_t xQueueReceive(QueueHandle_t xQueue,
						 void *pvBuffer,
						 TickType_t xTicksToWait);
 
/*
   @brief  从队列头部接收数据单元,不从队列中删除接收的单元
*/
BaseType_t xQueuePeek(QueueHandle_t xQueue,
					  void *pvBuffer,
					  TickType_t xTicksToWait);
 
/**
  * @brief  以下两个函数为上述两个函数的中断安全版本
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  */
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,
								void *pvBuffer,
								BaseType_t *pxHigherPriorityTaskWoken);
 
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void *pvBuffer);

​ 查询队列信息

c 复制代码
/**
  * @brief  查询队列剩余可用空间数
  * @param  xQueue:被查询的队列句柄
  * @retval 返回队列中可用的空间数
  */
UBaseType_t uxQueueSpacesAvailable(QueueHandle_t xQueue);
 
/**
  * @brief  查询队列有效数据单元个数
  * @param  xQueue:被查询的队列句柄
  * @retval 当前队列中保存的数据单元个数
  */
UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);
 
/**
  * @brief  查询队列有效数据单元个数函数的中断安全版本
  */
UBaseType_t uxQueueMessagesWaitingFromISR(const QueueHandle_t xQueue);

4.2 邮箱(mailbox)

​ 邮箱的本质就是队列,只不过是长度为1的对列,并且对于邮箱来说,写操作都是覆盖的,所以该队列满了也没事。照样可以进行写操作。而相应的读取数据时,数据并不会被移除。需要注意的是,这里讲到的函数并不只适用于邮箱,也适用于队列,只是由于覆盖和不移除的特质,常常如此使用,所以专门拎出来叫做"邮箱"

4.2.1 任务中读写邮箱

c 复制代码
//内容写入邮箱,(用覆盖的方式写入)
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void * pvItemToQueue);

QueueHandle_t xQueue:队列句柄
const void * pvItemToQueue:数据指针,指向要复制的指针的开头,大小在创立队列时就已经确定了。
  
//读取,但是不移除已经读取的数据
BaseType_t xQueuePeek( QueueHandle_t xQueue,
                       void *pvBuffer, 
                       TickType_t xTicksToWait );

QueueHandle_t xQueue:要读取的队列
void *pvBuffer:buffer指针,队列中头部的数据会被复制到这个指针之中,复制多少呢?在创建队列的时候已经决定了。
TickType_t xTicksToWait:读取的超时时间,与发送的超时时间一样,设置为0表示非阻塞,>0表示阻塞最大时间,portMAX_DELAY表示等待数据,直到有数据为止。

4.2.2 中断中读写邮箱

c 复制代码
//写入,带中断保护
BaseType_t xQueueOverwriteFromISR (QueueHandle_t xQueue, 
                                   const void * pvItemToQueue,
                                   BaseType_t *pxHigherPriorityTaskWoken);

//检测队列是否已满,(只适用于ISR)
BaseType_t xQueueIsQueueFullFromISR( const QueueHandle_t xQueue );

4.3 队列集

​ 队列集的本质也是一个队列,只不过之前存储的是一个个数据,而现在队列集存储的是一个个队列。使用队列集时需我们在FreeRTOSConfig.h中配置宏,队列集的长度等于队列集中所有队列长度之和。

c 复制代码
#define configUSE_QUEUE_SETS 1 /*Queue Set 的函数开关*/

4.3.1 队列集的创建

C 复制代码
/*队列集的长度应该是 队列A的长度+队列B的长度*/
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );

const UBaseType_t uxEventQueueLength:队列集的长度
  
c 复制代码
BaseType_t xQueueAddToSet(QueueSetMemberHandle_t xQueueOrSemaphore,    
                          QueueSetHandle_t xQueueSet );

QueueSetMemberHandle_t xQueueOrSemaphore:队列成员
QueueSetHandle_t xQueueSet:要加入到哪个队列集中

4.3.2 队列集读写使用

​ 队列集的读写和队列是相似的,他很像是select,从很多队列中找出是谁有数据,然后进行读写工作。(仍待学习)

4.4 信号量

4.4.1 信号量基础

​ 与Linux中的信号量十分相似,他不能够传输数据,只能够用来表示资源的个数,占用情况,任务可以对信号量进行give和take操作,即类似于Linux中的pv操作。常见的信号量有四种:计数信号量,二值信号量,互斥信号量(就是互斥量),递归信号量。这一节中我们主要了解计数信号量和二值信号量。这几种信号量除了取值不一样以外,其实操作都是一样的。

  • 信号量只传递状态,即(信号),而不能传递数据。所以我们一般用来做任务间的同步。同时相比于队列,他更加节省内存。
  • 计数型信号量的值:0~整数,用来表示资源池中还有多少资源可用。
  • 二进制信号量:取值返回为0或者1,但是二进制信号量的初始值都为0。

​ 其实我们使用队列也可以实现这样的功能,那么为什么我们需要信号量呢?主要有以下几点:

  • 使用队列可以传递数据,数据的保存需要空间。
  • 使用信号量时不需要传递数据,更加节省空间。
  • 使用信号量时不需要复制数据,效率更高。

ps:使用信号量时,需要配置FreeRTOSConfig.h中的宏定义。

c 复制代码
//信号量使用时,需要将此宏定义的值设置为1 
#define configUSE_COUNTING_SEMAPHORES  1 

//另外,若在keil编译时出现如下错误,也可能是由于宏定义configUSE_COUNTING_SEMAPHORES的缺失导致的。
Undefined symbol xQueueCreateCountingS

4.4.2 give,take操作

  • give给所用信号量+1,进行解锁操作。
  • take给所用信号量 - 1,进行加锁操作。
  • 对于计数信号量来说:give可以无限的往上加,直到达到了信号量的取值范围。
  • 对于二值信号量来说,若值为1,再进行give操作是无效的。
c 复制代码
//对指定的信号量进行give操作
BaseType_t xSemaphoreGive( xSemaphoreHandle_t xSemaphore ); 

xSemaphoreHandle_t xSemaphore:信号量对应的句柄
c 复制代码
//对指定的信号量进行take操作
 xSemaphoreTakeRecursive( SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );

xSemaphoreHandle_t xSemaphore:信号量对应的句柄
TickType_t xBlockTime:超时时间选项
  									 > 0 即超时的滴答数,可以用pdMS_TO_TICKS宏来规定时间。
  									 不阻塞,即阻塞时间为0, take不成功时,返回err
   									 portMAX_DELAY 一直阻塞take资源,直到成功take。

4.4.3 信号量的使用

(1)创建信号量

c 复制代码
/**
  * @brief  动态分配内存创建二值信号量函数
  * @retval None
  * @param  xSemaphore:创建的二值信号量句柄
  */
void vSemaphoreCreateBinary(SemaphoreHandle_t xSemaphore);
 
/**
  * @brief  静态分配内存创建二值信号量函数
  * @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
  * @param  pxSemaphoreBuffer:指向一个StaticSemaphore_t类型的变量,该变量将用于保存信号量的状态
  */
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(
									StaticSemaphore_t *pxSemaphoreBuffer);
 
/**
  * @brief  动态分配内存创建计数信号量函数
  * @retval 返回创建成功的信号量句柄,如果返回NULL则表示内存不足无法创建
  * @param  uxMaxCount:可以达到的最大计数值
  * @param  uxInitialCount:创建信号量时分配给信号量的计数值 
  */
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
 
/**
  * @brief  静态分配内存创建计数信号量函数
  * @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
  * @param  uxMaxCount:可以达到的最大计数值
  * @param  uxInitialCount:创建信号量时分配给信号量的计数值
  * @param  pxSempahoreBuffer:指向StaticSemaphore_t类型的变量,该变量然后用于保存信号量的数据结构体
  */
SemaphoreHandle_t xSemaphoreCreateCountingStatic(
									UBaseType_t uxMaxCount,
									UBaseType_t uxInitialCount,
									StaticSemaphore_t pxSempahoreBuffer);


//例如:
SemaphoreHandle_t xSemaphore //创建一个信号量句柄
xSemaphore = xSemaphoreCreateCounting( 10, 0 );//创立一个初始值为0,最大值为10的信号量

(2)(3)进行take,give操作(Linux中的pv操作)

(4)删除信号量

c 复制代码
/**
  * @brief  释放信号量函数
  * @retval 如果信号量释放成功,则返回pdTRUE;如果发生错误,则返回pdFALSE
  * @param  xSemaphore:要释放的信号量的句柄
  */
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
 
/**
  * @brief  释放信号量的中断安全版本函数
  * @retval 如果成功给出信号量,则返回pdTRUE,否则errQUEUE_FULL
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  */
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);

4.5 互斥量

4.5.1 互斥量与信号量实现互斥异同点

​ 在前面学习的信号量中,我们似乎已经可以实现互斥的功能了,那么接下来学习的互斥量和二值信号量用来做互斥,有什么区别呢?

在这里我们解决三个问题,分别是上锁解锁任务不同,优先级反转,递归上锁/解锁问题。

  1. 上锁/解锁任务不同:即可以在任务1中上锁后,在任务2中解锁。这个问题无论是互斥量还是信号量都解决不了。
  2. 由于上锁阻塞,导致的优先级发生翻转,低优先级任务比高优先任务先执行的情况。使用互斥量而不是二值信号量可以解决这个问题。
  3. 在递归中上锁,一直申请,但不释放资源。会造成递归死锁。可以通过使用递归锁来解决。

4.5.2 优先级反转

​ 使用二值信号量作为互斥量的时候会导致优先级反转的问题。而互斥量带有优先级继承的功能,可以解决这个问题(优先级继承我们后面解释)。

优先级反转实验:

c 复制代码
#include <...若干>

void led_task(void *param);
void usart_task1(void *param);
void usart_task2(void *param);
void usart_task3(void *param);

void SystemClock_Config(void);

SemaphoreHandle_t xSemaphore;//二值信号量的全局变量

int main()
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
 
  
	xSemaphore = xSemaphoreCreateCounting( 1, 0 );
	xSemaphoreGive(xSemaphore ); 
	
	xTaskCreate(usart_task1, "task1", 100, NULL, 1, NULL);
	xTaskCreate(usart_task2, "task2", 100, NULL, 2, NULL);
	xTaskCreate(usart_task3, "task3", 100, NULL, 3, NULL);
	
	vTaskStartScheduler();
)
  
//三个任务,都是打印串口,但是优先级不一样,这里为了方便分析,我们只打印一次。
void usart_task1(void *param)
{
		printf("task1 taking\n");
		xSemaphoreTake(xSemaphore, portMAX_DELAY); 
		printf("task 1:priority [1]\n");
		vTaskDelay( pdMS_TO_TICKS(1000));
		printf("task1 finish\n");
	
		printf("1 give mutex\n");
		xSemaphoreGive(xSemaphore);
  
	vTaskDelete( NULL );
}

  
//任务2和任务3的开始我们都vTaskDelay延迟了10ms,用来模拟任务1先执行,之后再任务1进入阻塞态后任务2和3同时抢占的情况
void usart_task2(void *param)
{
	vTaskDelay( pdMS_TO_TICKS(10)); 
	
	for(int i=0; i<=10; i++)
	{
		//xSemaphoreTake(xSemaphore, portMAX_DELAY);
		printf("task 2:priority [2]\n");
		//xSemaphoreGive(xSemaphore); 
	}
		
	vTaskDelete( NULL );
}

void usart_task3(void *param)
{
		vTaskDelay( pdMS_TO_TICKS(10));
  
		printf("task3 takeing\n");
		xSemaphoreTake(xSemaphore, portMAX_DELAY);
		printf("task 3:priority [3]\n");;
		vTaskDelay( pdMS_TO_TICKS(1000));
		printf("task3 finish\n");
	
		printf("3 give mutex\n");
		xSemaphoreGive(xSemaphore); 

  
	vTaskDelete( NULL );
}

实验现象及分析:

再上述实验中,我们使用vTaskDelay()来让task1先执行,之后task2和task3进行抢占。接下来我们分析执行过程,为什么会发生优先级反转情况:

  1. task1执行,进行take操作上锁后开始执行程序,task1程序进入到vTaskDelay()时,task1进入阻塞态
  2. task2和task3开始抢占运行态,但是task3的优先级高,所以task3抢占成功。
  3. task3抢占成功后,要进行take操作,但是这时候,资源是被task1占用的,所以task3再调用了take操作后进入阻塞态。
  4. task2优先级高,所以先执行task2,(此时task3因为锁的关系进入阻塞态无法运行)。
  5. task2运行结束,由于task3一直阻塞,所以此时是task3抢占后发现自己还是阻塞状态,没有办法,只能等task1什么时候把锁解开,进行give操作后,才能够抢占。

以上,才会出现本应该按照顺序3->2->1优先级运行的任务,变成了2->1->3的运行顺序。

4.5.3 优先级继承

​ 前面的实验中,task1上的锁在task3中被使用了,所以导致了翻转问题以及使得task1的优先级提高。**优先级继承(priority inheritance)是指当高优先级进程(t3)请求一个已经被被低优先级(t1)占有的临界资源时,将低优先级进程(t1)的优先级临时提升到与高优先级进程一样的级别,使得低优先级进程能更快地运行,从而更快地释放临界资源。低优先级进程离开临界区后,其优先级恢复至原本的值。**当我们使用互斥锁的时候,就会有优先级继承的功能。

4.5.4 互斥量的创建,删除

​ 在创建时,需要配置宏定义#define configUSE_MUTEXES 1

c 复制代码
/**
  * @brief  动态分配内存创建互斥信号量函数
  * @retval 创建互斥信号量的句柄
  */
SemaphoreHandle_t xSemaphoreCreateMutex(void);
 
/**
  * @brief  静态分配内存创建互斥信号量函数
  * @param  pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
  * @retval 返回成功创建后的互斥锁的句柄,如果返回NULL则表示内存不足创建失败
  */
SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *pxMutexBuffer);
 
/**
  * @brief  动态分配内存创建递归互斥信号量函数
  * @retval 创建递归互斥信号量的句柄,如果返回NULL则表示内存不足创建失败
  */
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
 
/**
  * @brief  动态分配内存创建二值信号量函数
  * @param  pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
  */
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(StaticSemaphore_t pxMutexBuffer);




/**
  * @brief  删除信号量函数
  * @param  xSemaphore:要删除的信号量的句柄
  * @retval None
  */
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);

4.6 事件组(事件标志组)

4.6.1 事件位

​ 事件的"标志"(即事件位)是一个布尔值(即0和1),用于指示某个事件是否发生了。而事件"组"就是事件位的集合。集合中有多少个事件位是由configUSE_16_BIT_TICKS宏所决定的,若配置为1,那么事件组长度为16个位,每个事件组包含8个可用的事件位。若配置为0,那么事件组的长度为32个位,每个事件组包含24个可用的事件位。(无论是哪种模式,事件组的高8位永远是保留位)。

​ 任何知道事件组存在的任务或ISR都可以访问它。任意数量的任务可以在同一事件组中设置位,并且任意数量的任务可以从同一事件组中读取位。所以我们可以利用事件组来进行任务间的通知,同步。

事件组可以用来标识事件的状态,从而针对不同的状态做出对应的动作,比如下面的几个例子:

  • 当收到一条消息并且需要把这条消息处理时,将某个位(标志)置1表示这个事件,当队列中没有消息需要处理的时候就可以将这个位(标志)置 0。
  • 当把队列中的某个消息需要通过网络发送时,将某个位(标志)置1表示这个事件,当没有数据需要从网络发送出去的时候,就将这个位(标志)置 0。
  • 现在需要向网络中发送一个心跳信息,将某个位(标志)置 1。现在不需要向网络中发送心跳信息,这个位(标志)置 0。
  • 等等等等,事件组的作用十分广泛。

4.6.2 事件组

​ 之前我们就已经提到过了,事件组就是一组事件位的集合,事件组中的不同的事件位通过其位编号来访问,按照之前的来举例子,那么可以有如下的定义:

  • 事件标志组的 bit0 表示队列中的消息是否处理掉。
  • 事件标志组的 bit1 表示是否有消息需要从网络中发送出去。
  • 事件标志组的 bit2 表示现在是否需要向网络发送心跳信息。
  • 。。。

在使用事件组之前,我们需要设置宏定义configUSE_16_BIT_TICKS,这里我们再次强调

c 复制代码
#define configUSE_16_BIT_TICKS 1	// 时,事件标志组可以存储 8 个事件位
#define configUSE_16_BIT_TICKS 0	// 时,事件标志组存储 24 个事件位。
//ps:高八位永远是保留位不使用!!!
  
//而时间组存储事件位依靠的是EventBits_t的数据类型,这个数据类型可以是16位的也可以是32位的,主要取决于上面的宏定义。

4.6.3 事件组的工作流程

(1)创建,删除事件组

​ 创建事件组有动态和静态两种方式,我们常使用的都是静态创建方式。

c 复制代码
/*			
		@brief:动态创建事件组。
		@retval:成功返回事件组的句柄,失败返回NULL,常见的失败原因是内存空间不足导致创建失败。
*/
EventGroupHandle_t xEventGroupCreate(void);

/*			
		@brief:静态创建事件组。
		@retval:成功返回事件组的句柄,失败返回NULL,常见的失败原因是内存空间不足导致创建失败。
		@param:pxEventGroupBuffer:指向StaticEventGroup_t类型的变量,该变量用于存储事件组数据结构体。这个结构体需要我们提前配置好。
*/
EventGroupHandle_t xEventGroupCreateStatic(StaticEventGroup_t *pxEventGroupBuffer);


/*---------------------------------------------------------------------------------------------------------------*/
/*			
		@brief:删除已经创建事件组。
		@retval:None
		@param:EventGroupHandle_t xEventGroup:要删除的事件组的句柄。
*/
void vEventGroupDelete(EventGroupHandle_t xEventGroup);

(2)设置,清零事件组的某个位

c 复制代码
/*			
 		@brief:用来设置一个事件组中某个位。
 		@retval:如果uxBitsToSet中的一个或多个位在事件组被设置之后为1,那么xEventGroupSetBits将返回pdPASS。                     				如果uxBitsToSet中的所有位在事件组被设置之后都为0(即事件组已经在调用之前就处于这种状态),那么													xEventGroupSetBits将返回errQUEUE_FULL。
 		@param:EventGroupHandle_t xEventGroup:要设置的事件组的句柄
					 const EventBits_t uxBitsToSet:指定要在事件组中设置的一个或多个位的位值,一般来说设置单个位时常采用位操作进行																			 设置,例如设置0位时,用(1<<0)来设置,而设置多个位时,例如设置为0x09																							(即=0000 1001)表示置位3和位0。注意位是从0开始算的,而不是1哦。
*/
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );


/*---------------------------------------------------------------------------------------------------------------*/
/*
   @brief:将某个事件组中的某个位清零。
   @retval:返回清除指定位之前的事件组的值。
   @param:xEventGroup:要清除的事件位所在的事件组的句柄。
   				uxBitsToSet:表示要在事件组中清除一个或多个位的按位值。
*/
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear);


/*
  	@brief:下面的两个函数是上述两个函数的中断安全版本。
  	@retval:消息已发送到RTOS守护进程任务,则返回pdPASS,否则将返回pdFAIL
  	@param:pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
*/
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup,
									 								const EventBits_t uxBitsToSet,
									 								BaseType_t *pxHigherPriorityTaskWoken);
 
BaseType_t xEventGroupClearBitsFromISR(EventGroupHandle_t xEventGroup,
									   								const EventBits_t uxBitsToClear);

事件组使用例子:

c 复制代码
/*example1: 将事件组 EventGroup_Test 的位 1 和 3 置位*/
EventBits_t val;
val = xEventGroupSetBits(EventGroup_Test, 0x0A);

/*example2: 将事件组 EventGroup_Test 的位 0 和 2 清零*/
EventBits_t val;
val = xEventGroupClearBits(EventGroup_Test, 0x05);

(3)等待事件组的某个位

​ FreeRTOS 关于事件组提出了等待事件组和事件组同步两个比较重要的 API 函数,分别对应两种不同的使用场景,等待事件组主要用于使用事件组进行事件的管理,而另外一主要用于使用事件组进行任务间的同步。首先我们来了解一下等待事件组和事件组同步的概念

c 复制代码
/*
  	@brief:允许任务读取事件组的值,并且可以选择在阻塞状态下等待事件组中的一个或多个事件位被设置(如果事件位尚未设置)
  	@retval:返回事件位,等待完成设置/或阻塞时间过期时的事件组值。
  	@param:EventGroupHandle_t xEventGroup:所操作事件组的句柄
            const EventBits_t uxBitsToWaitFor:所等待事件位的掩码,例如设置为0x05表示等待第0位和/或第2位
            const BaseType_t xClearOnExit:pdTRUE表示事件组条件成立退出阻塞状态时将掩码指定的所有位清零;																									 pdFALSE表示事件组条件成立退出阻塞状态时不将掩码指定的所有位清零;
            const BaseType_t xWaitForAllBits:pdTRUE表示等待掩码中所有事件位都置1,条件才算成立(逻辑与);																										  pdFALSE表示等待掩码中所有事件位中一个置1,条件就成立(逻辑或);
            															 通俗来说的意思就是,是否等待所有事件都成立。
            TickType_t xTicksToWait:任务进入阻塞状态等待时间成立的超时节拍数。portMAX_DELAY为永不超时。
*/
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
                                 const EventBits_t uxBitsToWaitFor,
                                 const BaseType_t xClearOnExit,
                                 const BaseType_t xWaitForAllBits,
                                 TickType_t xTicksToWait );

/*
    @brief:事件组同步
    @retval:返回函数退出时事件组的值
    @param:EventGroupHandle_t xEventGroup:所操作事件组的句柄
    			 uxBitsToSet:设置和测试位的事件组
   			   uxBitsToWaitFor:指定事件组中要测试的一个或多个事件位的按位值
    		   xTicksToWait:任务进入阻塞状态等待时间成立的超时节拍数
*/
//关于返回值:举个简单的例子就容易理解:假设目前有两个任务,分别为 TASK1 和 TASK2 ,如果 TASK1 被执行过程中因为延时等原因先于 TASK2 调用了 xEventGroupSync() 函数,参数 uxBitsToSet 被设置为 0x01(0000 0001),参数 uxBitsToWaitFor 被设置为 0x05(0000 0101),则 TASK1 执行到该函数时会将事件组中位 0 的值置 1 ,然后进入阻塞状态,等待位 2 和位 0 同时被置 1 ;如果 TASK2 与 TASK1 一样,只不过落后于 TASK1 执行 xEventGroupSync() 函数,并且参数 uxBitsToSet 被设置为 0x04(0000 0100),当 TASK2 执行该函数时会将事件组中位 2 的值置 1 ,此时满足解锁条件,所以 TASK2 不会进入阻塞状态,同时 TASK1 也满足解锁条件,从阻塞状态中退出,这时候假设任务优先级一致,则 TASK1 和 TASK2 会同时从同步点开始运行后续的程序代码,从而达到同步的目的。
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
													const EventBits_t uxBitsToSet,
													const EventBits_t uxBitsToWaitFor,
													TickType_t xTicksToWait);

PS:事件组只起到通知的作用,如果想把数据保存起来,就要另外使用其他方法保存数据。使用事件组的时候,需要#define configSUPPORT_STATIC_ALLOCATION 。

4.6.5 事件组控制块

c 复制代码
//事件的标志组存储在EventBits_t 类型的变量中,该变量在事件组结构体中定义。除了这个,FreeRTOS还使用了一个链表来记录等待事件的任务,所有等待某个事件的任务都会被挂载到这个等待事件列表xTasksWaitingForBits下。

4.6.6 事件组实验

​ 配合中断,打印xEventGroupWaitBits的返回值。方便我们理解事件组的返回值。

c 复制代码
#define REDLEDEVENT (1<<0)
#define GREENLEDEVENT (1<<1)
#define BLUELEDEVENT (1<<2)

void SystemClock_Config(void);
void task_led(void *param);
EventGroupHandle_t event_handle1;

int mian()
{
	event_handle1 = xEventGroupCreate();
  
  xTaskCreate(task_led, 
			  		"task1",
					  100,
					  NULL,
					  3,
					  NULL);
			  
	  vTaskStartScheduler();
}
void task_led(void *param)
{
	EventBits_t val;
	while(1)
	{
		val = xEventGroupWaitBits( event_handle1,
							       REDLEDEVENT|GREENLEDEVENT|BLUELEDEVENT,
							       pdTRUE,
							       pdFALSE,
							       portMAX_DELAY);
		
		printf("return val:%lu\n", val);
		
	}
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_PIN)
{   
	switch (GPIO_PIN)
	{
		case GPIO_PIN_12:
			xEventGroupSetBits(event_handle1, REDLEDEVENT);
			break;
		case GPIO_PIN_13:
			xEventGroupSetBits(event_handle1, GREENLEDEVENT);
			break;
		case GPIO_PIN_14:
			xEventGroupSetBits(event_handle1, BLUELEDEVENT);
			break;
	}
}

实验现象:按顺序按下KEY1,KEY2,KEY3。观察printf的输出值(即wait的返回值)

相关推荐
weixin_4526006934 分钟前
如何为老化的汽车铅酸电池充电
开发语言·单片机·安全·汽车·电机·电源模块·充电桩
极客小张2 小时前
ESP32 步进电机精准控制:打造高精度 DIY 写字机器人,实现流畅书写体验
单片机·嵌入式硬件·mcu·物联网·机器人·硬件工程
安步当歌5 小时前
【FFmpeg】av_write_trailer函数
c语言·c++·ffmpeg·视频编解码·video-codec
HanLop6 小时前
C语言-动态内存管理
c语言·开发语言
EthanWsir6 小时前
嵌入式C语言面试相关知识——关键字(不定期更新)
c语言·开发语言·面试
XD7429716366 小时前
【TB作品】51单片机 Proteus仿真 超声波 LCD1602显示实验
嵌入式硬件·51单片机·proteus
我们的五年8 小时前
【算法:贪心】:贪心算法介绍+基础题(四个步骤);柠檬水找零(交换论证法)
c语言·数据结构·c++·算法·贪心算法
赟赟、嵌入式8 小时前
linux RTC时钟时间出现了明显的偏移
linux·驱动开发·嵌入式硬件·实时音视频
是梅梅森8 小时前
GPIO配置-PIN_Speed的理解
stm32
龙迅代理(jiuli)8 小时前
LT8711GX 国产芯片 Type-C/DP1.4转HDMI2.1 用于加密狗 对接站
c语言·开发语言