Day 3 of 5
⏱ ~60 minutes
RTOS in 5 Days — Day 3

Queues and IPC

Tasks are isolated — they can't share variables safely. Queues are FreeRTOS's thread-safe message passing mechanism. Today you'll master them.

Queue Basics

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.

ISR-Safe Queue Operations

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 and Mailboxes

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().

c
// 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);
}
💡
Queue length = maximum burst size you need to absorb. If the producer sends 10 readings in 1 second and the consumer processes them at 1/second, you need a queue of at least 9. Size your queues based on worst-case burst — an overfull queue means lost data.
📝 Day 3 Exercise
Build a Multi-Sensor Aggregator
  1. Create 3 sensor tasks (simulated with random numbers), each posting to the same queue. One consumer task processes all readings.
  2. Print the queue depth with uxQueueMessagesWaiting() every second. Is it staying stable or growing?
  3. Implement a 'sensor fusion' task: wait for one reading from each of 3 separate sensor queues, then combine them into one output.
  4. Use event groups: each sensor sets a bit when its reading is ready. The fusion task waits for all 3 bits, then reads all queues.
  5. Add queue overflow handling: if xQueueSend() returns pdFAIL, increment a counter and log once per second.

Day 3 Summary

  • Queues copy by value — no pointer ownership issues; safe across tasks and ISRs
  • xQueueSend blocks if full; xQueueSendFromISR never blocks — use correct API
  • portYIELD_FROM_ISR() ensures a context switch happens immediately if a higher-priority task was unblocked
  • Event groups synchronize multiple events; mailboxes (queue len=1) deliver only the latest value
Challenge

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.

Finished this lesson?