
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.
Consider a system where a logging task receives messages from three sources:
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.
FreeRTOS provides three core functions for queue sets:
| Function | Purpose |
|---|---|
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.
#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);}
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));}}
void LoggingTask(void *pvParameters) {QueueSetMemberHandle_t readyHandle;ErrorCode_t errCode;NetPacket_t netPkt;for (;;) {// Block indefinitely until ANY member has datareadyHandle = 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 semaphoreLogSensorData();}}}
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).
+------------------------------------------------------------------+| 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() || |+------------------------------------------------------------------+
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:
configUSE_MUTEXES + configUSE_PRIORITY_INHERITANCE) for any shared resources accessed by producersxQueueSend()/xSemaphoreGive() to member objects will fail and new events are lostA 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:
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):
| Approach | Avg Wake Latency | CPU During Idle |
|---|---|---|
| Polling (10 ms delay) | 5,000 µs | ~1% (polling) |
| Queue Set (blocking) | 2.1 µs | 0% (truly blocked) |
The queue set adds one extra kernel lookup versus a direct queue receive, but eliminates all polling overhead.
xQueueAddToSet() After Creation// WRONG: Created but not addedQueueHandle_t q = xQueueCreate(10, sizeof(int));QueueSetHandle_t qs = xQueueCreateSet(10);// Missing: xQueueAddToSet(q, qs);
xQueueReceive() Without Checking the Handle// WRONG: Assumes errorQueue is readyreadyHandle = xQueueSelectFromSet(qs, portMAX_DELAY);xQueueReceive(errorQueue, &data, 0); // Will fail if netQueue was actually ready!
Always switch on the returned handle.
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.
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.
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.
queue.cQuick Links
Legal Stuff




