Tasks are isolated — they can't share variables safely. Queues are FreeRTOS's thread-safe message passing mechanism. Today you'll master them.
A FreeRTOS queue is a thread-safe FIFO buffer. xQueueCreate(length, itemSize) allocates it. xQueueSend(queue, &item, timeout) copies the item to the queue. xQueueReceive(queue, &item, portMAX_DELAY) blocks until an item is available, then copies it out. Queues copy by value — not by pointer. This prevents use-after-free bugs. For large data, send a pointer to a pool-allocated buffer.
From an ISR, use xQueueSendFromISR() and xQueueReceiveFromISR() with a BaseType_t xHigherPriorityTaskWoken parameter. If the queue unblocks a higher-priority task, set this flag. At the end of the ISR, call portYIELD_FROM_ISR(xHigherPriorityTaskWoken) — this triggers a context switch if needed. Never call regular queue functions from an ISR.
Event groups: 24-bit flags that tasks can set, clear, and wait on. xEventGroupWaitBits() blocks until all (or any) specified bits are set. Useful for synchronizing multiple events. Mailbox: a queue of length 1. The last message always overwrites the previous. Use for sensor values where you only care about the latest reading, not the history. Implemented with xQueueOverwrite().
// Queue-based producer/consumer pattern
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
typedef struct {
uint32_t timestamp;
float temperature;
float humidity;
uint8_t device_id;
} SensorReading_t;
QueueHandle_t xSensorQueue;
// Producer: reads sensor, sends to queue
void vSensorTask(void *pv) {
SensorReading_t reading;
reading.device_id = (uint8_t)(uint32_t)pv;
while (1) {
reading.timestamp = xTaskGetTickCount();
reading.temperature = readTemperature(); // your sensor
reading.humidity = readHumidity();
// Send to queue, wait up to 100ms if full
if (xQueueSend(xSensorQueue, &reading, pdMS_TO_TICKS(100)) != pdPASS) {
// Queue full — log or increment dropped counter
dropped_count++;
}
vTaskDelay(pdMS_TO_TICKS(500)); // 2Hz
}
}
// Consumer: receives from queue, processes
void vProcessTask(void *pv) {
SensorReading_t reading;
while (1) {
// Block indefinitely until item available
if (xQueueReceive(xSensorQueue, &reading, portMAX_DELAY) == pdPASS) {
printf("[%lu] Dev%d: %.1f°C %.1f%%\n",
reading.timestamp, reading.device_id,
reading.temperature, reading.humidity);
if (reading.temperature > 30.0f) {
triggerAlert(reading.device_id);
}
}
}
}
// ISR version: called from timer interrupt
void TIM2_IRQHandler(void) {
static SensorReading_t r;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
r.timestamp = xTaskGetTickCountFromISR();
r.temperature = readTemperatureFast(); // non-blocking sensor
xQueueSendFromISR(xSensorQueue, &r, &xHigherPriorityTaskWoken);
// Yield if we unblocked a higher-priority task
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
TIM2->SR &= ~TIM_SR_UIF; // clear interrupt flag
}
int main(void) {
// Create queue: holds 10 SensorReading_t structs
xSensorQueue = xQueueCreate(10, sizeof(SensorReading_t));
configASSERT(xSensorQueue != NULL);
xTaskCreate(vSensorTask, "SENS", 256, (void*)1, 2, NULL);
xTaskCreate(vProcessTask, "PROC", 512, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}
Implement a ring buffer (circular buffer) as a FreeRTOS-safe alternative to a queue. The difference: a ring buffer passes data by reference (pointer to a fixed memory pool), avoiding copies. Implement rb_write() that returns a pointer to a slot, and rb_read() that returns the oldest slot. Use a semaphore to signal when data is ready. Compare throughput of your ring buffer vs a FreeRTOS queue for 1000 messages of 256 bytes.