天天看点

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

目录

  • 1、概述
  • 2、事件组的特性
    • 2.1 事件组、事件标志和事件位
    • 2.2 事件组位长的设置
    • 2.3 多任务访问
  • 3、使用事件组管理事件
    • 3.1 xEventGroupCreate()
    • 3.2 xEventGroupSetBits()
    • 3.3 xEventGroupGetBits()
    • 3.4 xEventGroupWaitBits()
    • 3.5 示例
  • 4、使用事件组同步任务
    • 4.1 xEventGroupSync()
    • 4.2 实例
    • 5 总结

1、概述

实时嵌入式系统必须采取行动来响应事件。前面描述了 FreeRTOS 允许将事件传递给任务的功能。此类功能包括信号量和队列,它们都具有以下属性:

-》它们允许任务在阻塞状态下等待单个事件发生。

-》当事件发生时,它们解除对单个任务的阻塞——解除阻塞的任务是等待事件的最高优先级任务。

事件组是 FreeRTOS 的另一个功能,它允许将事件传达给任务。与队列和信号量不同:

事件组允许任务在阻塞状态下等待多个事件之一的组合发生。

事件组在事件发生时解除等待同一事件或事件组合的所有任务的阻塞。

事件组的这些独特属性使其可用于同步多个任务、将事件广播到多个任务、允许任务在阻塞状态等待一组事件中的任何一个发生

,以及允许任务处于阻塞状态中等待多个操作的完成。

事件组还提供了减少应用程序使用的 RAM 的机会,因为通常可以用单个事件组替换许多二进制信号量。

事件组功能是可选的。要包含事件组功能,请将 FreeRTOS 源文件 event_groups.c 构建为项目的一部分。

2、事件组的特性

2.1 事件组、事件标志和事件位

一个事件组可以同时设置8个或24个事件。

事件“标志”是一个布尔值(1 或 0),用于指示事件是否发生。

事件“组”是一组事件标志。

一个事件标志只能为 1 或 0,允许将一个事件标志的状态存储在一个位中,并将一个事件组中所有事件标志的状态存储在一个变量中;事件组中每个事件标志的状态由 EventBits_t 类型变量中的单个位表示。因此,事件标志也称为事件“位”。

如果 EventBits_t 变量中的某个位设置为 1,则该位表示的事件已经发生。如果EventBits_t 变量中的某个位设置为0,则该位表示的事件尚未发生。

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

例如:如果事件组的值为 0x92(二进制 1001 0010),即,仅设置事件位 1、4 和 7,因此仅发生了位 1、4 和 7 表示的事件。

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

而1,4,7对应的事件含义是由开发者定义的。比如,将事件组中的位 1 定义为“已从网络接收到消息”;将事件组中的第 4位定义为“一条消息已准备好发送到网络上”;将事件组中的第 7 位定义为“中止当前网络连接”。

2.2 事件组位长的设置

事件组中的事件位数取决于 FreeRTOSConfig.h 中的 configUSE_16_BIT_TICKS 编译时间配置的常量:

如果 configUSE_16_BIT_TICKS 为 1,则每个事件组包含 8 个可用事件位。

如果 configUSE_16_BIT_TICKS 为 0,则每个事件组包含 24 个可用事件位。

2.3 多任务访问

事件组本身就是对象,任何知道它们存在的任务或 ISR 都可以访问这些对象。任意数量的任务可以设置同一事件组中的位,任意数量的任务可以从同一事件组中读取位。

3、使用事件组管理事件

3.1 xEventGroupCreate()

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

描述

创建一个新的事件组并返回一个句柄,通过该句柄可以引用所创建的事件组。

事件组存储在EventGroupHandle_t类型的变量中。如果configUSE_16_BIT_TICKS设置为1,则事件组中实现的位数(或标志)为8;如果configUSE_1 6_BIT_TIC设置为0,则为24。

返回值

NULL : 事件组创建失败。因为没有足够的堆内存。

非NULL的其它值:事件组创建成功,返回的是事件组的句柄。

3.2 xEventGroupSetBits()

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

描述

xEventGroupSetBits() API 函数设置事件组中的一个或多个位,通常用于通知任务已发生由正在设置的一个或多个位表示的事件。

设置RTOS事件组中的位(标志)。不能从中断调用此函数。

