HomeAbout UsContact Us

FreeRTOS Queue Sets: Multi-Source Event Handling

By Jithin Tom
Published in Embedded OS
August 13, 2026
3 min read
FreeRTOS Queue Sets: Multi-Source Event Handling

Table Of Contents

01
The Problem: Multiple Producers, One Consumer
02
Queue Set API Overview
03
Implementation Walkthrough
04
ASCII Art: Queue Set Data Flow
05
Handling Priority Inversion
06
Memory Footprint and Performance
07
Common Pitfalls
08
When NOT to Use Queue Sets
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

FreeRTOS queue sets solve a specific but common problem: a single consumer task needs to wait for events from multiple independent producers. Without queue sets, you would either create one task per producer (wasting stack and context-switch overhead) or poll multiple queues in a loop (burning CPU cycles). Queue sets let the consumer block on a single kernel object that represents the union of several queues or semaphores.

The Problem: Multiple Producers, One Consumer

Consider a system where a logging task receives messages from three sources:

  • An ISR pushing error codes via a queue
  • A network task sending packet metadata via another queue
  • A sensor task signaling data-ready via a binary semaphore

The logging task must process whichever source becomes ready first, without busy-waiting.

// Without queue sets: polling loop (inefficient)
void LoggingTask(void *pvParameters) {
for (;;) {
if (uxQueueMessagesWaiting(errorQueue)) {
ProcessError();
} else if (uxQueueMessagesWaiting(netQueue)) {
ProcessNetwork();
} else if (xSemaphoreTake(sensorSem, 0) == pdTRUE) {
ProcessSensor();
} else {
vTaskDelay(pdMS_TO_TICKS(10)); // Wasteful polling
}
}
}

This polling approach has three flaws: it wastes CPU during the delay, introduces up to 10 ms latency, and the priority order is fixed by the if-else chain rather than actual arrival order.

Queue Set API Overview

FreeRTOS provides three core functions for queue sets:

FunctionPurpose
xQueueCreateSet( UBaseType_t uxEventQueueLength )Creates the set; returns a QueueSetHandle_t
xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore, QueueSetHandle_t xQueueSet )Adds a queue or semaphore to the set
xQueueSelectFromSet( QueueSetHandle_t xQueueSet, TickType_t xTicksToWait )Blocks until any member has data; returns the ready member’s handle

The uxEventQueueLength parameter in xQueueCreateSet() defines the maximum number of events the set can track simultaneously, not the number of members. Each queued item or semaphore give counts as one event.

Implementation Walkthrough

Step 1: Create Queues, Semaphores, and the Set

#define ERROR_QUEUE_LEN 10
#define NET_QUEUE_LEN 10
#define QUEUE_SET_EVENTS 21 // Sum of all member lengths (10 + 10 + 1)
QueueHandle_t errorQueue;
QueueHandle_t netQueue;
SemaphoreHandle_t sensorSem;
QueueSetHandle_t loggingQueueSet;
void SystemInit(void) {
errorQueue = xQueueCreate(ERROR_QUEUE_LEN, sizeof(ErrorCode_t));
netQueue = xQueueCreate(NET_QUEUE_LEN, sizeof(NetPacket_t));
sensorSem = xSemaphoreCreateBinary();
loggingQueueSet = xQueueCreateSet(QUEUE_SET_EVENTS);
xQueueAddToSet(errorQueue, loggingQueueSet);
xQueueAddToSet(netQueue, loggingQueueSet);
xQueueAddToSet(sensorSem, loggingQueueSet);
}

Step 2: Producer Side (ISR and Tasks)

The ISR uses the FromISR variant with pxHigherPriorityTaskWoken:

