FreeRTOS Event Groups Explained. Bit based synchronization in real systems
1. Introduction
In many embedded systems, tasks do not only wait for data. They wait for conditions. A network stack might need to wait until the link is up and an address is configured. A control task might need to wait until sensors are initialized and calibration is complete. A power management task might need to wait until all subsystems report that they are idle before entering a low power mode.
These situations are different from message passing. There is often no payload to transfer and no stream of work items to drain. What matters is whether the system has reached a certain state. FreeRTOS provides queues and semaphores, but those primitives are primarily about moving data or signaling individual events. When the problem is about representing and waiting on system state, they start to feel indirect and awkward.
This is the gap that Event Groups are meant to fill. They provide a way to represent multiple boolean conditions in a single object and to let tasks block until one or more of those conditions becomes true. To use them well, it helps to understand their bit based model, how they differ from queues and semaphores, and what kinds of synchronization problems they are designed to solve.
2.
What an Event Group is and why it exists
An Event Group is a kernel object that holds a set of bits. Each bit represents a condition or a state flag. A task can set bits, clear bits, or wait until a specific combination of bits is set. Unlike a queue, there is no data associated with each bit. Unlike a semaphore, there is no count. The Event Group simply represents shared state.
This model is useful when several parts of the system need to report progress or readiness, and other tasks need to wait until the system reaches a particular configuration. For example, one task might set a bit when hardware initialization is done, another task might set a bit when configuration is loaded, and a third task might wait until both bits are set before starting normal operation.
You can think of an Event Group as a small shared register managed by the kernel, with blocking semantics built around it. Tasks do not poll the bits in a loop. They ask the kernel to wake them when the required pattern appears. This keeps the code event driven.
It is also important to understand what an Event Group is not. It is not a message queue and it is not a work list. If the same event happens multiple times before a task wakes up, the bit is still just set. There is no history and no accumulation. Event Groups are about current state, not about counting events.
Figure 1. An Event Group represents several boolean conditions as bits that tasks can set, clear, and wait on.
3.
How Event Groups differ from queues and semaphores
Queues are designed to transfer data between contexts. Each send adds an item, and each receive removes one. Semaphores are designed to count resources or protect critical sections. Each give and take changes a count or a lock state. Both of these primitives model things that happen over time.
Event Groups model something different. They model conditions. A bit is either set or clear. Setting a bit does not enqueue anything and clearing a bit does not acknowledge anything. It simply changes the visible state of the system. Tasks that wait on an Event Group are not waiting for a message to arrive. They are waiting for the system to reach a particular state.
This difference matters for design. If you need to guarantee that every event is processed exactly once, an Event Group is the wrong tool. If you need to express that several independent initialization steps must all be complete before you proceed, an Event Group is often a better fit than a collection of semaphores or custom flags.
Figure 2. Queues and semaphores represent events over time, while Event Groups represent the current state of the system using bits.
4.
Bit based waiting and synchronization patterns
The main strength of Event Groups is the ability to wait on combinations of bits. A task can wait until any one of a set of bits is set, or until all of them are set. This lets you express simple synchronization barriers and state transitions directly in the kernel API.
A common pattern is a startup barrier. Several tasks perform initialization in parallel. Each one sets a bit when it finishes. Another task waits until all those bits are set before enabling normal operation. Another pattern is mode control, where different bits represent different system states and tasks wait for the specific state they care about before acting.
Event Groups also support clearing bits automatically when a wait condition is satisfied. This is useful when a bit represents a one time condition that should be consumed by the waiting task. Used carefully, this can model simple handshakes without introducing extra semaphores or queues.
At the same time, the bit model has limits. Because bits do not count, fast repeated events can collapse into a single set bit. If that loss of information matters, a queue or a semaphore is a better choice. Event Groups work best when the question is “is this condition true yet” rather than “how many times did this happen”.
Figure 3. A task can wait for any bit or for all bits in an Event Group, which makes it easy to express barriers and simple state transitions.
5.
Practical example. Waiting for system readiness
Consider a system that has three independent initialization steps. The hardware drivers must be ready, the configuration must be loaded, and the network link must be up. Each of these steps is handled by a different task. The main application task should not start until all three are complete.
With an Event Group, each task sets one bit when it finishes its part. The application task waits until all three bits are set. There is no polling and no need to chain semaphores together. The code expresses the design directly.
#define EVT_HW_READY (1U << 0) #define EVT_CFG_READY (1U << 1) #define EVT_NET_READY (1U << 2)
static EventGroupHandle_t sysEvents;
{ InitHardware(); xEventGroupSetBits(sysEvents, EVT_HW_READY); vTaskDelete(NULL); }
void CfgTask(void *arg) { LoadConfiguration(); xEventGroupSetBits(sysEvents, EVT_CFG_READY); vTaskDelete(NULL); }
void NetTask(void *arg) { BringUpNetwork(); xEventGroupSetBits(sysEvents, EVT_NET_READY); vTaskDelete(NULL); }
void AppTask(void *arg) { xEventGroupWaitBits(sysEvents, EVT_HW_READY | EVT_CFG_READY | EVT_NET_READY, pdFALSE, pdTRUE, portMAX_DELAY);
for (;;) { RunApplicationLoop(); } }
void AppInit(void) { sysEvents
= xEventGroupCreate(); xTaskCreate(HwInitTask, "hw", 512, NULL, 2, NULL); xTaskCreate(CfgTask, "cfg", 512, NULL, 2, NULL); xTaskCreate(NetTask, "net", 512, NULL, 2, NULL); xTaskCreate(AppTask, "app", 512, NULL, 1, NULL); } |
In this example, the application task blocks until all three bits are set. The fourth parameter to xEventGroupWaitBits selects whether the task waits for any bit or for all bits. Here, waiting for all bits expresses a simple and explicit startup barrier.
This works well because the information being modeled is state. Once hardware, configuration, and network are ready, they remain ready. There is no need to count how many times those events happened. There is only a need to know whether the system is ready to proceed.
Figure 4. Each initialization task sets one bit, and the application task waits until all required bits are set before starting.
6.
Conclusion
Event Groups provide a simple and efficient way to represent and wait on system state in FreeRTOS. By modeling conditions as bits and letting tasks block on combinations of those bits, they express common synchronization patterns such as startup barriers and mode transitions directly in the kernel.
They work best when the problem is about readiness, configuration, or mode rather than about streams of events. Used in that role, they reduce polling, simplify control flow, and make system level dependencies easier to see. Like Queue Sets, they are a precise tool for a specific class of problems, and they are most effective when they are used with a clear design intent.
