Skip to content

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 THIS
volatile 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.


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:

PropertyDetail
FIFO orderFirst item sent is the first item received
Fixed lengthSet at creation time — cannot grow
Fixed item sizeEvery slot holds exactly sizeof(YourType) bytes
Copy semanticsData is copied in/out — the original variable is safe to reuse
Thread-safeMultiple tasks can send/receive concurrently without corruption
BlockingA task can sleep until the queue has data (or space) — zero CPU while waiting

#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.”

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.

void ADCTask(void *pvParameters) {
(void)pvParameters;
for (;;) {
int16_t sample = readADC();
xQueueSend(xQueueSamples, &sample, pdMS_TO_TICKS(5));
vTaskDelay(pdMS_TO_TICKS(10));
}
}
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));
}
}
ValueBehavior
0Don’t wait — return immediately if the queue is full
pdMS_TO_TICKS(N)Wait up to N ms for space to open
portMAX_DELAYWait forever (blocks until space is available)

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.

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.


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?
);
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);
}
DoDon’t
xQueueSendFromISR()xQueueSend()
xQueueReceiveFromISR()xQueueReceive()
xTaskGetTickCountFromISR()xTaskGetTickCount()
Always call portYIELD_FROM_ISR()Forget the yield (causes delayed wakeup)
Keep ISRs short — send data, get outProcess data inside the ISR

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.


UBaseType_t pending = uxQueueMessagesWaiting(xQueueSamples);
UBaseType_t free = uxQueueSpacesAvailable(xQueueSamples);

These are useful for debugging and tuning:

  • If pending is consistently close to the queue length, your consumer is too slow — increase priority, reduce work, or increase queue length.
  • If pending is always 0, the consumer is faster than the producer — the queue length could be smaller.

Instead of sending raw data, you can send commands — a very useful pattern for tasks that control hardware or manage state:

// Define the command vocabulary
typedef 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 Hz
Command 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));

MistakeWhat happensFix
Using xQueueSend in an ISRScheduler corruption, random crashesUse xQueueSendFromISR
Forgetting portYIELD_FROM_ISRWoken task waits until next tick instead of running immediatelyAlways call it after FromISR operations
Queue item size mismatchSends partial data or reads garbagesizeof(YourType) must match at creation and at send/receive
Passing a pointer instead of a valueQueue copies the pointer, not the data — data may be gone when consumer readsSend the actual struct, not a pointer to it
Queue too short for burst trafficItems dropped during peaksMonitor with uxQueueMessagesWaiting, increase length
portMAX_DELAY on send without checking returnProducer blocks forever if consumer is stuckUse bounded timeouts and check return value

FunctionContextPurpose
xQueueCreate(len, size)Task (before scheduler)Create a queue
xQueueSend(q, &item, timeout)TaskAdd item to back of queue
xQueueSendToFront(q, &item, timeout)TaskAdd item to front (high priority)
xQueueReceive(q, &buf, timeout)TaskRemove and copy oldest item
xQueuePeek(q, &buf, timeout)TaskCopy oldest item without removing
xQueueSendFromISR(q, &item, &woken)ISRAdd item from interrupt
xQueueReceiveFromISR(q, &buf, &woken)ISRRemove item from interrupt
uxQueueMessagesWaiting(q)AnyItems currently in queue
uxQueueSpacesAvailable(q)AnyEmpty slots remaining
xQueueReset(q)TaskEmpty the queue