FreeRTOS学习笔记十三【事件组】
目的
- 介绍事件组的实际用途。
- 事件组相对于其他同步功能的优缺点。
- 如何设置事件组中的事件位。
- 如何在阻塞状态中等待在事件组中的事件位。
- 如何使用事件组来同步一组任务。
简介
嵌入式实时操作系统必须实时响应事件,前面介绍了几种将事件传递给任务的方法,如信号量,队列。它们都具有一下特性:
- 它们允许在阻塞态下等待单个事件的发送。
- 它们在事件发送时可以取消阻塞的单个任务(取消的是等待事件优先级最高的任务)。
事件组是FreeRTOS提供另一种事件传递的方法,它与队列和信号量的不同点:
- 时间组允许任务在阻塞态下等待一个或多个事件。
- 事件发生时,事件将取消阻塞等待同一事件或事件组合的所有任务。
事件组的这些特性可以用于多个任务同步,将事件广播到多个任务,允许任务在阻塞态下等待一组事件中的任何一个发生,并允许任务在阻塞态等待多个事件以完成多个操作。有时也可以用单个时间组替代多个二值信号量,以减少RAM的使用。
事件组功能是可选的,要使用事件组的功能,需要将event_groups.c添加到项目中。
事件组的特征
事件组、事件标志、事件位
事件标志是一个布尔值(1或0),用于指示事件是否发生。事件组是一组事件标志,事件标志只能是1或0,允许事件标志存储在一个位中,事件组中的所有事件标志的状态存储在单个变量中,其中每个事件标志由EvenBits_t类型的变量中的一个位表示,因此,事件标志也成为事件位。如果EvenBits_t变量中的某个位为1,则表示该位表示的事件以发生。如果EvenBits_t变量中的某个位为0,则表示该位表示的事件未发生。下图显示了各个事件标志位如何映射到EvenBits_t变量中的各个位。
例如:事件组的值为0x92,即事件位1、4、7为1,因此仅发生由位1、4、7表示的事件,如下图。
其中,每位的具体意义由应用程序定义,例如:
- 位0定义为“已接受到网络消息”。
- 位1定义为“已将消息发送到网络”。
- 位2定义为“网络中断”。
- 等等。
一个事件组可表示的位数由FreeRTOSConfig.h中的configUSE_16_BIT_TICKS配置:
- configUSE_16_BIT_TICKS为1,则每个事件组可以包含8个可用事件位。
- configUSE_16_BIT_TICKS为0,则每个事件组可以包含24个可用事件位。
多个任务访问事件组
事件组本身也是一个对象,任何直到它句柄的任务或ISR都可以访问它,任意数量的任务都可以在同一事件组中设置位,也可以从同一事件组读取位。
通过事件组管理事件
xEventGroupCreate()
在使用事件组之前必须先创建它,它的API原型如下:
EventGroupHandle_t xEventGroupCreate( void );
返回值:
- NULL
创建失败,没有足够的内存空间存放事件组。 - 非NULL
创建成功,返回的是事件组的句柄。
xEventGroupSetBits()
xEventGroupSetBits()用于设置事件组中的一个或多个事件位,也可用于通知任务当前已设置的位。注意不要在中断中使用它,中断中应使用中断安全版API,它的原型如下:
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
参数:
- xEventGroup
事件组的句柄。 - uxBitsToSet
它是一个位掩码,指定事件组中哪些位设置为1。通过事件组现有值与uxBitsToSet中的值按位或操作来更新事件组的值。例如:uxBitsToSet为0x04,则将事件组中的bit 2(从bit 0开始)设置为1,而其他位不变。
返回值:
返回调用xEventGroupSetBits()时事件组的值,注意,返回的值不一定包含uxBitsToSet设置的位,因为这些位可能被其他任务再次清除。
xEventGroupSetBitsFromISR()
xEventGroupSetBitsFromISR()是xEventGroupSetBits()的中断安全版本。"given"信号量是一种确定性操作,因为事先知道给出信号量可能导致最多一个任务离开阻塞状态。但当在事件组中设置事件位时,事先不知道有多少任务将离开阻塞状态,所以事件组中的设置事件位是不确定性操作。因此FreeRTOS设计和实现标准不允许在中断服务程序内执行不确定性操作,或禁止中断。所以xEventGroupSetBitsFromISR()不直接在中断服务例程中设置事件位,而是将操作推迟到RTOS守护程序任务。它的原型如下:
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken );
参数和前面介绍的参数相同,因此这里不再介绍。因为设置事件位的操作被推迟到了守护程序任务,所以这里返回的也不再是调用时的事件组的值。而是pdPASS和pdFALSE。
- pdPASS
成功将操作命令发送到定时器命令队列。 - pdFALSE
如果定时器命令队列为满,则没有将操作发送到定时器命令队列。
xEventGroupWaitBits()
xEventGroupWaitBits()函数允许任务读取事件组的值,并且事件尚未发生,可以在阻塞状态下选择等待事件组中的一个或多个事件。它的原型如下:
EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
参数:
- xEventGroup
事件组的句柄。 - uxBitsToWaitFor
它是一个位掩码,指定要在事件组中测试的事件位(一个或多个)。例如,调用任务想要等待事件组中的事件位0和(或)事件位2被设置,则将uxBitsToWaitFor设置为0x05。 - xClearOnExit
如果调用任务满足解除阻塞条件,并且xClearOnExit为pdTRUE,则在xEventGroupWaitBits()退出前,清除uxBitsToWaitFor指定的事件位。如果xClearOnExit为pdFALSE,则xEventGroupWaitBits()函数不会修改事件组的事件位。 - xWaitForAllBits
uxBitsToWaitFor指定的是事件组中要测试的位,而当xWaitForAllBits为pdFALSE时uxBitsToWaitFor中任何位被设置就退出函数(超时后也会退出),当xWaitForAllBits为pdTRUE时,uxBitsToWaitFor中设置的所有位被设置才退出函数(超时后也会退出)。 - xTicksToWait
最长的超时时间,如果在设定时间内没有达到上面的要求则会退出函数,这个参数和前面介绍的xTicksToWait使用方法一样。
返回值:
如果有事件位被设置,并且满足调用任务解除阻塞条件,则返回的值是满足调用任务退出阻塞态的事件组的值。如果是由于超时退出,则返回的值将不满足调用任务退出阻塞态的事件组的值。
调用程序用于确定任务是否将进入阻塞态以及任务何时将离开阻塞态的条件称为解除阻塞条件。解除阻塞条件由uxBitsToWaitFor和xWaitForAllBits两个参数的组合指定:
- uxBitsToWaitFor指定要测试的事件组中的哪些事件位
- xWaitForAllBits指定是使用按位或(任何指定的事件位被设置)测试还是按位与(所有指定的事件位都要全部被设置)测试
如果在调用xEventGroupWaitBits()时满足解除阻塞条件,则任务不会进入阻塞态。下表提供了导致任务进入或退出阻塞态的条件示例,其中只给出了4位,其余的没有给出可以假设为0.
| 事件组的值 | uxBitsToWaitFor | xWaitForAllBits | 行为 |
|---|---|---|---|
| 0000 | 0101 | pdFALSE | 调用任务将进入阻塞状态,因为事件组中未设置bit 0或bit 2,并且在事件组中设置bit 0或bit 2时任务将退出阻塞态 |
| 0100 | 0101 | pdTRUE | 调用任务将进入阻塞状态,因为事件组中未设置bit0和bit2,并且在事件组中设置bit 0和bit 2时任务将退出阻塞态 |
| 0100 | 0110 | pdFALSE | 调用任务不会进入阻塞态,因为事件组中有一位(bit 2)被设置 |
| 0100 | 0110 | pdTRUE | 调用任务会进入阻塞态,因为事件组中只有一位被设置,这里需要两位同时被设置才能退出阻塞态 |
调用任务使用uxBitsToWaitFor参数指定要测试的位,并且可以设置调用任务在满足解除阻塞条件后将这些位清除,但也可以使用xEventGroupClearBits()函数来清除事件位,但是使用该API函数可能产生竞争条件:
- 有多个任务使用同一事件组
- 通过不同的任务或ISR设置事件组中的事件位
如果直接在xEventGroupWaitBits()函数中使用xClearOnExit,就可以避免这些潜在的竞争条件,如果xClearOnExit设置为pdTRUE,则事件位的测试和清除对于调用任务而言将是一个原子操作(不受其他任务或中断的影响)。
示例
示例中将演示:
- 创建事件组。
- 从ISR中设置事件组中的事件位。
- 从任务中设置事件组中的事件位。
- 阻塞事件组。
/* 定义事件位的意义 */
#define mainFIRST_TASK_BIT ( 1UL << 0UL )
#define mainSECOND_TASK_BIT ( 1UL << 1UL )
#define mainISR_BIT ( 1UL << 2UL )
/* 设置事件位的任务 */
static void vEventBitSettingTask( void *pvParameters )
{
const TickType_t xDelay200ms = pdMS_TO_TICKS( 200UL ), xDontBlock = 0;
for( ;; )
{
vTaskDelay( xDelay200ms );
/* 设置bit 0 */
vPrintString( "Bit setting task -\t about to set bit 0.\r\n" );
xEventGroupSetBits( xEventGroup, mainFIRST_TASK_BIT );
vTaskDelay( xDelay200ms );
/* 设置bit 1 */
vPrintString( "Bit setting task -\t about to set bit 1.\r\n" );
xEventGroupSetBits( xEventGroup, mainSECOND_TASK_BIT );
}
}
/* ISR */
static uint32_t ulEventBitSettingISR( void )
{
static const char *pcString = "Bit setting ISR -\t about to set bit 2.\r\n";
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* ISR中输出提示信息 */
xTimerPendFunctionCallFromISR( vPrintStringFromDaemonTask,
( void * ) pcString,
0,
&xHigherPriorityTaskWoken );
/* 设置bit 2 */
xEventGroupSetBitsFromISR( xEventGroup, mainISR_BIT, &xHigherPriorityTaskWoken );
/* 根据xHigherPriorityTaskWoken判断是否需要调度程序 */
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
/* 获取事件位的任务 */
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" );
}
}
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 );
vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulEventBitSettingISR );
vTaskStartScheduler();
for( ;; );
return 0;
}
当xEventGroupWaitBits()中xWaitForAllBits设置为pdFALSE(示例中)的运行结果:
当xEventGroupWaitBits()中xWaitForAllBits设置为pdTRUE的运行结果:
使用事件组同步任务
引入
有时,应用程序需要两个或多个任务彼此同步。例如,任务A接收事件,将事件所需的一些处理委托给任务B、任务C、任务D三个任务,如果任务A在其他三个任务没有完成当前事件的处理时无法接收下一个事件,此时四个任务就需要彼此同步。每个任务执行到同步点后将在此等待其他任务完成处理并到达相应的同步点后才能继续执行,如此处的任务A只能在其他任务都达到同步点后才能接收另一个事件。
事件组可用于创建同步点,并同步多个任务:
- 必须为每个参与同步的任务分配唯一的事件位。
- 每个任务在到达同步点时设置自己的事件位。
- 设置自己的事件位后,事件组上的每个任务都会阻塞,以等待代表其他同步任务的事件位被设置。
但是在这个方案中不能使用xEventGroupSetBits()和xEventGroupWaitBits()函数。如果使用它们,那么设置一个bit(表示任务已达到同步点)和测试bits(确定其他任务是否已到达同步点)将会是两个单独的操作。例如:
- 任务A和任务B已到达同步点,因此它们的事件位被设置,并且它们都处于阻塞态等待任务C到达同步点。
- 任务C到达同步点,并调用xEventGroupSetBits()设置事件组中的事件位,一旦设置了任务C的事件位,任务A和任务B就会离开阻塞态,并清除这三个事件位。
- 任务C调用xEventGroupWaitBits()等待三个事件位,但那时,三个这三个事件位已被清除,任务A和任务B已经离开了各自的同步点,因此同步失败。
要成功使用事件组来创建同步点,事件位的设置以及事件位的测试必须作为单个不间断操作执行,为此,FreeRTOS提供了xEventGroupSync()函数。
xEventGroupSync()
xEventGroupSync()提供使用事件组彼此同步两个或多个任务的功能。该函数作为单一的操作允许任务在事件组中设置一个或多个事件位,然后等待事件位在同一事件组中被设置。它的原型如下:
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );
参数:
- xEventGroup
事件组的句柄。 - uxBitsToSet
它是一个位掩码,指定在事件组中要设置为1的事件位。将uxBitsToSet的值与现有值按位或来更新事件组的值。 - uxBitsToWaitFor
它也是一个位掩码,指定要在事件组中测试的事件位。 - xTicksToWait
等待的超时时间。
返回值:
满足调用任务解除阻塞条件时,返回值是满足调用任务解除阻塞条件时(在任何位被自动清零前)的事件组的值。如果是因为超时而返回,则返回值是超时时间到达时事件组的值,此时,该值将不符合任务解除阻塞条件。
示例
/* 定义事件组事件位的意义 */
#define mainFIRST_TASK_BIT ( 1UL << 0UL )
#define mainSECOND_TASK_BIT( 1UL << 1UL )
#define mainTHIRD_TASK_BIT ( 1UL << 2UL )
/* 事件组句柄 */
EventGroupHandle_t xEventGroup;
/* 任务函数 */
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 );
uxThisTasksSyncBit = ( EventBits_t ) pvParameters;
for( ;; )
{
/* 随机的延时,防止单个任务一直同时到达同步点(用于模拟每个任务处理事件的时间不同) */
xDelayTime = ( rand() % xMaxDelay ) + xMinDelay;
vTaskDelay( xDelayTime );
vPrintTwoStrings( pcTaskGetTaskName( NULL ), "reached sync point" );
/* 等待同步 */
xEventGroupSync( xEventGroup,
uxThisTasksSyncBit,/* 表示此任务到达同步点设置的位 */
uxAllSyncBits, /* 需要等待同步的位 */
portMAX_DELAY );
vPrintTwoStrings( pcTaskGetTaskName( NULL ), "exited sync point" );
}
}
int main( void )
{
/* 创建用于同步的事件组 */
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;
}
运行结果: