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

Mutexes and Synchronization

Shared resources in a multi-tasking system need protection. Today you'll use mutexes, binary semaphores, and understand priority inversion.

Mutex vs Binary Semaphore

Both block tasks. The difference: a mutex has ownership — only the task that took it can give it back. A binary semaphore has no ownership — any task can give it. Use a mutex for protecting a shared resource (SPI bus, printf, global data). Use a binary semaphore for signaling between tasks (ISR signals task that data is ready). FreeRTOS mutexes include priority inheritance — this prevents priority inversion.

Priority Inversion

Classic problem: Low-priority task L holds mutex M. High-priority task H wants M — blocks. Medium-priority task C (no interest in M) runs and preempts L. H is now blocked waiting for L, which is blocked waiting for C. The high-priority task is effectively at the lowest priority. Fix: priority inheritance — when H blocks on a mutex held by L, temporarily boost L to H's priority so it runs and releases the mutex quickly. FreeRTOS mutexes do this automatically.

Counting Semaphores and Resource Pools

A counting semaphore starts at N and allows up to N tasks to access a resource simultaneously. Example: 3 SPI channels — semaphore count = 3. A task takes one channel (count decrements), uses it, gives back (count increments). When count = 0, tasks block. Use counting semaphores for resource pools, rate limiting, or producer-consumer with a bounded buffer (separate semaphores for 'empty slots' and 'filled slots').

c
// Mutex protecting a shared UART, avoiding garbled output
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t xUARTMutex;
SemaphoreHandle_t xDataReady;

// Thread-safe printf via mutex
void safe_printf(const char *fmt, ...) {
    if (xSemaphoreTake(xUARTMutex, pdMS_TO_TICKS(100)) == pdPASS) {
        va_list args;
        va_start(args, fmt);
        vprintf(fmt, args);
        va_end(args);
        xSemaphoreGive(xUARTMutex);
    }
    // If mutex not available in 100ms, drop the message
}

// ISR signals data ready (binary semaphore, no ownership)
void DMA1_Stream0_IRQHandler(void) {
    BaseType_t xWoken = pdFALSE;
    xSemaphoreGiveFromISR(xDataReady, &xWoken);
    portYIELD_FROM_ISR(xWoken);
    // clear DMA interrupt flag...
}

// Task waits for DMA completion signal
void vProcessTask(void *pv) {
    while (1) {
        // Block until ISR signals data is ready
        if (xSemaphoreTake(xDataReady, portMAX_DELAY) == pdPASS) {
            safe_printf("DMA complete, processing...\n");
            // process DMA buffer
        }
    }
}

// Multiple tasks safely using shared UART
void vTask1(void *pv) {
    uint32_t i = 0;
    while (1) {
        safe_printf("[Task1] iteration %lu\n", i++);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void vTask2(void *pv) {
    uint32_t i = 0;
    while (1) {
        safe_printf("[Task2] iteration %lu\n", i++);
        vTaskDelay(pdMS_TO_TICKS(700));
    }
}

int main(void) {
    xUARTMutex = xSemaphoreCreateMutex();
    xDataReady = xSemaphoreCreateBinary();
    configASSERT(xUARTMutex && xDataReady);

    xTaskCreate(vTask1,       "T1",   256, NULL, 1, NULL);
    xTaskCreate(vTask2,       "T2",   256, NULL, 1, NULL);
    xTaskCreate(vProcessTask, "PROC", 256, NULL, 2, NULL);
    vTaskStartScheduler();
    while (1);
}
💡
Hold mutexes for the shortest time possible. Taking a mutex → doing computation → releasing is wrong. The pattern: take mutex → read data → release mutex → do computation with local copy. Long critical sections block other tasks from accessing the shared resource.
📝 Day 4 Exercise
Protect Shared Peripherals
  1. Create two tasks that both write to UART. Without a mutex, observe garbled output (interleaved characters).
  2. Add a mutex. Confirm that messages are no longer garbled.
  3. Implement priority inversion: task H (priority 3) and task L (priority 1) share a mutex. Task M (priority 2) exists but doesn't use the mutex. Observe task M running while H is blocked. Confirm FreeRTOS resolves this with priority inheritance.
  4. Use uxSemaphoreGetCount() to verify your counting semaphore state machine is correct.
  5. Implement a deadlock intentionally: task A takes mutex1 then tries mutex2; task B takes mutex2 then tries mutex1. Both block forever. Observe with the monitor task and FreeRTOS+Trace.

Day 4 Summary

  • Mutex: owns-based, protects shared resources, priority inheritance built in
  • Binary semaphore: signal between tasks (ISR→task), no ownership, xGiveFromISR in ISR
  • Priority inversion: low-priority task blocks high-priority — FreeRTOS mutexes inherit priority automatically
  • Hold mutexes briefly: take, copy data, release — compute outside the critical section
Challenge

Implement a thread-safe memory pool allocator as a FreeRTOS library. Pool contains N fixed-size blocks. pool_alloc() returns a free block (or NULL if pool empty). pool_free(block) returns it. Use a counting semaphore (count = N) for blocking alloc and a mutex for the free list. Compare performance to pvPortMalloc() for 10,000 alloc/free cycles — a pool allocator should be significantly faster and produce no fragmentation.

Finished this lesson?