Table of Contents
KEY TAKEAWAYS
- Binary semaphores signal events between tasks or from ISR to task — they are the simplest synchronization primitive for “something happened” notifications.
- Event groups let a task wait for multiple conditions (ANY or ALL) simultaneously, enabling clean multi-source synchronization without complex flag polling.
- FreeRTOS task notifications are 45% faster than semaphores and use no additional RAM — ideal for simple one-to-one signaling.
Part of the Complete Guide to RTOS for Embedded Systems series.
Beyond queues (which transfer data), RTOS provides signaling mechanisms for coordinating between tasks without passing data. Semaphores, event groups, and task notifications each serve different use cases. This article explains when to use each one, with practical patterns for common embedded scenarios.
Binary Semaphores: ISR-to-Task Signaling
A binary semaphore is the simplest signaling mechanism. One side “gives” it (sets it to available), the other side “takes” it (blocks until available, then clears it). It’s binary — there’s no count; it’s either available or not.
The primary use case: ISR-to-task notification.
SemaphoreHandle_t xUartRxSem = xSemaphoreCreateBinary();// ISR — minimal work, signal the task void USART1_IRQHandler(void) { if (USART1->SR & USART_SR_RXNE) { rx_byte = USART1->DR; rx_buffer[rx_idx++] = rx_byte; if (rx_byte == ‘n’ || rx_idx >= RX_BUF_SIZE) { BaseType_t woken = pdFALSE; xSemaphoreGiveFromISR(xUartRxSem, &woken); portYIELD_FROM_ISR(woken); } } }
// Task — blocks efficiently until data is ready void vUartProcessTask(void *pvParameters) { for (;;) { xSemaphoreTake(xUartRxSem, portMAX_DELAY); // Zero CPU while waiting process_rx_buffer(rx_buffer, rx_idx); rx_idx = 0; } } “`
Important: if the ISR fires multiple times before the task processes, the semaphore only records that at least one event happened — it doesn’t count. If you need to count events, use a counting semaphore.
Binary semaphore vs mutex: they look similar but serve different purposes. Binary semaphores are for signaling (one task gives, another takes). Mutexes are for mutual exclusion (same task takes and gives). Mutexes have priority inheritance; binary semaphores do not.
Counting Semaphores: Resource Counting
A counting semaphore maintains a count that represents available resources. Each “take” decrements the count; each “give” increments it. When the count reaches zero, tasks block on “take.”
#define NUM_DMA_CHANNELS 4
SemaphoreHandle_t xDmaSem = xSemaphoreCreateCounting(NUM_DMA_CHANNELS, NUM_DMA_CHANNELS);void *acquire_dma_channel(uint32_t timeout_ms) { if (xSemaphoreTake(xDmaSem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE) { return find_free_dma_channel(); // Protected by the semaphore count } return NULL; // Timeout — all channels busy }
void release_dma_channel(void *channel) { mark_channel_free(channel); xSemaphoreGive(xDmaSem); } “`
Counting semaphores are also useful as event counters for ISRs:
SemaphoreHandle_t xButtonSem = xSemaphoreCreateCounting(10, 0); // Max 10 pending eventsvoid EXTI_IRQHandler(void) { // Each button press increments the count BaseType_t woken = pdFALSE; xSemaphoreGiveFromISR(xButtonSem, &woken); portYIELD_FROM_ISR(woken); }
void vButtonTask(void *pvParameters) { for (;;) { xSemaphoreTake(xButtonSem, portMAX_DELAY); // Processes each press individually, even if multiple queued handle_button_press(); } } “`
Event Groups: Multi-Source Synchronization
Event groups (event flags) are powerful when a task needs to wait for multiple conditions. Each bit in the event group represents a condition, and a task can wait for ANY combination (OR) or ALL bits (AND).
#define EVT_SENSOR_READY (1 << 0)
#define EVT_GPS_FIX (1 << 1)
#define EVT_NETWORK_UP (1 << 2)
#define EVT_CONFIG_LOADED (1 << 3)
#define ALL_INIT_EVENTS (EVT_SENSOR_READY | EVT_GPS_FIX | EVT_NETWORK_UP | EVT_CONFIG_LOADED)EventGroupHandle_t xInitGroup = xEventGroupCreate();
// Different tasks set their bits as they complete initialization void vSensorTask(void *pvParameters) { init_sensors(); xEventGroupSetBits(xInitGroup, EVT_SENSOR_READY); // Continue with normal operation… }
void vGpsTask(void *pvParameters) { wait_for_gps_fix(); xEventGroupSetBits(xInitGroup, EVT_GPS_FIX); // Continue… }
// Main task waits for ALL subsystems to initialize void vMainTask(void *pvParameters) { EventBits_t bits = xEventGroupWaitBits(xInitGroup, ALL_INIT_EVENTS, // Wait for all four bits pdFALSE, // Don’t clear bits pdTRUE, // Wait for ALL (AND) pdMS_TO_TICKS(30000)); // 30-second timeout
if ((bits & ALL_INIT_EVENTS) == ALL_INIT_EVENTS) { printf(“All subsystems initializedn”); start_normal_operation(); } else { printf(“Init timeout! Missing: 0x%Xn”, ALL_INIT_EVENTS & ~bits); enter_safe_mode(); } } “`
Event groups are ideal for:
- Synchronizing multiple tasks at a barrier point
- Implementing state machines that depend on multiple inputs
- System initialization coordination
- Watchdog health monitoring (each task sets its “alive” bit)
FreeRTOS Task Notifications: Lightweight and Fast
FreeRTOS task notifications are direct-to-task signals that don’t require creating a separate kernel object (semaphore, queue, or event group). Each task has a built-in 32-bit notification value and a pending state.
// Used as a binary semaphore replacement — 45% faster, zero RAM overhead
void EXTI_IRQHandler(void) {
BaseType_t woken = pdFALSE;
vTaskNotifyGiveFromISR(xProcessingTask, &woken);
portYIELD_FROM_ISR(woken);
}void vProcessingTask(void *pvParameters) { for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // Block until notified process_data(); } } “`
Task notifications can also replace event groups for simple cases:
// Setting specific bits (like event group)
xTaskNotify(xTargetTask, (1 << 2) | (1 << 5), eSetBits);// Waiting for specific bits uint32_t notification_value; xTaskNotifyWait(0, 0xFFFFFFFF, ¬ification_value, portMAX_DELAY); if (notification_value & (1 << 2)) { // Bit 2 was set } “`
Limitations of task notifications:
- Only one task can receive (no multiple waiters like semaphores)
- Can only notify a specific task (you need the task handle)
- Only one notification value per task
- Cannot be used from an ISR to wait (only to send)
Use task notifications for one-to-one ISR-to-task or task-to-task signaling. Use semaphores/event groups when multiple tasks may wait for the same event.
📖 Related: Mutual Exclusion in RTOS: Mutexes, Critical Sections, and Atomics • State Machine Pattern in C — With Practical Examples

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.



