Queues: Task-to-Task and ISR Communication
1. The Problem: Sharing Data Between Tasks
Section titled “1. The Problem: Sharing Data Between Tasks”You have two tasks: one reads the ADC, another updates the display. The ADC task writes a sample to a global variable. The display task reads it. What could go wrong?
// DON'T DO THISvolatile int16_t gLatestSample; // shared global
void ADCTask(void *p) { for (;;) { gLatestSample = readADC(); // write vTaskDelay(pdMS_TO_TICKS(10)); }}
void DisplayTask(void *p) { for (;;) { int16_t val = gLatestSample; // read — but WHEN? drawValue(val); vTaskDelay(pdMS_TO_TICKS(50)); }}A queue solves this. It’s a thread-safe FIFO buffer managed by the kernel. The producer sends data into it; the consumer receives data from it. FreeRTOS handles all the locking internally — you never touch a mutex or worry about atomicity.
2. How Queues Work
Section titled “2. How Queues Work”A queue is a fixed-size buffer that stores copies of your data (not pointers — actual copies). When you send, the data is copied into the queue. When you receive, it’s copied out.
Producer ──send──▶ [ slot 0 | slot 1 | slot 2 | ... | slot N-1 ] ──receive──▶ Consumer ▲ ▲ head (oldest) tail (newest)Key properties:
| Property | Detail |
|---|---|
| FIFO order | First item sent is the first item received |
| Fixed length | Set at creation time — cannot grow |
| Fixed item size | Every slot holds exactly sizeof(YourType) bytes |
| Copy semantics | Data is copied in/out — the original variable is safe to reuse |
| Thread-safe | Multiple tasks can send/receive concurrently without corruption |
| Blocking | A task can sleep until the queue has data (or space) — zero CPU while waiting |
3. Creating a Queue
Section titled “3. Creating a Queue”#include "FreeRTOS.h"#include "queue.h"
QueueHandle_t xQueueSamples;
// In main(), before vTaskStartScheduler():xQueueSamples = xQueueCreate( 16, // length: how many items the queue can hold sizeof(int16_t) // item size: bytes per item);
if (xQueueSamples == NULL) { // Creation failed — not enough heap memory // Light an LED, halt, or handle the error while (1) {}}How to choose the length:
- Think about the worst-case burst from the producer before the consumer catches up.
- A sensor sampling at 100 Hz into a task that processes every 50 ms? The consumer handles ~5 samples per cycle — a queue of 16 gives comfortable headroom.
- Larger queues use more heap (
length × item_size + overhead). Don’t allocate 1000 slots “just in case.”
4. Sending to a Queue (from a Task)
Section titled “4. Sending to a Queue (from a Task)”BaseType_t xQueueSend( QueueHandle_t xQueue, // the queue const void *pvItemToQueue, // pointer to the data to copy in TickType_t xTicksToWait // how long to wait if the queue is full);Returns pdTRUE if the item was sent, errQUEUE_FULL if the timeout expired.
Example: Sending a Simple Value
Section titled “Example: Sending a Simple Value”void ADCTask(void *pvParameters) { (void)pvParameters;
for (;;) { int16_t sample = readADC(); xQueueSend(xQueueSamples, &sample, pdMS_TO_TICKS(5)); vTaskDelay(pdMS_TO_TICKS(10)); }}Example: Sending a Struct
Section titled “Example: Sending a Struct”typedef struct { uint32_t timestamp; int16_t value;} Sample;
QueueHandle_t xQueueSamples;// Created with: xQueueCreate(16, sizeof(Sample));
void ADCTask(void *pvParameters) { (void)pvParameters;
for (;;) { Sample s; s.timestamp = xTaskGetTickCount(); s.value = readADC();
if (xQueueSend(xQueueSamples, &s, pdMS_TO_TICKS(5)) != pdTRUE) { // Queue full — sample dropped. Consider logging this. }
vTaskDelay(pdMS_TO_TICKS(10)); }}The Timeout Parameter
Section titled “The Timeout Parameter”| Value | Behavior |
|---|---|
0 | Don’t wait — return immediately if the queue is full |
pdMS_TO_TICKS(N) | Wait up to N ms for space to open |
portMAX_DELAY | Wait forever (blocks until space is available) |
5. Receiving from a Queue (from a Task)
Section titled “5. Receiving from a Queue (from a Task)”BaseType_t xQueueReceive( QueueHandle_t xQueue, // the queue void *pvBuffer, // where to copy the received item TickType_t xTicksToWait // how long to wait if the queue is empty);Returns pdTRUE if an item was received, pdFALSE if the timeout expired.
Example: Blocking Consumer
Section titled “Example: Blocking Consumer”void DisplayTask(void *pvParameters) { (void)pvParameters; Sample s;
for (;;) { // Block forever until a sample arrives — uses zero CPU while waiting if (xQueueReceive(xQueueSamples, &s, portMAX_DELAY) == pdTRUE) { drawValue(s.value, s.timestamp); } }}This is the most common pattern: the consumer blocks on the queue with portMAX_DELAY. It wakes up only when data arrives — no polling, no wasted CPU cycles.
6. Sending from an ISR
Section titled “6. Sending from an ISR”Inside an interrupt handler, you must use the FromISR variant. The regular xQueueSend calls scheduler functions that are not safe inside ISRs — using them will corrupt the kernel.
BaseType_t xQueueSendFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken // output: was a task woken?);The Full ISR Pattern
Section titled “The Full ISR Pattern”void ADC0SS0_IRQHandler(void) { // 1. Clear the interrupt flag ADCIntClear(ADC0_BASE, 0);
// 2. Prepare the data BaseType_t xHigherPriorityTaskWoken = pdFALSE; Sample s = { .timestamp = xTaskGetTickCountFromISR(), .value = ADCSequenceDataGet(ADC0_BASE, 0) };
// 3. Send to queue (FromISR variant!) xQueueSendFromISR(xQueueSamples, &s, &xHigherPriorityTaskWoken);
// 4. Yield if we woke a higher-priority task portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}ISR Rules Summary
Section titled “ISR Rules Summary”| Do | Don’t |
|---|---|
xQueueSendFromISR() | xQueueSend() |
xQueueReceiveFromISR() | xQueueReceive() |
xTaskGetTickCountFromISR() | xTaskGetTickCount() |
Always call portYIELD_FROM_ISR() | Forget the yield (causes delayed wakeup) |
| Keep ISRs short — send data, get out | Process data inside the ISR |
7. Peeking Without Removing
Section titled “7. Peeking Without Removing”Sometimes you want to inspect the next item without consuming it:
Sample head;if (xQueuePeek(xQueueSamples, &head, 0) == pdTRUE) { // head contains a copy of the oldest item // the item is STILL in the queue}Use this when multiple consumers need to see the same data, or when you need to decide whether to process an item before committing to removing it.
8. Monitoring Queue Health
Section titled “8. Monitoring Queue Health”UBaseType_t pending = uxQueueMessagesWaiting(xQueueSamples);UBaseType_t free = uxQueueSpacesAvailable(xQueueSamples);These are useful for debugging and tuning:
- If
pendingis consistently close to the queue length, your consumer is too slow — increase priority, reduce work, or increase queue length. - If
pendingis always 0, the consumer is faster than the producer — the queue length could be smaller.
9. Pattern: Command Queue
Section titled “9. Pattern: Command Queue”Instead of sending raw data, you can send commands — a very useful pattern for tasks that control hardware or manage state:
// Define the command vocabularytypedef enum { CMD_BUZZER_ON, CMD_BUZZER_OFF, CMD_SET_FREQUENCY} CommandId;
typedef struct { CommandId id; uint16_t arg; // meaning depends on the command} Command;
QueueHandle_t xQueueCommands;// Created with: xQueueCreate(8, sizeof(Command));The receiver task acts as a command dispatcher:
void BuzzerTask(void *pvParameters) { (void)pvParameters; Command cmd;
for (;;) { if (xQueueReceive(xQueueCommands, &cmd, portMAX_DELAY) == pdTRUE) { switch (cmd.id) { case CMD_BUZZER_ON: buzzerEnable(); break; case CMD_BUZZER_OFF: buzzerDisable(); break; case CMD_SET_FREQUENCY: buzzerSetFreq(cmd.arg); break; } } }}Any other task can send commands:
// Turn on the buzzer at 440 HzCommand c1 = { .id = CMD_SET_FREQUENCY, .arg = 440 };xQueueSend(xQueueCommands, &c1, pdMS_TO_TICKS(10));
Command c2 = { .id = CMD_BUZZER_ON, .arg = 0 };xQueueSend(xQueueCommands, &c2, pdMS_TO_TICKS(10));10. Common Mistakes
Section titled “10. Common Mistakes”| Mistake | What happens | Fix |
|---|---|---|
Using xQueueSend in an ISR | Scheduler corruption, random crashes | Use xQueueSendFromISR |
Forgetting portYIELD_FROM_ISR | Woken task waits until next tick instead of running immediately | Always call it after FromISR operations |
| Queue item size mismatch | Sends partial data or reads garbage | sizeof(YourType) must match at creation and at send/receive |
| Passing a pointer instead of a value | Queue copies the pointer, not the data — data may be gone when consumer reads | Send the actual struct, not a pointer to it |
| Queue too short for burst traffic | Items dropped during peaks | Monitor with uxQueueMessagesWaiting, increase length |
portMAX_DELAY on send without checking return | Producer blocks forever if consumer is stuck | Use bounded timeouts and check return value |
11. Quick Reference
Section titled “11. Quick Reference”| Function | Context | Purpose |
|---|---|---|
xQueueCreate(len, size) | Task (before scheduler) | Create a queue |
xQueueSend(q, &item, timeout) | Task | Add item to back of queue |
xQueueSendToFront(q, &item, timeout) | Task | Add item to front (high priority) |
xQueueReceive(q, &buf, timeout) | Task | Remove and copy oldest item |
xQueuePeek(q, &buf, timeout) | Task | Copy oldest item without removing |
xQueueSendFromISR(q, &item, &woken) | ISR | Add item from interrupt |
xQueueReceiveFromISR(q, &buf, &woken) | ISR | Remove item from interrupt |
uxQueueMessagesWaiting(q) | Any | Items currently in queue |
uxQueueSpacesAvailable(q) | Any | Empty slots remaining |
xQueueReset(q) | Task | Empty the queue |