有关可以从中断调用的版本,请参阅xEventGroupSetBitsFromISR()。

在事件组中设置的位将自动解除那些处于阻塞状态的任务。这些任务正等待这些被设置的位而处于阻塞状态。

参数

参数 说明
xEventGroup 要被设置的事件组的句柄
uxBitsToSet 一种位掩码,指定事件组中要设置为1的单个或多个事件位。事件组的值通过将事件组的现有值与uxBitsToSet中传递的值按位“或”来更新例如,将uxBitsToSet设置为0x08以设置位3。将uxBitsToSet设置为0x09以设置位3和位0。同时保持事件组中所有其他事件位不变。

反回值

返回调用xEventGroupSetBits()时事件组中位的值。

返回值被uxBitsToSet参数指定的位可能被清除,原因有两个:

1.如果设置的位导致等待此位的任务离开阻塞状态,则该位可能已被自动清除(请参阅xEventGroupWaitBits()的xClearBitsOnExit参数。

2.由于位被设置而离开阻塞状态的任何任务(或任何就绪状态任务),其优先级高于调用xEventGroupSetBits()的任务的优先级,将执行该高优先级任务,并可能在调用xEventGroup SetBits)返回之前更改事件组值。

3.3 xEventGroupGetBits()

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

描述

返回事件组中事件位(事件标志)的当前值。此功能不能在中断中使用。有关可用于中断的版本,请用xEventGroupGetBitsFromISR()。

参数

xEventGroup : 目标事件组句柄。

3.4 xEventGroupWaitBits()

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

描述

xEventGroupWaitBits() API 函数允许任务读取事件组的值,如果尚未设置事件位,则可选择在阻塞状态等待事件组中的一个或多个事件位被设置。

参数

参数 说明
xEventGroup 事件组句柄
uxBitsToWaitFor 一个位值,指示事件组中要检测的位。例如,要等待位0 and/or 位2,请将uxBitsToWaitFor设置为0x05。要等待位0 and/ or 位1 and / or 2,请将uxBitsToWaitFor设置为0x07。uxBitsToWaitFor不能设置为0。
xClearOnExit 如果xClearOnExit设置为pdTRUE,则如果xEventGroupWaitBits()因超时以外的任何原因返回,则在xEventGroupWaitBits)返回之前,将在事件组中清除作为uxBitsToWaitFor参数传递的值中设置的任何位。超时值由xTicksToWait参数设置。如果xClearOnExit设置为pdFALSE,则当对xEventGroupWaitBits()的调用返回时,不会更改事件组中设置的位。
xWaitForAllBits xWaitForAllBits用于创建逻辑AND测试(所有检测位必须都被置1)或逻辑or测试(一个或多个检测位被置1就可以)。
xTicksToWait 阻塞等待时长,以tick计数的时间单位

返回值

等待的事件位被设置或块时间到期时事件组的值。如果较高优先级的任务或中断在调用任务离开阻止状态和退出xEventGroupWaitBits()函数之间更改了事件位的值,则事件组中事件位的当前值将与返回值不同。

测试返回值以了解设置了哪些位。如果xEventGroupWaitBits()因超时而返回,则不会设置所有等待的位。如果xEventGroupWaitBits()返回是因为它等待的位被设置,则返回的值是在xClearOnExit参数设置为pdTRUE的情况下自动清除任何位之前的事件组值。

3.5 示例

此示例演示如何创建一个事件组,通过中断服务程序设置事件组中的位,从任务中设置事件组中的位,阻塞一个事件组。

xEventGroupWaitBits() xWaitForAllBits 参数的效果通过首先执行 xWaitForAllBits 设置为 pdFALSE 的示例,然后执行

xWaitForAllBits 设置为 pdTRUE 的示例来演示。

事件位 0 和事件位 1 从任务中设置。事件位 2 通过中断服务程序设置。

使用下方代码所示的 #define 语句为这三位指定描述名称。

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

以下代码显示了设置事件位 0 和事件位 1 的任务的实现。它位于一个循环中,重复设置一个位,然后另一个位,每次调用EventGroupSetBits() 之间有 200 毫秒的延迟。在设置每个位之前打印出一个字符串,以允许在控制台中看到执行顺序。

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

