FreeRTOS Deep Dive · Part 4 of 8

Queues, Semaphores
& Mutexes — The Full Picture

FreeRTOS STM32 Intermediate IPC Source Code
FreeRTOS tutorial
FreeRTOS Deep Dive — Part 4
Queues, Semaphores & Mutexes
42:18
Part 4 / 8
42:18
Intermediate
FreeRTOS v10 · STM32F4
01

Overview

In this part of the FreeRTOS Deep Dive series we tackle the three most misunderstood IPC primitives: queues, semaphores, and mutexes. By the end of the video you'll know exactly when to reach for each one — and more importantly, when not to.

We build a live producer–consumer pipeline on a real STM32F4 Discovery board, deliberately trigger a priority inversion bug, watch the system deadlock on the oscilloscope, then fix it with a priority-inheritance mutex. All source code is linked below.

  • Understand the difference between queues, binary semaphores, counting semaphores, and mutexes
  • Build a queue-based producer–consumer pipeline between three tasks
  • Reproduce priority inversion live on hardware and see its impact on timing
  • Fix priority inversion using xSemaphoreCreateMutex() with priority inheritance
  • Know the runtime cost of each primitive and choose appropriately
02

Core Concepts at a Glance

📬
Queue
A FIFO buffer that passes data copies between tasks. Safe by design — no shared memory. Use when you need to move data, not just signal.
xQueueCreate(length, itemSize)
🚦
Semaphore
A signalling token with no data payload. Binary = on/off. Counting = resource pool. Use for ISR-to-task signalling or limiting concurrency.
xSemaphoreCreateBinary() / Counting()
🔒
Mutex
A semaphore with ownership and priority inheritance. Only the task that took it can give it back. Use exclusively to protect shared resources.
xSemaphoreCreateMutex()
⚠️
Never use a Mutex from an ISR
Mutexes use priority inheritance which requires a task context. From an ISR, use a binary semaphore with xSemaphoreGiveFromISR() instead.
03

Queues

Queues are FreeRTOS's primary inter-task communication mechanism. They copy data by value — not by pointer — which keeps tasks fully decoupled. A sending task blocks if the queue is full; a receiving task blocks if it's empty.

01
Creating and using a Queue
queue_demo.c
C
// Define the data type to pass through the queue
typedef struct {
    uint32_t timestamp;
    float    temperature;
    uint8_t  sensor_id;
} SensorReading_t;

// Create a queue that holds 10 SensorReading_t items
QueueHandle_t xSensorQueue;
xSensorQueue = xQueueCreate(10, sizeof(SensorReading_t));

