Shared resources in a multi-tasking system need protection. Today you'll use mutexes, binary semaphores, and understand priority inversion.
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.
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.
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').
// 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);
}
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.