以下代码 显示了在事件组中设置位 2 的中断服务例程的实现。同样,在设置位之前打印出一个字符串,以允许在控制台中看到执行顺序。然而,在这种情况下,由于不应直接在中断服务例程中执行控制台输出,因此 xTimerPendFunctionCallFromISR() 用于在 RTOS 守护程序任务的上下文中执行输出。

与前面的示例一样,中断服务程序由一个强制软件中断的简单周期性任务触发。在此示例中,每 500 毫秒生成一次中断。

static uint32_t ulEventBitSettingISR( void )
{
	static const char *pcString = "Bit setting ISR -\t about to set bit 2.\r\n";
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
 	/* Print out a message to say bit 2 is about to be set. Messages cannot be
 	printed from an ISR, so defer the actual output to the RTOS daemon task by
 	pending a function call to run in the context of the RTOS daemon task. */
 	xTimerPendFunctionCallFromISR( vPrintStringFromDaemonTask, 
 									( void * ) pcString, 
 									0, 
 									&xHigherPriorityTaskWoken );
 	/* Set bit 2 in the event group. */
 	xEventGroupSetBitsFromISR( xEventGroup, mainISR_BIT, &xHigherPriorityTaskWoken );
 	/* xTimerPendFunctionCallFromISR()和xEventGroupSetBitsFromISR()都写入计时器命令队列,并且都使用相同的
	xHigherPriorityTaskWoken变量。如果写入计时器命令队列导致RTOS守护进程任务离开阻塞状态,如果RTOS守护进程任务的优先
	级高于当前正在执行的任务的优先级(该中断中断的任务),则xHigherPriorityTaskWoken将被设置为pdTRUE.
	.xHigherPriorityTaskWoken用作portYIELD_FROM_ISR()的参数。如果xHigherPriorityTaskWoken等于pdTRUE,
	则调用portYIELD_FROM_ISR()将请求上下文切换。如果xHigherPriorityTaskWoken仍然是pdFALSE,
	那么调用portYIELD_FROM_ISR()将无效。 */
 	portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
           

以下的代码 显示了调用 xEventGroupWaitBits() 以阻塞事件组的任务的实现。该任务为事件组中设置的每个位打印一个字

符串。

xEventGroupWaitBits() xClearOnExit 参数设置为 pdTRUE,因此导致调用 xEventGroupWaitBits() 返回的事件位将在

xEventGroupWaitBits() 返回之前自动清除。

static void vEventBitReadingTask( void *pvParameters )
{
	EventBits_t xEventGroupValue;
	const EventBits_t xBitsToWaitFor = ( mainFIRST_TASK_BIT | mainSECOND_TASK_BIT | mainISR_BIT );
 	for( ;; )
 	{
 		/* 阻塞以等待事件组中的对应事件位被置位。 */
 		xEventGroupValue = xEventGroupWaitBits( xEventGroup,      //事件组
 												xBitsToWaitFor,   //检测的事件位
 												pdTRUE,    //退出阻塞后,清除对应的事件位。
 												pdFALSE,   // 或操作,只要有一个需要的事件位被置位就退出。
 												portMAX_DELAY   /* 一直等待 */
 											);
 		/* 打印对应事件位的信息. */
 		if( ( xEventGroupValue & mainFIRST_TASK_BIT ) != 0 )
 		{
 			vPrintString( "Bit reading task -\t Event bit 0 was set\r\n" );
 		}
 		if( ( xEventGroupValue & mainSECOND_TASK_BIT ) != 0 )
 		{
 			vPrintString( "Bit reading task -\t Event bit 1 was set\r\n" );
 		}
 		if( ( xEventGroupValue & mainISR_BIT ) != 0 )
 		{
 			vPrintString( "Bit reading task -\t Event bit 2 was set\r\n" );
 		}
 	}
 }
           

main() 函数在启动调度程序之前创建事件组和任务。从事件组中读取的任务的优先级高于写入事件组的任务的优先级,保证每次满足读取任务的解锁条件时,读取任务都会抢占写入任务。

int main( void )
{
 	xEventGroup = xEventGroupCreate();  //创建事件组
 	/* 创建任务,用以设置事件组中的事件位 */
 	xTaskCreate( vEventBitSettingTask, "Bit Setter", 1000, NULL, 1, NULL );
 	/* 创建任务,用以读取事件组中的事件位。 */
 	xTaskCreate( vEventBitReadingTask, "Bit Reader", 1000, NULL, 2, NULL );
 	/* 创建任务用以生成中断 */
 	xTaskCreate( vInterruptGenerator, "Int Gen", 1000, NULL, 3, NULL );
 	/* 设置中断ISR. */
 	vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulEventBitSettingISR );
 	vTaskStartScheduler();
 	for( ;; );
 	return 0;
}
           

