之前已经学过两个任务之间可以利用信号量、队列来通信,任务可以利用这两个机制等待某一个事件发生,但是假如需要等待多个事件发生呢?这就需要用到事件组了。
事件组可以让任务进入阻塞态,等待一个或多个事件的组合发生。当事件发生时,事件组将正在等待同一事件或事件组合的所有任务解除阻塞。
事件组的特性使它们对于同步多个任务、向多个任务广播事件、允许任务在阻塞态下等待一组事件中的任何一个发生,以及允许任务在阻塞态下等待多个操作完成非常有用。通常可以用单个事件组替换多个二进制信号量,所以可以减少RAM的使用。
事件组功能是可选的。要包含事件组功能,构建FreeRTOS时要包含源文件event_groups.c。
目录
1.事件组的特性
2.如何使用事件组
2.1创建事件组
2.2设置事件位
2.3等待事件位
3.使用事件组同步任务
1.事件组的特性
事件标志(event flags)和事件位(event bits)
事件标志是一个布尔值(1或0),用于指示事件是否发生。事件组其实就是一组事件标志。
一个事件标志只能是1或0,将一个事件标志的状态存储在一个位中,一个事件组中所有事件标志的状态存储在一个变量中。
事件组中每个事件标志的状态由EventBits_t类型变量中的单个位表示。因此,事件标志也被称为事件“位”。如果EventBits_t变量中的一个位被设置为1,那么该位表示的事件已经发生。如果EventBits_t变量中的一个位被设置为0,那么该位表示的事件还没有发生。
事件组中的事件位数取决于FreeRTOSConfig.h1中的configUSE_16_BIT_TICKS
如果configUSE_16_BIT_TICKS为1,则每个事件组包含8个可用的事件位。
如果configUSE_16_BIT_TICKS为0,则每个事件组包含24个可用的事件位。
使用事件组的实际例子:
FreeRTOS+TCP TCP/IP栈的实现提供了如何使用事件组来同时设计和最小化资源使用的实际示例。
TCP套接字必须响应许多不同的事件。包括接受事件(accept)、绑定事件(bind)、读取事件(read)和关闭事件(close)。套接字可以响应事件取决于套接字的状态。例如,如果套接字已经创建,但还没有绑定到地址,那么它可以接收绑定事件,但不会接收读取事件(如果没有地址,它就不能读取数据)。
FreeRTOS+TCP套接字的状态保存在一个名为FreeRTOS_Socket_t的结构中。该结构包含一个事件组,该事件组为套接字必须处理的每个事件定义了一个事件位。FreeRTOS+TCP API调用该块来等待事件或事件组,在事件组上阻塞。事件组还包含一个“abort”位,允许TCP连接被终止,无论套接字当时正在等待哪个事件。
2.如何使用事件组
2.1创建事件组
事件组是EventGroupHandle_t类型的变量。xEventGroupCreate() API函数用于创建事件组,并返回一个EventGroupHandle_t。
EventGroupHandle_t xEventGroupCreate( void );
2.2设置事件位
xEventGroupSetBits() API函数可以在事件组中设置一个或多个位,用于通知任务所设置的位表示的事件已经发生。
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
参数 | 作用 |
xEventGroup | 要设置位的事件组的句柄。 |
uxBitsToSet | 指定事件组中要设置为1的事件位或事件位的位掩码。事件组的值通过将事件组的现有值与uxBitsToSet中传递的值按位或来更新。 例如,将uxBitsToSet设置为0x04(二进制0100)将导致事件组中的事件位3被设置,而事件组中的所有其他事件位保持不变。 |
返回值 | 返回调用xEventGroupSetBits()时事件组的值。注意,返回值不一定包含uxBitsToSet指定的位,因为这些位可能已经被不同的任务再次清除。 |
xEventGroupSetBitsFromISR()是xEventGroupSetBits()的中断安全版本。
给出信号量是一种确定性操作,因为预先知道给出信号量最多会导致一个任务离开阻塞态。但是当在事件组中设置位时,预先不知道有多少任务将离开阻塞状态,因此在事件组中设置位不是一个确定性操作。
FreeRTOS设计和实现标准不允许在中断服务例程中或中断被禁用时执行非确定性操作。因此,xEventGroupSetBitsFromISR()不直接在中断服务例程中设置事件位,而是将操作推迟到RTOS守护任务。
不了解守护任务的可以看这篇:FreeRTOS全解析-7.中断安全API和推迟中断处理
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken );
参数 | 作用 |
xEventGroup | 要设置位的事件组的句柄。 |
uxBitsToSet | 指定事件组中要设置为1的事件位或事件位的位掩码。 |
pxHigherPriorityTaskWoken | xEventGroupSetBitsFromISR()不直接在中断服务例程中设置事件位,而是通过在timer命令队列上发送命令将操作推迟到RTOS守护进程任务。如果守护任务处于阻塞态,等待timer命令队列上的数据可用,则写入timer命令队列将使守护任务离开阻塞态。如果守护任务的优先级高于当前正在执行的任务(被中断的任务)的优先级,那么,在内部,xEventGroupSetBitsFromISR()将把* pxhigherprioritytaskkoken设置为pdTRUE。 如果xEventGroupSetBitsFromISR()值为pdTRUE,则应该在中断退出之前执行上下文切换。这将确保中断直接返回到守护进程任务,因为守护进程任务将是最高优先级的就绪态任务。 还是不会用的可以看这篇FreeRTOS全解析-7.中断安全API和推迟中断处理 |
返回值 | 1.pdPASS,数据成功发送到timer命令队列。 2. pdFALSE,如果'set bits'命令不能写入定时器命令队列,因为队列已满,则返回pdFALSE。 |
2.3等待事件位
xEventGroupWaitBits() API函数允许任务读取事件组的值,如果事件组中的一个或多个事件位尚未设置,则可以选择在阻塞态下等待事件组中的一个或多个事件位设置完成。
EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
uxBitsToWaitFor和xWaitForAllBits参数值的组合决定了调用xEventGroupWaitBits() 的任务,是否要进入阻塞态等待。
uxBitsToWaitFor指定要测试事件组中的事件位;
xWaitForAllBits指定按位或测试或按位与测试。pdTRUE表示uxBitsToWaitFor中指定的位都被设置,才会解除阻塞,pdFALSE表示有一个设置了就行。
例子:
事件组的值 | uxBitsToWaitFor | xWaitForAllBits | 结果 |
0000 | 0101 | pdFALSE | 进入阻塞态,因为位0和2都没有设置 |
0100 | 0101 | pdTRUE | 进入阻塞态,位0没有设置,位2设置了,但pdTRUE表示两个都得被设置 |
0100 | 0110 | pdFALSE | 不会阻塞,位1没有设置,位2设置了,pdFALSE表示有一个设置了就行 |
0100 | 0110 | pdTRUE | 进入阻塞态,位1没有设置,位2设置了,pdTRUE表示都得设置 |
xClearOnExit被设置为pdTRUE,那么在调用任务退出xEventGroupWaitBits() API函数之前,uxBitsToWaitFor指定的事件位将被清除回0。如果xClearOnExit被设置为pdFALSE,那么事件组中事件位的状态不会被xEventGroupWaitBits() API函数修改。
事件位还可以使用xEventGroupClearBits() API函数手动清除,手动清除要注意访问冲突问题,如果xClearOnExit设置为pdTRUE,则事件位的测试和清除将显示在将任务调用为原子操作(不可被其他任务或中断中断)。
3.使用事件组同步任务
一个任务等待多个事件很简单,比如A任务的操作需要B、C、D任务中的某个操作,那A任务调用xEventGroupWaitBits()去等待,当B、C、D中操作完成了,B、C、D分别调用一下xEventGroupSetBits(),都调用后A任务就会继续运行。
但是有时需要两个或多个任务相互同步。例如:
任务A接收一个事件,然后将该事件所需的一些处理委托给其他三个任务:任务B、任务C和任务D(也就是说,没有A就没有BCD)。如果任务A在任务B、C和D都完成处理之前不能接收另一个事件(也就是说没有BCD就没有A),那么这四个任务将需要彼此同步。每个任务的同步点在该任务完成处理后,并且在其他每个任务完成处理之前不能继续进行。任务A只有在所有四个任务都达到同步点后才能接收另一个事件。
事件组用来创建同步点:
必须参与同步的每个任务在事件组中被分配一个唯一的事件位。
每个任务在到达同步点时设置自己的事件位。
在设置了自己的事件位之后,每个任务都阻塞在事件组上,等待其他同步任务的事件位也设置好。
但是,xEventGroupSetBits()和xEventGroupWaitBits() API函数不能在此场景中使用。如果使用了它们,那么位的设置(表示任务已达到其同步点)和位的测试(确定其他同步任务是否已达到其同步点)将作为两个独立的操作执行。要明白为什么这是一个问题,考虑一个场景,任务A,任务B和任务C尝试使用事件组进行同步:
1.任务A和任务B已经到达同步点,所以它们的事件位在事件组中设置,它们处于阻塞态,等待任务C的事件位也设置完成。
2. 任务C到达同步点,使用xEventGroupSetBits()设置其在事件组中的位。只要任务C的位设置好,任务A和任务B就会离开阻塞状态,并清除所有三个事件位。
3.然后任务C调用xEventGroupWaitBits()来等待所有三个事件位都设置好,但是到那时,所有三个事件位都已经被清除,任务A和任务B已经离开了各自的同步点,因此同步失败。
要成功地使用事件组创建同步点,事件位的设置和随后的事件位测试必须作为单个不可中断操作执行,就要用到xEventGroupSync() API函数。
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );
参数意思和xEventGroupWaitBits() 是一样的。
如果xEventGroupSync() 是因为满足了解除阻塞条件而返回,返回前会自动清除相应的位。
返回值是事件组的值,满足条件返回时,虽然事件组相应的位会被清0,但是返回值是没清除0的值。因为等待超时返回的话,就是事件组的值。
例如:等待的位是1和2,当事件组为0110,即位1,和2都被设置为1了,也就是说符合解除阻塞条件了,函数就会返回这时,返回值就是0110.
等待的位是1和2,当事件组为0100,或者0000,或者其他不是0110的情况,并且已经超时了,函数就会返回这时,返回值就是为0100,或者0000,或者其他不是0110的情况.
一个完整例子:
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" );
}
}
EventGroupHandle_t xEventGroup;
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;
}
任务在到达同步点时打印"reached sync point",然后等待同步,同步后离开同步点,打印"exited sync point"
系列文章合集:
FreeRTOS文章合集
嵌入式Linux基础文章合集
ARM学习系列合集
C语言学习系列合集