FreeRTOS的任务间通信

news2025/2/25 6:30:11

文章目录

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

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

/*
    @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));
/**
  * @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 队列的发送,接收,查询

​ 队列发送(写队列)

/*
    @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);

​ 队列接收(读队列)

/* 
    @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);

​ 查询队列信息

/**
  * @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 任务中读写邮箱

//内容写入邮箱,(用覆盖的方式写入)
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 中断中读写邮箱

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

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

4.3 队列集

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

#define configUSE_QUEUE_SETS 1 /*Queue Set 的函数开关*/

4.3.1 队列集的创建

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

const UBaseType_t uxEventQueueLength:队列集的长度
  
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中的宏定义。

//信号量使用时,需要将此宏定义的值设置为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操作是无效的。
//对指定的信号量进行give操作
BaseType_t xSemaphoreGive( xSemaphoreHandle_t xSemaphore ); 

xSemaphoreHandle_t xSemaphore:信号量对应的句柄
//对指定的信号量进行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)创建信号量

/**
  * @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)删除信号量

/**
  * @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 优先级反转

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

优先级反转实验:

#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

/**
  * @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,这里我们再次强调

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

4.6.3 事件组的工作流程

(1)创建,删除事件组

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

/*			
		@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)设置,清零事件组的某个位

/*			
 		@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);

事件组使用例子:

/*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 函数,分别对应两种不同的使用场景,等待事件组主要用于使用事件组进行事件的管理,而另外一主要用于使用事件组进行任务间的同步。首先我们来了解一下等待事件组和事件组同步的概念

/*
  	@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 事件组控制块

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

4.6.6 事件组实验

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

#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的返回值)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1902017.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用块的网络 VGG

一、AlexNet与VGG 1、深度学习追求更深更大&#xff0c;使用VGG将卷积层组合为块 2、VGG块&#xff1a;3*3卷积&#xff08;pad1&#xff0c;n层&#xff0c;m通道&#xff09;、2*2最大池化层 二、VGG架构 1、多个VGG块后接全连接层 2、不同次数的重复块得到不同的架构&a…

系统集成项目管理工程师第12章思维导图发布

今天发布系统集成项目管理工程师新版第12章脑图的图片版

01背包问题-队列分支限界法-C++

0-1背包问题-队列分支限界法 问题描述&#xff1a; 给定n种物品和一个背包。物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包中的物品&#xff0c;使得装入背包中物品的总价值最大&#xff1f;对于给定的n种物品的重量和价值&#xff0c;以及背包的容量&…

如何选择一家适合自己的商城源码?

商城源码的选择取决于多个因素&#xff0c;包括商城的功能需求、稳定性、易用性、可定制性以及价格等。启山智软作为在市场上被广泛认可且表现优异的商城源码提供商&#xff0c;具有以下的特点和优势&#xff1a; 特点①&#xff1a;国内知名的B2B2C开源商城源码系统&#xff…

Go语言--工程管理、临时/永久设置GOPATH、main函数以及init函数

工作区 Go 代码必须放在工作区中。工作区其实就是一个对应于特定工程的目录&#xff0c;它应包含3个子目录:src 目录、pkg目录和bin 目录。 src 目录:用于以代码包的形式组织并保存 Go源码文件。(比如:.go.chs等)pkg 目录:用于存放经由 go install 命令构建安装后的代码包(包…

东芝TB6560AHQ/AFG步进电机驱动IC:解锁卓越的电机控制性能

作为一名工程师&#xff0c;一直在寻找可靠且高效的组件来应用于你的项目中。东芝的TB6560AHQ/AFG步进电机驱动IC能够提供精准且多功能的电机控制&#xff0c;完全符合现代应用的高要求&#xff0c;保证高性能和易用性。在这篇文章中&#xff0c;我们将探讨TB6560AHQ/AFG的主要…

陈志泊主编《数据库原理及应用教程第4版微课版》的实验题目参考答案实验2

实验目的 1&#xff0e;掌握在SQL Server中使用对象资源管理器和SQL命令创建数据库与修改数据库的方法。 2&#xff0e;掌握在SQL Server中使用对象资源管理器或者SQL命令创建数据表和修改数据表的方 法&#xff08;以SQL命令为重点&#xff09;。 实验设备 操作系统:Win11…

DEPTHAI 2.27.0 发布!

小伙伴们大家好&#xff0c;我们发布了DepthAI 2.27.0版本&#xff0c;本次对DepthAI库有了一些小更新&#xff0c;以下是更新内容。 功能 设置DEPTHAI_ENABLE_FEEDBACK_CRASHDUMP时自动故障转储收集&#xff1b; 漏洞修补 修复深度超出ImageAlign节点时生成PointCloud的问…

Matplotlib Artist 1 概览

Matplotlib API中有三层 matplotlib.backend_bases.FigureCanvas&#xff1a;绘制区域matplotlib.backend_bases.Renderer&#xff1a;控制如何在FigureCanvas上绘制matplotlib.artist.Artist&#xff1a;控制render如何进行绘制 开发者95%的时间都是在使用Artist。Artist有两…

Java开源ERP系统Axelor汉化方法初探

Axelor简介 汉化过程介绍 定义语言和本地化 导出多语言记录 导入翻译 验证翻译 调整翻译 Axelor简介 2024年6月份Axelor ERP发布了8.1版本&#xff0c;适配JDK11及PostgreSQL12及以上版本&#xff08;7及以前版本适配JDK8及PostgreSQL10&#xff09;数据库。v8版本较之前…

景区气象站:守护旅行安全的智能向导

在繁忙的现代社会&#xff0c;人们越来越渴望逃离城市的喧嚣&#xff0c;寻找一处宁静的自然之地放松身心。景区&#xff0c;作为大自然与人类文明交织的瑰宝&#xff0c;吸引了无数游客前来探访。然而&#xff0c;多变的天气往往给游客的旅行带来不确定性。 景区气象站&#x…

Python + OpenCV 开启图片、写入储存图片

这篇教学会介绍OpenCV 里imread()、imshow()、waitKey() 方法&#xff0c;透过这些方法&#xff0c;在电脑中使用不同的色彩模式开启图片并显示图片。 imread() 开启图片 使用imread() 方法&#xff0c;可以开启图片&#xff0c;imread() 有两个参数&#xff0c;第一个参数为档…

nginx的正向代理和反向代理以及tomcat

nginx的正向代理和反向代理&#xff1a; 正向代理以及缓存配置&#xff1a; 代理&#xff1a;客户端不再是直接访问服务端&#xff0c;通过代理服务器访问服务端。 正向代理&#xff1a;面向客户端&#xff0c;我们通过代理服务器的IP地址访问目标范围端。 服务端只知道代理…

10.x86游戏实战-汇编指令lea

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 工具下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1rEEJnt85npn7N38Ai0_F2Q?pwd6tw3 提…

云端AI大模型群体智慧后台架构思考

1 大模型的调研 1.1 主流的大模型 openai-chatgpt 阿里巴巴-通义千问 一个专门响应人类指令的大模型。我是效率助手&#xff0c;也是点子生成机&#xff0c;我服务于人类&#xff0c;致力于让生活更美好。 百度-文心一言&#xff08;千帆大模型&#xff09; 文心一言"…

Web漏洞扫描工具AppScan与AWVS测评及使用体验

AppScan和AWVS业界知名的Web漏洞扫描工具&#xff0c;你是否也好奇到底哪一个能力更胜一筹呢&#xff1f;接下来跟随博主一探究竟吧。 1. 方案概览 第一步&#xff1a;安装一个用于评测的Web漏洞靶场&#xff08;本文采用最知名和最广泛使用的靶场&#xff0c;即OWASP Benchma…

04.ffmpeg打印音视频媒体信息

目录 1、相关头文件 2、相关结构体 3、相关函数 4、函数详解 5、源码附上 1、相关头文件 #include <libavformat/avformat.h> 包含格式相关的函数和数据结构 #include <libavutil/avutil.h> 包含一些通用实用函数 2、相关结构体 AV…

Redis基础教程(九):redis有序集合

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; &#x1f49d;&#x1f49…

Zabbix触发器

目录 触发器基础概念 创建和管理触发器 示例 定义一个触发器 在 Zabbix 中&#xff0c;触发器&#xff08;Trigger&#xff09;用于定义在监控数据满足特定条件时触发警报或动作。触发器是实现监控告警和自动响应的核心组件之一。以下是关于 Zabbix 触发器的详细解释和用法…

刷代码随想录有感(127):动态规划——判断是否为子序列

题干&#xff1a; 代码&#xff1a; class Solution { public:bool isSubsequence(string s, string t) {vector<vector<int>>dp(s.size() 1, vector<int>(t.size() 1, 0));for(int i 1; i < s.size(); i){for(int j 1; j < t.size(); j){if(s[i …