
FreeRTOS event groups are a powerful synchronization primitive for coordinating tasks and interrupt service routines (ISRs). However, their 24-bit internal counter can overflow when too many events are set without being cleared, leading to lost events and unpredictable system behavior. This article explains the root cause of event group overflow, provides detection techniques, and offers a practical solution with a thread-safe wrapper implementation.
Consider a scenario where an ISR sets event group bits at a high frequency (e.g., every microsecond from a timer interrupt), while a task waits for specific bit patterns. Over time, the cumulative number of bits set can exceed 2^24-1 (16,777,215), causing the event group value to wrap around to zero. This overflow resets the event group state, making the task miss events that were set after the overflow point.
FreeRTOS stores event group flags in a 24-bit integer (EventBits_t). Each bit set via xEventGroupSetBits() increments the internal representation. When all 24 bits are set (value 16,777,215), the next bit-set operation causes an integer overflow, resetting the value to zero. This behavior is documented in the FreeRTOS API reference but often overlooked in high-frequency interrupt scenarios.
The overflow condition occurs when:
Total events set since last clear >= 2^24
In systems with frequent interrupts (e.g., 1MHz timer ISR setting one bit per interrupt), overflow can occur in under 17 seconds. Even lower-frequency interrupts accumulate over hours or days, making this a latent reliability issue.
Early detection prevents field failures. Implement these checks in your application:
xEventGroupSetBits() that validates pre- and post-operation values.Example detection code:
void vCheckEventGroupOverflow(EventGroupHandle_t xEventGroup){EventBits_t uxCurrent = xEventGroupGetBits(xEventGroup);if (uxCurrent > 0xFFFFF0) { // Within 255 bits of maxlog_warning("Event group near overflow: %lu", (unsigned long)uxCurrent);}}
The most robust solution combines three strategies:
Here’s a complete wrapper implementation:
#include "FreeRTOS.h"#include "event_groups.h"#define SAFE_EVENT_GROUP_MAX 0xFFFFF0 // Leave room for 255 bits#define EVENT_GROUP_CHECK_INTERVAL_MS 1000typedef struct {EventGroupHandle_t xEventGroup;StaticEventGroup_t xEventGroupBuffer;TimerHandle_t xClearTimer;} SafeEventGroup_t;/* Forward declaration */static void vClearEventGroupTimerCallback(TimerHandle_t xTimer);/* Create a safe event group */SafeEventGroup_t* xSafeEventGroupCreate(void){SafeEventGroup_t* pxSafeGroup = pvPortMalloc(sizeof(SafeEventGroup_t));if (pxSafeGroup != NULL) {pxSafeGroup->xEventGroup = xEventGroupCreateStatic(&(pxSafeGroup->xEventGroupBuffer));pxSafeGroup->xClearTimer = xTimerCreateStatic("EventGroupClear",pdMS_TO_TICKS(EVENT_GROUP_CHECK_INTERVAL_MS),pdTRUE,(void*)pxSafeGroup,vClearEventGroupTimerCallback,&(pxSafeGroup->xClearTimerBuffer));if (pxSafeGroup->xEventGroup == NULL || pxSafeGroup->xClearTimer == NULL) {vPortFree(pxSafeGroup);return NULL;}xTimerStart(pxSafeGroup->xClearTimer, 0);}return pxSafeGroup;}/* Safely set bits from ISR or task */EventBits_t xSafeEventGroupSetBits(SafeEventGroup_t* pxSafeGroup, const EventBits_t uxBitsToSet){EventBits_t uxReturn;UBaseType_t uxSavedInterruptStatus;/* Enter critical section to protect the read-modify-write operation */uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();{EventBits_t uxCurrent = xEventGroupGetBits(pxSafeGroup->xEventGroup);EventBits_t uxNew = uxCurrent | uxBitsToSet;/* Check for potential overflow */if (uxNew > SAFE_EVENT_GROUP_MAX) {/* Clear the group before setting new bits to prevent overflow */xEventGroupClearBits(pxSafeGroup->xEventGroup, 0xFFFFFFFF);uxNew = uxBitsToSet; // Only set the requested bits after clear}uxReturn = xEventGroupSetBitsFromISR(pxSafeGroup->xEventGroup, uxBitsToSet);}portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);return uxReturn;}/* Safely wait for bits */EventBits_t xSafeEventGroupWaitBits(SafeEventGroup_t* pxSafeGroup,const EventBits_t uxBitsToWaitFor,const BaseType_t xClearOnExit,const BaseType_t xWaitForAllBits,TickType_t xTicksToWait){return xEventGroupWaitBits(pxSafeGroup->xEventGroup,uxBitsToWaitFor,xClearOnExit,xWaitForAllBits,xTicksToWait);}/* Timer callback to periodically check and clear if needed */static void vClearEventGroupTimerCallback(TimerHandle_t xTimer){SafeEventGroup_t* pxSafeGroup = (SafeEventGroup_t*)pvTimerGetTimerID(xTimer);if (pxSafeGroup != NULL) {EventBits_t uxCurrent = xEventGroupGetBits(pxSafeGroup->xEventGroup);if (uxCurrent > SAFE_EVENT_GROUP_MAX) {xEventGroupClearBits(pxSafeGroup->xEventGroup, 0xFFFFFFFF);}}}/* Delete the safe event group */void vSafeEventGroupDelete(SafeEventGroup_t* pxSafeGroup){if (pxSafeGroup != NULL) {xTimerDelete(pxSafeGroup->xClearTimer, 0);vEventGroupDelete(pxSafeGroup->xEventGroup);vPortFree(pxSafeGroup);}}
Here’s how to use the wrapper in a 1MHz timer ISR scenario:
/* Global safe event group handle */SafeEventGroup_t* xTimerEventGroup = NULL;/* Timer ISR prototype */void vTimer1ISR(void);void main(void){/* Initialize hardware */prvSetupHardware();/* Create safe event group for timer events */xTimerEventGroup = xSafeEventGroupCreate();configASSERT(xTimerEventGroup);/* Start timer ISR (1MHz frequency) */vSetupTimer1ISR(vTimer1ISR);/* Start scheduler */vTaskStartScheduler();for(;;);}void vTimer1ISR(void){BaseType_t xHigherPriorityTaskWoken = pdFALSE;/* Set bit 0 in the safe event group from ISR */xSafeEventGroupSetBits(xTimerEventGroup, 0x01);/* Clear interrupt flag */CLEAR_TIMER1_INTERRUPT_FLAG();/* Yield if needed */portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}void vTimerTask(void* pvParameters){const EventBits_t uxWaitBits = 0x01; // Wait for timer bitconst TickType_t xBlockTime = pdMS_TO_TICKS(10);for(;;) {/* Wait for timer event */xSafeEventGroupWaitBits(xTimerEventGroup,uxWaitBits,pdTRUE, // Clear on exitpdFALSE, // Don't wait for all bitsxBlockTime);/* Process timer event */vProcessTimerEvent();}}
Verify your implementation with these tests:
FreeRTOS event group overflow is a latent reliability issue in high-frequency interrupt systems. By understanding the 24-bit counter limitation, implementing detection mechanisms, and using a thread-safe wrapper with periodic clearing, you can prevent lost events and ensure robust task-ISR communication. The provided wrapper implementation offers a drop-in replacement for standard event group operations with built-in overflow protection.
Quick Links
Legal Stuff