xEventGroupWaitBits()函数中的xWaitForAllBits 参数被设置为pdFALSE时的输出。

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

xEventGroupWaitBits()函数中的xWaitForAllBits 参数被设置为pdTRUE时的输出。

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

4、使用事件组同步任务

应用程序的设计有时需要两个或多个任务相互同步。例如,考虑一个设计,其中任务 A 接收事件,然后将事件所需的一些处理委托给其他三个任务:任务 B、任务 C 和任务 D。如果任务 A 直到任务 B、C 和任务 D 已完成它们自已的工作后才能接收另一个事件。 那么所有四个任务都需要相互同步。每个任务的同步点将在该任务完成其处理之后,并且在其他每个任务都完成相同的操作之前无法继续进行。任务 A 只能在所有四个任务都达到它们的同步点后才能接收另一个事件。

也就是说,任务A,B,C,D四个任务都会分别阻塞在一个同步点上,然后当大家都达到同步点后,才同时离开阻塞进入READY状态。

但是,在这种场景下不能使用 xEventGroupSetBits() 和 xEventGroupWaitBits() API 函数。如果它们被使用,那么位的设置(表明一

个任务已经到达它的同步点)和位的测试(以确定其他同步任务是否已经到达它们的同步点)将作为两个独立的操作来执行。要了

解为什么会出现问题,请考虑任务 A、任务 B 和任务 C 尝试使用事件组进行同步的场景:

  1. 任务A和任务B已经到达同步点,所以它们的事件位在事件组中被置位,它们处于阻塞状态等待任务C的事件位也被置位。
  2. 任务 C 到达同步点,并使用 xEventGroupSetBits() 设置其在事件组中的位。一旦任务 C 的位置位,任务 A 和任务 B 就会离开阻塞

    状态,并清除所有三个事件位。

  3. 然后任务 C 调用 xEventGroupWaitBits() 等待所有三个事件位都被置位,但此时所有三个事件位都已被清除,任务 A 和任务 B 已离

    开各自的同步点,因此同步失败了。

要成功地使用事件组创建同步点,事件位的设置和事件位的后续测试必须作为单个不间断操作执行。为此提供了 xEventGroupSync()

API 函数。

4.1 xEventGroupSync()

提供 xEventGroupSync() 以允许两个或多个任务使用事件组相互同步。该函数允许任务在一个事件组中设置一个或多个事件位,

然后等待在同一事件组中设置一组事件位,然后等待在同一事件组中设置事件位的组合作为单个不间断操作。

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

参数

参数 说明
xEventGroup 事件组句柄
uxBitsToSet 一个位掩码,指定要在事件组中设置为 1 的事件位或事件位。事件组的值通过将事件组的现有值与uxBitsToSet 中传递的值进行按位或运算来更新。例如,将 uxBitsToSet 设置为 0x04(二进制 0100)将导致设置事件位3(如果尚未设置),同时保持事件组中的所有其他事件位不变。
uxBitsToWaitFor 一个位掩码,指定要在事件组中测试的一个或多个事件位。例如,如果调用任务想要等待事件组中的事件位 0、1 和 2 被设置,则将 uxBitsToWaitFor 设置为 0x07(二进制 111)
xTicksToWait 等待阻塞时间,时间单位是tick

返回值

Returned Value:如果xEventGroupSync() 因为调用任务的解锁条件被满足而返回,那么返回的值就是事件的值满足调用任务的解锁条件时的值(在任何位自动清零之前)。在这种情况下,返回值也将满足调用任务的解锁条件。

如果 xEventGroupSync() 是因为xTicksToWait 参数指定的阻塞时间到期而返回,则返回值是阻塞时间到期时事件组的值。在这种情况下,返回值将不满足调用任务的解锁条件。

4.2 实例

本示例 使用 xEventGroupSync() 同步单个任务实现的三个实例。 task 参数用于将任务在调用 xEventGroupSync() 时设置的事件位传

