Event Flag Synchronization Patterns in Real-Time Operating Systems
Event flags (or event groups) provide bitmap-based synchronization primitives enabling complex coordination patterns between multiple producers and consumers. Unlike counting semaphores, setting an already active flag does not queue additional events; the state remains binary until explicitly cleared. This characteristic makes them ideal for signaling asynchronous hardware occurrences—such as interrupts from peripherals—where the occurrence count matters less than the binary state transition.
Core Mechanisms
Each flag group manages a 32-bit bitmask where individual bits represent distinct events. Threads can wait for combinations using logical OR (any flag) or AND (all flags) conditions. When a thread invokes a wait operation, it can specify whether successful matches automatically clear the corresponding bits or leave them intact for manual cleanup.
Key CMSIS-RTOS2 interfaces include:
osEventFlagsId_t osEventFlagsNew(const osEventFlagsAttr_t *attr);
Creates a new flag group. Returns a handle for subsequent operations.
uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags);
Atomically sets one or more bits (e.g., 0x05 sets bits 0 and 2). If threads are waiting on these bits, they become eligible for scheduling according to their wait criteria.
uint32_t osEventFlagsClear(osEventFlagsId_t ef_id, uint32_t flags);
Explicitly clears specified bits, typically after processing the associated event.
uint32_t osEventFlagsWait(osEventFlagsId_t ef_id,
uint32_t flags,
uint32_t options,
uint32_t timeout);
Blocks until the specified flags meet the condition defined in options. Common options include osFlagsWaitAny (trigger if any bit matches) and osFlagsWaitAll (trigger only if all bits match). Adding osFlagsNoClear requires manual clearing via osEventFlagsClear, which is essential when multiple processing stages must observe the same event.
Practical Implementation: Interrupt-Driven Button Handling
Consider a scenario where a mechanical button connects to a GPIO line configured for falling-edge interrupts. The ISR signals the event, while a dedicated task handles debouncing and action execution.
Hardware Configuration
Configure the GPIO for interrupt generation on the falling edge:
#define USER_BUTTON_PIN GPIO_PIN_0
#define USER_BUTTON_PORT GPIOA
#define BTN_FLAG_PRESSED 0x01U
static osEventFlagsId_t hButtonEvent;
void ConfigureButtonInterrupt(void)
{
GPIO_InitTypeDef cfg = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
cfg.Pin = USER_BUTTON_PIN;
cfg.Mode = GPIO_MODE_IT_FALLING;
cfg.Pull = GPIO_PULLUP;
HAL_GPIO_Init(USER_BUTTON_PORT, &cfg);
HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
uint8_t IsButtonStillPressed(void)
{
return (HAL_GPIO_ReadPin(USER_BUTTON_PORT, USER_BUTTON_PIN) == GPIO_PIN_RESET);
}
Interrupt Service Routine
Keep the ISR minimal—defer processing to the thread context:
void HAL_GPIO_EXTI_Callback(uint16_t pin)
{
if (pin == USER_BUTTON_PIN)
{
osEventFlagsSet(hButtonEvent, BTN_FLAG_PRESSED);
}
}
Processing Task
The task remains blocked indefinitely, consuming no CPU until the ISR signals an event. Upon waking, it validates the button state after a debounce interval:
void vButtonMonitorTask(void *pvParams)
{
(void)pvParams;
uint32_t notifiedFlags;
hButtonEvent = osEventFlagsNew(NULL);
ConfigureButtonInterrupt();
for (;;)
{
notifiedFlags = osEventFlagsWait(
hButtonEvent,
BTN_FLAG_PRESSED,
osFlagsWaitAny | osFlagsNoClear,
osWaitForever
);
if (notifiedFlags & BTN_FLAG_PRESSED)
{
osDelay(20); // Mechanical debounce
if (IsButtonStillPressed())
{
ExecuteButtonCommand();
}
osEventFlagsClear(hButtonEvent, BTN_FLAG_PRESSED);
}
}
}
Using osFlagsNoClear ensures the flag persists during the debounce window. If the task were to clear the flag automatically upon wake, a rapid second interrupt occurring during debounce might be lost. Manual clearing after validation guarantees that only processed events are removed from the pending state.