// Producer task — sends data into the queue
void vProducerTask(void *pvParameters) {
    SensorReading_t reading;
    while (1) {
        reading.timestamp   = xTaskGetTickCount();
        reading.temperature = BME680_ReadTemp();
        reading.sensor_id   = 1;

        // Block up to 100ms if queue is full
        if (xQueueSend(xSensorQueue, &reading,
                       pdMS_TO_TICKS(100)) != pdPASS) {
            // Queue overflow — handle gracefully
        }
        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

// Consumer task — receives data from the queue
void vConsumerTask(void *pvParameters) {
    SensorReading_t reading;
    while (1) {
        // Block indefinitely until data arrives
        if (xQueueReceive(xSensorQueue, &reading,
                          portMAX_DELAY) == pdPASS) {
            ProcessReading(&reading);
        }
    }
}
💡
Queue Storage is Allocated at Creation
FreeRTOS allocates the full queue buffer upfront from the heap. A queue of 10 × sizeof(SensorReading_t) = 10 × 8 bytes = 80 bytes of heap. Always check xQueueCreate() returns non-NULL.
04

Semaphores

Semaphores are pure signalling mechanisms — they carry no data. The most common use case is ISR-to-task deferred processing: the ISR fires, gives a semaphore, and a waiting task wakes up to do the heavy work.

02
Binary Semaphore — ISR to Task
semaphore_isr.c
C
SemaphoreHandle_t xDataReadySem;

void setup(void) {
    // Create binary semaphore — starts empty (taken)
    xDataReadySem = xSemaphoreCreateBinary();

    xTaskCreate(vProcessingTask, "Process",
                512, NULL, 3, NULL);
}

// ISR — runs in interrupt context, keep it SHORT
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // Signal the processing task
    xSemaphoreGiveFromISR(xDataReadySem,
                          &xHigherPriorityTaskWoken);

    // Yield if a higher-priority task was woken
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

// Task — blocks until semaphore is given by ISR
void vProcessingTask(void *pvParameters) {
    while (1) {
        xSemaphoreTake(xDataReadySem, portMAX_DELAY);
        // Safe to do heavy processing here
        ProcessNewData();
    }
}
03
Counting Semaphore — Resource Pool
counting_sem.c
C
// Limit concurrent access to 3 DMA channels
#define MAX_DMA_CHANNELS 3

SemaphoreHandle_t xDMAPool;

// Max count = 3, initial count = 3 (all available)
xDMAPool = xSemaphoreCreateCounting(MAX_DMA_CHANNELS,
                                     MAX_DMA_CHANNELS);

void vTransferTask(void *pvParameters) {
    while (1) {
        // Acquire a DMA channel slot (blocks if all busy)
        xSemaphoreTake(xDMAPool, portMAX_DELAY);

        DMA_StartTransfer(...);
        DMA_WaitComplete(...);

        // Release channel back to pool
        xSemaphoreGive(xDMAPool);
    }
}
05

Mutexes

A mutex is a binary semaphore with two extra properties: ownership (only the task that took it can give it) and priority inheritance (FreeRTOS temporarily boosts the holder's priority to prevent inversion). Use mutexes exclusively to protect shared resources like peripherals or buffers.

04
Mutex — Protecting a shared UART
mutex_uart.c
C
SemaphoreHandle_t xUARTMutex;

void UART_Init(void) {
    xUARTMutex = xSemaphoreCreateMutex();
}

// Thread-safe UART print — any task can call this
void UART_SafePrint(const char *msg) {
    // Take mutex — will block if another task holds it
    if (xSemaphoreTake(xUARTMutex,
                       pdMS_TO_TICKS(100)) == pdPASS) {

        HAL_UART_Transmit(&huart2,
                          (uint8_t*)msg,
                          strlen(msg),
                          HAL_MAX_DELAY);

        // MUST give back — only the owner can release
        xSemaphoreGive(xUARTMutex);
    } else {
        // Timeout — log error, don't deadlock
    }
}
06

Priority Inversion & How Mutexes Fix It

Priority inversion is one of the most dangerous bugs in RTOS systems — and one of the hardest to catch in testing. It happens when a high-priority task is blocked waiting for a resource held by a low-priority task, which itself gets preempted by a medium-priority task. The result: the medium-priority task effectively runs ahead of the high-priority one.

// Priority Inversion — without Mutex
Task H (HIGH)
──waits──▶
Resource
◀──held by──
Task L (LOW)
▲ Task M preempts Task L — Task H stays blocked indefinitely
Task M (MED)
── runs freely ──
// effectively higher priority than Task H!
🚨
This killed the Mars Pathfinder
The 1997 NASA Mars Pathfinder rover experienced system resets caused by priority inversion between a high-priority bus management task, a medium-priority communications task, and a low-priority weather data task sharing a semaphore. The fix was to enable priority inheritance — the same feature FreeRTOS mutexes give you automatically.
// Priority Inheritance — with Mutex (FreeRTOS fix)
Task H (HIGH)
──waits──▶
Mutex
◀──held by──
Task L → boosted to HIGH
▲ FreeRTOS boosts Task L to HIGH priority — Task M can no longer preempt it
Task M (MED)
── waits its turn ──
// correct priority ordering restored
07

Full Producer–Consumer Demo

This is the complete demo from the video. Three tasks — a sensor reader, a processor, and a logger — pass data through queues and use a mutex to protect the shared SD card handle.

05
Full three-task IPC pipeline
ipc_pipeline.c
C
// ── Shared handles ──────────────────────────────────
QueueHandle_t     xRawQueue;      // Sensor → Processor
QueueHandle_t     xLogQueue;      // Processor → Logger
SemaphoreHandle_t xSDMutex;       // Protects SD card

// ── Task 1: Sensor Reader (HIGH priority) ───────────
void vSensorTask(void *pv) {
    RawData_t raw;
    TickType_t xLast = xTaskGetTickCount();
    while(1) {
        raw.ts   = xTaskGetTickCount();
        raw.temp = BME680_ReadTemp();
        raw.hum  = BME680_ReadHumidity();
        xQueueSend(xRawQueue, &raw, pdMS_TO_TICKS(10));
        vTaskDelayUntil(&xLast, pdMS_TO_TICKS(100));
    }
}

// ── Task 2: Processor (MEDIUM priority) ─────────────
void vProcessorTask(void *pv) {
    RawData_t       raw;
    ProcessedData_t out;
    while(1) {
        if(xQueueReceive(xRawQueue, &raw, portMAX_DELAY)) {
            out.ts           = raw.ts;
            out.temp_avg     = EMA(raw.temp);
            out.hum_avg      = EMA(raw.hum);
            out.crc          = CRC16(&out, sizeof(out) - 2);
            xQueueSend(xLogQueue, &out, pdMS_TO_TICKS(10));
        }
    }
}

// ── Task 3: Logger (LOW priority) ───────────────────
void vLoggerTask(void *pv) {
    ProcessedData_t out;
    while(1) {
        if(xQueueReceive(xLogQueue, &out, portMAX_DELAY)) {
            // Mutex protects the shared SD file handle
            xSemaphoreTake(xSDMutex, portMAX_DELAY);
            SD_WriteLine(&out);
            xSemaphoreGive(xSDMutex);
        }
    }
}

// ── Initialisation ───────────────────────────────────
void vInitIPC(void) {
    xRawQueue = xQueueCreate(16, sizeof(RawData_t));
    xLogQueue = xQueueCreate(32, sizeof(ProcessedData_t));
    xSDMutex  = xSemaphoreCreateMutex();
    xTaskCreate(vSensorTask,    "Sensor",    512,  NULL, 4, NULL);
    xTaskCreate(vProcessorTask, "Processor", 768,  NULL, 3, NULL);
    xTaskCreate(vLoggerTask,    "Logger",    1024, NULL, 2, NULL);
}
08

When to Use What

PrimitiveCarries Data?ISR Safe?Ownership?Best For
Queue✓ Yes✓ FromISRPassing structs between tasks
Binary Semaphore✗ No✓ YesISR → task deferred processing
Counting Semaphore✗ No✓ YesResource pool management
Mutex✗ No✗ Never✓ YesProtecting shared resources
Recursive Mutex✗ No✗ Never✓ ReentrantNested critical sections
💡
The Simple Rule
If you need to move data — use a Queue. If you need to signal — use a semaphore. If you need to protect — use a mutex. Never mix these roles.
09

Exercises

  • Modify the producer–consumer demo to add a third queue stage between Processor and Logger
  • Replace the binary semaphore ISR demo with a counting semaphore and handle 3 simultaneous GPIO events
  • Deliberately introduce priority inversion by replacing xSemaphoreCreateMutex() with xSemaphoreCreateBinary() and observe timing on an oscilloscope
  • Use uxQueueMessagesWaiting() in the Monitor task to detect queue build-up and trigger an alert LED
  • Profile each primitive's overhead using the FreeRTOS runtime stats feature and compare context switch times

Continue the Series

Part 5 covers FreeRTOS heap models — heap_1 through heap_5 — and how to tune memory allocation for deeply constrained devices.