递给每个实例。该任务在调用 xEventGroupSync() 之前打印一条消息,并在返回对 xEventGroupSync() 的调用之后再次打印。每条消息都包含一个时间戳。这允许在产生的输出中观察执行顺序。伪随机延迟用于防止所有任务同时到达同步点。

static void vSyncingTask( void *pvParameters )
{
	const TickType_t xMaxDelay = pdMS_TO_TICKS( 4000UL );
	const TickType_t xMinDelay = pdMS_TO_TICKS( 200UL );
	TickType_t xDelayTime;
	EventBits_t uxThisTasksSyncBit;
	const EventBits_t uxAllSyncBits = ( mainFIRST_TASK_BIT | 
 										mainSECOND_TASK_BIT | 
 										mainTHIRD_TASK_BIT );
 	/* 创建三个任务的实例-每个任务使用一个不同事件位用于同步。事件位通过每个任务实例的参数*pvParameters传递进任务。
  	并存在uxThisTaskSyncBit变量中。*/
 	uxThisTasksSyncBit = ( EventBits_t ) pvParameters;
 	for( ;; )
 	{
 		/* 通过延迟伪随机时间来花费一些时间执行操作。这可以防止此任务的所有三个实例同时到达同步点,因此可以更容易地观察示例的行为。*/
 		xDelayTime = ( rand() % xMaxDelay ) + xMinDelay;
 		vTaskDelay( xDelayTime );
 		/* 打印一条消息以显示此任务已达到其同步点。pcTaskGetTaskName()是一个API函数,它返回创建任务时分配给任务的名称。 */
 		vPrintTwoStrings( pcTaskGetTaskName( NULL ), "reached sync point" );
 		/*等待所有任务到达同步点 */
 		xEventGroupSync( xEventGroup,    //事件组句柄
  						uxThisTasksSyncBit,    //此任务设置的位,表示它已到达同步点。
  						uxAllSyncBits,  //等待的位,每个参与同步的任务一位。
  						portMAX_DELAY );
 		/* 打印一条消息以显示此任务已通过其同步点。由于使用了无限期延迟,因此只有在所有任务到达各自的同步点后才会执行以下行。*/
 		vPrintTwoStrings( pcTaskGetTaskName( NULL ), "exited sync point" );
 	}
}
           

以下为主函数

/* Definitions for the event bits in the event group. */
#define mainFIRST_TASK_BIT ( 1UL << 0UL ) /* Event bit 0, set by the first task. */
#define mainSECOND_TASK_BIT( 1UL << 1UL ) /* Event bit 1, set by the second task. */
#define mainTHIRD_TASK_BIT ( 1UL << 2UL ) /* Event bit 2, set by the third task. */
/* Declare the event group used to synchronize the three tasks. */
EventGroupHandle_t xEventGroup;
int main( void )
{
 	/* Before an event group can be used it must first be created. */
 	xEventGroup = xEventGroupCreate();
 	/* 创建任务的三个实例。每个任务都有一个不同的名称,稍后打印出来,以直观地指示正在执行的任务。
 	当任务到达其同步点时要使用的事件位使用任务参数传递到任务中。 */
 	xTaskCreate( vSyncingTask, "Task 1", 1000, mainFIRST_TASK_BIT, 1, NULL );
 	xTaskCreate( vSyncingTask, "Task 2", 1000, mainSECOND_TASK_BIT, 1, NULL );
	xTaskCreate( vSyncingTask, "Task 3", 1000, mainTHIRD_TASK_BIT, 1, NULL );
 	
 	vTaskStartScheduler();
 
 	for( ;; );
 	return 0;
}
           

执行示例 时产生的输出如下图 所示。可以看出,即使每个任务在不同(伪随机)时间到达同步点,但每个任务同

时退出同步点 1(即最后一个任务到达同步点的时间)

【ESP32+freeRTOS学习笔记-(九)事件组】1、概述2、事件组的特性3、使用事件组管理事件4、使用事件组同步任务

5 总结

事件组同时处理一组事件,可以做为事件广播,以及任务同步的工具。

事件广播可以将一个事件通知给一个或多个任务。也可以将多个任务通知给多个任务。这里主要用到的API为xEventGroupSetBits() 和 xEventGroupWaitBits。

事件组用于任务同步时,可以让多个任务同时退出阻塞,同步启动。这里主要用到的API为xEventGroupSync()。