10. Event Groups
An Event Group is another inter-task synchronization kernel object that offers his own set of particular features:
An Event Group is, as you can guess after its name, a group of binary 'flags' (0 or 1). Each flag is able to work pretty much like a binary semaphore. When your application requires several binary semaphores, it is worth considering the use of a single event group instead. It would save memory...
In addition, with Event Groups, you can have a task waiting for a combination of events to occur (not only one), each coming from either one task, or separate tasks.
Several tasks can wait for the same combination of events and get unblocked altogether when that combination occur. Of course, the first waiting task to get active is the one of highest priority level.
Unlike binary semaphores, you can get unblocked when some flags (or a combination of them) are set without clearing them upon success, leaving those flags sets for other tasks to synchronize.
Basically, a Event Group is a 16 or 32 bit register where each bit is a flag that represents the state On/Off of a user-defined event. By using usual masking techniques, a task can wait for a number of flags to be set with either an AND-type or an OR-type filter. In addition, the waiting task may choose to reset or not the awaited event flags after reading.
Let us go thru a few practical examples.
1. Simultaneous activations
Edit the main() function so that it simply creates an Event Group and then starts 3 tasks:
/*
* main.c
*
* Created on: 28/02/2018
* Author: Laurent
*/
#include "main.h"
// Static functions
static void SystemClock_Config (void);
// FreeRTOS tasks
void vTask1 (void *pvParameters);
void vTask2 (void *pvParameters);
void vTask3 (void *pvParameters);
// Kernel objects
EventGroupHandle_t myEventGroup; // <-- Declare Event Group here
// Define Event Group flags
#define BIT0 ( (EventBits_t)( 0x01 <<0) ) // This is not mandatory but it provides
#define BIT1 ( (EventBits_t)( 0x01 <<1) ) // friendly alias for individual event
// Trace User Events Channels
traceString ue1;
// Main function
int main()
{
// Configure System Clock
SystemClock_Config();
// Initialize LED pin
BSP_LED_Init();
// Initialize Debug Console
BSP_Console_Init();
// Start Trace Recording
xTraceEnable(TRC_START);
// Create Event Group // <-- Create Event Group here
myEventGroup = xEventGroupCreate();
// Register the Trace User Event Channels
ue1 = xTraceRegisterString("state");
// Create Tasks
xTaskCreate(vTask1, "Task_1", 256, NULL, 1, NULL);
xTaskCreate(vTask2, "Task_2", 256, NULL, 2, NULL);
xTaskCreate(vTask3, "Task_3", 256, NULL, 3, NULL);
// Start the Scheduler
vTaskStartScheduler();
while(1)
{
// The program should never be here...
}
}
Task_1 is a state machine that sets the two event bits [BIT1 BIT0] in a sequence changing every 2ms { [0 0] , [x 1] , [1 x] , [1 1] } ('x' means 'leave current value as it is now').
/*
* Task_1 - State machine
*/
void vTask1 (void *pvParameters)
{
uint8_t state;
state = 0;
while(1)
{
// LED toggle
BSP_LED_Toggle();
switch(state)
{
case 0:
{
vTracePrintF(ue1, "%d", state);
xEventGroupClearBits(myEventGroup, BIT0 | BIT1); // [0 0]
state = 1;
break;
}
case 1:
{
vTracePrintF(ue1, "%d", state);
xEventGroupSetBits(myEventGroup, BIT0); // [x 1]
state = 2;
break;
}
case 2:
{
vTracePrintF(ue1, "%d", state);
xEventGroupSetBits(myEventGroup, BIT1); // [1 x]
state = 3;
break;
}
case 3:
{
vTracePrintF(ue1, "%d", state);
xEventGroupSetBits(myEventGroup, BIT0 | BIT1); // [1 1]
state = 0;
break;
}
}
// Wait for 2ms
vTaskDelay(2);
}
}
Before writing the receiving tasks Task_2 and Task_3, let us have a look on the xEventGroupWaitBits() waiting function prototype:
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
So we have :
Argument #1 : the name of the Event Group to use.
Argument #2 : a 32-bit mask with 'ones' for the bits to wait for, and 'zeros' everywhere else.
Argument #3 : 1 (pdTRUE) for clearing bits upon success, or 0 (pdFALSE) to leave them set.
Argument #3 : When two or more bits are awaited, 0 (pdFALSE) here applies an OR logic. Set to 1 (pdTRUE) to apply an AND logic.
Argument #4 : The timeout, same as for other kernel objects. You can choose to keep going if the timeout elapsed without any success in getting the awaited bits.
Then, Task_2 and Task_3 are both :
Waiting for an infinite time for BIT0 event flag to be set.
Clearing the BIT0 flag after reading.
Set to wait for all bits (which is nonsense here since there's only one bit to wait for).
/*
* Task_2
*/
void vTask2 (void *pvParameters)
{
while(1)
{
// Wait for myEventGroup :
// - bit #0
// - Clear on Exit
// - Wait for All bits (AND)
xEventGroupWaitBits(myEventGroup, BIT0, pdTRUE, pdTRUE, portMAX_DELAY);
// If the bit is set
my_printf("#");
}
}
/*
* Task 3
*/
void vTask3 (void *pvParameters)
{
while(1)
{
// Wait for myEventGroup
// - bit #0
// - Clear on Exit
// - Wait for All bits (AND)
xEventGroupWaitBits(myEventGroup, BIT0, pdTRUE, pdTRUE, portMAX_DELAY);
// If the bit is set
my_printf("-");
}
}
The main idea behind this demo is to demonstrate the ability of the Event Group to activate several tasks upon the same source event.
Before building, make sure that Event Group Events recording is enabled in trcKernelPortConfig.h file:
/**
* @def TRC_CFG_INCLUDE_EVENT_GROUP_EVENTS
* @brief Macro which should be defined as either zero (0) or one (1).
*
* If this is zero (0), the trace will exclude any "event group" events.
*
* Default value is 0 (excluded) since dependent on event_groups.c
*/
#define TRC_CFG_INCLUDE_EVENT_GROUP_EVENTS 1
Run the application. The debug console should confirm that both Task_2 and Task_3 enter active state periodically:
Then take a look at the recorded trace. As expected, both Task_3 and Task_2 are activated every time BIT0 is set by Task_1 (in states 1 and 3). One can note that even if Task_3 clears BIT0 as soon as is it read, we still got the correct activation of Task_2. Actually, both tasks entered the ready state altogether as soon as BIT0 was set by Task_1.
- Commit name "Event group activates two tasks" - Push onto Gitlab |
2. Not clearing event bits
Let us experiment the same but without clearing the event bit after reading in Task_2 and Task_3:
/*
* Task_2
*/
void vTask2 (void *pvParameters)
{
while(1)
{
// Wait for myEventGroup :
// - bit #0
// - Do not Clear on Exit
// - Wait for All bits (AND)
xEventGroupWaitBits(myEventGroup, BIT0, pdFALSE, pdTRUE, portMAX_DELAY);
// If the bit is set
my_printf("#");
}
}
/*
* Task 3
*/
void vTask3 (void *pvParameters)
{
while(1)
{
// Wait for myEventGroup
// - bit #0
// - Do not Clear on Exit
// - Wait for All bits (AND)
xEventGroupWaitBits(myEventGroup, BIT0, pdFALSE, pdTRUE, portMAX_DELAY);
// If the bit is set
my_printf("-");
}
}
The console shows that only Task_3 executes:
And the trace confirms. Because the event bit BIT0 is not cleared after Task_3 reading, the wait mechanism in Task_3 is broken. And because Task_3 has highest priority level, no other task can get active anymore.
Note that to get the above trace, you have to turn the snapshot mode to TRC_SNAPSHOT_MODE_STOP_WHEN_FULL. because as soon as the program loops into Task_3, starving all other tasks, the recorder can't get information anymore.
3. Waiting for two events (OR/AND)
Now try the following:
/*
* Task_2
*/
void vTask2 (void *pvParameters)
{
while(1)
{
// Wait for myEventGroup :
// - bit #0, bit #1
// - Clear on Exit
// - Wait for All bits (AND)
xEventGroupWaitBits(myEventGroup, (BIT0 | BIT1), pdTRUE, pdTRUE, portMAX_DELAY);
// If the bit is set
my_printf("#");
}
}
/*
* Task 3
*/
void vTask3 (void *pvParameters)
{
while(1)
{
// Wait for myEventGroup
// - bit #0, bit #1
// - Clear on Exit
// - Do not Wait for All bits (OR)
xEventGroupWaitBits(myEventGroup, (BIT0 | BIT1), pdTRUE, pdFALSE, portMAX_DELAY);
// If the bit is set
my_printf("-");
}
}
In the above code, Task_2 is activated only if both BIT0 AND BIT1 are set. Task_3 is activated whenever BIT0 OR BIT1 is set (and obviously when both are set).
As a result:
- Commit name "Event group wait for two bits" - Push onto Gitlab |
4. Wait function return value
Back to the beginning of this tutorial, you may recall that the return value of the xEventGroupWaitBits() function is of EventBits_t type, which is an alias for uint32_t. The return value is not Boolean (pdPass/pdFail) as it is for binary semaphores, mutexes of message queues. The return value of the xEventGroupWaitBits() function is the actual value of the event bits inside the group (before any "clear on exit" occurs ).
In the previous example, Task_3 is activated when either BIT0 or BIT1 is set. We can now test the return value so that different actions can be taken upon result:
/*
* Task 3
*/
void vTask3 (void *pvParameters)
{
EventBits_t evb_result, evb_msk;
// Prepare a mask for testing event bits
evb_msk = BIT1|BIT0;
while(1)
{
// Wait for myEventGroup
// - bit #0, bit #1
// - Clear on Exit
// - Do not Wait for All bits (OR)
evb_result = xEventGroupWaitBits(myEventGroup, (BIT0 | BIT1), pdTRUE, pdFALSE, portMAX_DELAY);
// If BIT0 is set
if ((evb_result & evb_msk) == BIT0) my_printf("[0]");
// If BIT1 is set
if ((evb_result & evb_msk) == BIT1) my_printf("[1]");
// If both BIT0 and BIT1 are set
if ((evb_result & evb_msk) == evb_msk) my_printf("[A]\r\n");
}
}
And the result:
- Commit name "Event group return value" - Push onto Gitlab |
Illustrating all possible use cases of Event Groups would take long. Based on the above example, you're hopefully now able to imagine synchronization schemes fulfilling your application needs.