9. Event Groups

 

Event Group are another inter-task synchronization objects that offer additional features compared to semaphores:

  • With Event Group, you can have a task waiting for a combination of events to occur (not only one), each coming from a separate task
  • 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.

Using an event group can replace several binary semaphores, thus saving memory.

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 or not to reset the awaited event flags after reading.

Let us go thru a few practical examples.

 

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) )   // Not mandatory
#define BIT1	( (EventBits_t)( 0x01 <<1) )   // Provide friendly alias for individual event

// Trace User Events Channels
traceString ue1;

// Main program
int main()
{
	// Configure System Clock
	SystemClock_Config();

	// Initialize LED pin
	BSP_LED_Init();

	// Initialize Debug Console
	BSP_Console_Init();

	// Start Trace Recording
	vTraceEnable(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);
	}
}

 

Then, Task_2 and Task_3 are both waiting for BIT0 event flag to be set. Both are clearing the flag after reading, both are 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("-");
	}
}

 

Before building, make sure that Event Group Events recording is enabled in trcConfig.h file:

/*****************************************************************************
 * TRC_CFG_INCLUDE_EVENT_GROUP_EVENTS
 *
 * 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:

image_001.png
 
 
Then take a look at the recorded trace. As expected, both Task_3 and Task_2 are activated everytime 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 the activation of Task_2. Actually, both tasks entered the ready state altogether as soon as BIT0 was set by Task_1.
 
image_000.png
 
 
 
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:

image_002.png
 
 
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.
 
image_003.png
 
 
 
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:

image_004.png
 
image_005.png

 

Illustrating all possible use cases of Event Groups would take long. Based on the above example, you're now able to imagine synchronization schemes fulfilling your application needs.