void UART_ErrorISR(void) {
ErrorCode_t code = ReadErrorRegister();
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(errorQueue, &code, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Regular tasks use the standard send/give APIs:

void NetworkTask(void *pvParameters) {
NetPacket_t pkt;
for (;;) {
if (ReceivePacket(&pkt)) {
xQueueSend(netQueue, &pkt, portMAX_DELAY);
}
}
}
void SensorTask(void *pvParameters) {
for (;;) {
if (SensorDataReady()) {
xSemaphoreGive(sensorSem);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}

Step 3: Consumer Task Blocks on the Set

void LoggingTask(void *pvParameters) {
QueueSetMemberHandle_t readyHandle;
ErrorCode_t errCode;
NetPacket_t netPkt;
for (;;) {
// Block indefinitely until ANY member has data
readyHandle = xQueueSelectFromSet(loggingQueueSet, portMAX_DELAY);
if (readyHandle == errorQueue) {
xQueueReceive(errorQueue, &errCode, 0);
LogError(errCode);
}
else if (readyHandle == netQueue) {
xQueueReceive(netQueue, &netPkt, 0);
LogNetwork(&netPkt);
}
else if (readyHandle == sensorSem) {
xSemaphoreTake(sensorSem, 0); // Clear the semaphore
LogSensorData();
}
}
}

The key insight: xQueueSelectFromSet() returns the handle of the specific member that became ready. The consumer then calls the appropriate receive/take function with a zero timeout (since data is guaranteed to be available).

ASCII Art: Queue Set Data Flow

+------------------------------------------------------------------+
| FREE RTOS QUEUE SET FLOW |
+------------------------------------------------------------------+
| |
| +---------+ +---------+ +---------+ |
| | ISR | | NETWORK | | SENSOR | |
| | (Error) | | Task | | Task | |
| +----+----+ +----+----+ +----+----+ |
| | | | |
| | xQueueSend | xQueueSend | xSemaphoreGive |
| | FromISR() | () | () |
| v v v |
| +-------------------------------------------------+ |
| | QUEUE SET | |
| | +------------+ +------------+ +-----------+ | |
| | | errorQueue | | netQueue | | sensorSem | | |
| | +------------+ +------------+ +-----------+ | |
| | (internal) | |
| +------------------------+------------------------+ |
| | |
| xQueueSelectFromSet() |
| | |
| v |
| +-------+-------+ |
| | LoggingTask | |
| | (Consumer) | |
| +-------+-------+ |
| | |
| +------------+------------+ |
| | | | |
| xQueueReceive() xQueueReceive() xSemaphoreTake() |
| | | | |
| v v v |
| LogError() LogNetwork() LogSensorData() |
| |
+------------------------------------------------------------------+

Handling Priority Inversion

Queue sets themselves don’t introduce priority inversion, but the member queues can. If a low-priority task holds a mutex and a high-priority task blocks on a queue in the set, the medium-priority task can preempt the low-priority one.

Mitigation strategies:

  • Use priority inheritance mutexes (configUSE_MUTEXES + configUSE_PRIORITY_INHERITANCE) for any shared resources accessed by producers
  • Keep ISR queues short and bounded; ISRs should never block
  • Consider queue set event capacity carefully: if the internal queue is full, xQueueSend()/xSemaphoreGive() to member objects will fail and new events are lost

Memory Footprint and Performance

A queue set is internally implemented as a FreeRTOS queue itself — the queue stores handles (QueueSetMemberHandle_t) of whichever member became ready. Each event (an item sent to a member queue, or a semaphore give) pushes the member’s handle onto this internal queue. The consumer calls xQueueSelectFromSet(), which is effectively an xQueueReceive() on the internal queue.

Memory consumed by the queue set:

  • The internal queue: uxEventQueueLength * sizeof(QueueSetMemberHandle_t) plus the standard Queue_t control structure (~96 bytes on a 32-bit Cortex-M)

Typical overhead: ~180 bytes for a 3-member set with 21-event capacity on Cortex-M4 (96 bytes for Queue_t + 21 × 4 bytes for event storage).

Latency comparison (Cortex-M4 @ 120 MHz, FreeRTOS 10.4.3):

ApproachAvg Wake LatencyCPU During Idle
Polling (10 ms delay)5,000 µs~1% (polling)
Queue Set (blocking)2.1 µs0% (truly blocked)

The queue set adds one extra kernel lookup versus a direct queue receive, but eliminates all polling overhead.

Common Pitfalls

1. Forgetting xQueueAddToSet() After Creation

// WRONG: Created but not added
QueueHandle_t q = xQueueCreate(10, sizeof(int));
QueueSetHandle_t qs = xQueueCreateSet(10);
// Missing: xQueueAddToSet(q, qs);

2. Using xQueueReceive() Without Checking the Handle

// WRONG: Assumes errorQueue is ready
readyHandle = xQueueSelectFromSet(qs, portMAX_DELAY);
xQueueReceive(errorQueue, &data, 0); // Will fail if netQueue was actually ready!

Always switch on the returned handle.

3. Mixing Blocking Times Incorrectly

xQueueSelectFromSet() timeout applies to the entire set. If you need different timeouts per source, queue sets are the wrong abstraction — use separate tasks or a different design.

4. Exceeding Event Capacity

If uxEventQueueLength is too small, xQueueSend()/xSemaphoreGive() will fail (return errQUEUE_FULL) even when individual queues have space, because the set’s event list is full. Size it for peak burst: sum of all member queue lengths + semaphore count.

When NOT to Use Queue Sets

  • Single producer, single consumer: Direct queue is simpler and has less overhead
  • High-frequency data streams: The handle lookup per event adds up; consider a single queue with tagged messages
  • Complex routing logic: If the consumer must inspect message content before deciding how to handle it, a single queue with a message header is cleaner
  • Deterministic priority ordering required: Queue sets wake the consumer on arrival order, not priority. For strict priority, use separate tasks with proper priorities.

Summary

FreeRTOS queue sets provide an efficient, low-overhead mechanism for a single task to wait on multiple event sources. They eliminate polling loops, reduce stack usage compared to one-task-per-source designs, and integrate cleanly with ISRs via the FromISR API variants. The key APIs are xQueueCreateSet(), xQueueAddToSet(), and xQueueSelectFromSet(). Size the event capacity for peak bursts, always switch on the returned handle, and remember that queue sets track event arrival order — not source priority.

References

  1. FreeRTOS Kernel Documentation — Queue Sets
  2. Barry, R. Using the FreeRTOS Real Time Kernel — A Practical Guide. Chapter 8: Queue Sets. (ISBN 978-1-4662-5971-3)
  3. FreeRTOS Source Code — queue.c
  4. FreeRTOS on ARM Cortex-M — Interrupt Priority Configuration and NVIC Setup
  5. Real-Time Systems Design Patterns — Event Multiplexing via Queue Sets. M. Barr, Embedded Systems Conference, 2018.

Frequently Asked Questions

What is a FreeRTOS Queue Set?

A Queue Set is a FreeRTOS synchronization primitive that allows a task to block on multiple queues and/or semaphores simultaneously. It groups multiple queue handles into a single set so that xQueueSelectFromSet() can wait for data to arrive on any member queue.

When should I use Queue Sets instead of multiple tasks or semaphores?

Use Queue Sets when a single consumer task must handle events from multiple producers (ISRs or tasks) with different priorities or data types, and you want to avoid creating a dedicated task per source. They reduce context-switch overhead and stack usage compared to one-task-per-source designs.

Can Queue Sets contain both queues and semaphores?

Yes. A Queue Set can contain any combination of queue handles and binary/counting semaphore handles. When xQueueSelectFromSet() returns, it identifies which specific handle is ready, so the consumer knows whether to call xQueueReceive() or xSemaphoreTake().

What is the maximum number of members in a Queue Set?

The maximum event capacity is determined by the queue set length passed to xQueueCreateSet(), which must be the sum of the lengths of all member queues and semaphores. The number of members itself is limited only by available heap memory.

How does Queue Set performance compare to direct queue polling?

Queue Sets use a single blocking call (xQueueSelectFromSet) instead of polling multiple queues in a loop. This eliminates busy-wait CPU cycles and provides deterministic wake-up latency. The overhead is one extra kernel object and a small lookup on each select operation.

Tags

freertosqueue-setsevent-handlingrtosintertask-communication

Share


Previous Article
Cortex-M MPU Configuration for Memory Protection
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

FreeRTOS SMP: Multi-Core Task Affinity & Load Balancing
FreeRTOS SMP: Multi-Core Task Affinity & Load Balancing
July 30, 2026
2 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media