In real-time embedded applications, multiple tasks often need access to shared resources such as serial ports, memory buffers, or hardware peripherals. If not handled properly, concurrent access can lead to race conditions, data corruption, or system instability. FreeRTOS provides mutexes—mutual exclusion objects—to handle such situations safely.
This blog post explains how FreeRTOS mutexes work, when and why to use them, and how to implement a mutex-based synchronization example on the ESP-WROVER-KIT, which is based on the ESP32 microcontroller.
What is a Mutex in FreeRTOS?
A mutex (short for mutual exclusion) is a binary semaphore with ownership and priority inheritance capabilities. It ensures exclusive access to a resource by allowing only one task to hold the mutex at a time. If another task attempts to take the mutex while it is already held, it will block until the mutex becomes available.
Mutexes in FreeRTOS support:
- Recursive locking: A task can lock the same mutex multiple times if it already owns it.
- Priority inheritance: Prevents priority inversion by temporarily boosting the priority of a low-priority task holding the mutex when a higher-priority task is waiting for it.
This makes mutexes ideal for protecting critical sections and shared peripherals.
When Should You Use a Mutex?
Use a mutex whenever you have:
- A shared resource (e.g., UART, SPI, I2C, global variable, buffer).
- Multiple tasks that may access or modify this resource.
- A need to ensure that only one task accesses the resource at a time.
Unlike binary semaphores, mutexes are designed specifically for resource protection with additional safeguards like priority inheritance.
FreeRTOS Mutex API Overview
Here are the key functions used for working with mutexes in FreeRTOS:
Create a mutex:
SemaphoreHandle_t xSemaphoreCreateMutex(void);
Take (lock) the mutex:
xSemaphoreTake(xMutex, portMAX_DELAY);
Give (unlock) the mutex:
xSemaphoreGive(xMutex);
Mutexes use the same SemaphoreHandle_t
type and API functions as semaphores but have special handling internally.
Example: Protecting UART Access with a Mutex
Let’s assume we have two tasks that both want to send messages over UART. If both write at the same time, the output may get jumbled. To prevent this, we’ll use a mutex to ensure that only one task writes to UART at any given time.
This example runs on the ESP-WROVER-KIT using the ESP-IDF framework.
Setup
We’ll create two tasks:
- Task A and Task B, both trying to log messages.
- A shared mutex is used to synchronize UART access.
Global Mutex Declaration
SemaphoreHandle_t xUARTMutex;
Task A
void task_a(void *pvParameters)
{
while (true)
{
if (xSemaphoreTake(xUARTMutex, portMAX_DELAY) == pdTRUE)
{
for (int i = 0; i < 3; i++)
{
ESP_LOGI("TaskA", "Sending message %d from Task A", i);
vTaskDelay(pdMS_TO_TICKS(500));
}
xSemaphoreGive(xUARTMutex);
}
vTaskDelay(pdMS_TO_TICKS(2000)); // Simulate other processing
}
}
Task B
void task_b(void *pvParameters)
{
while (true)
{
if (xSemaphoreTake(xUARTMutex, portMAX_DELAY) == pdTRUE)
{
for (int i = 0; i < 3; i++)
{
ESP_LOGI("TaskB", "Sending message %d from Task B", i);
vTaskDelay(pdMS_TO_TICKS(500));
}
xSemaphoreGive(xUARTMutex);
}
vTaskDelay(pdMS_TO_TICKS(2000)); // Simulate other processing
}
}
app_main Function
void app_main()
{
// Create the mutex
xUARTMutex = xSemaphoreCreateMutex();
if (xUARTMutex != NULL)
{
ESP_LOGI("Main", "UART Mutex created");
// Create tasks
xTaskCreate(task_a, "TaskA", 2048, NULL, 2, NULL);
xTaskCreate(task_b, "TaskB", 2048, NULL, 2, NULL);
}
else
{
ESP_LOGE("Main", "Failed to create UART Mutex");
}
}
Expected Output
On your serial monitor, you will see output similar to the following, without messages from the two tasks overlapping:
TaskA: Sending message 0 from Task A
TaskA: Sending message 1 from Task A
TaskA: Sending message 2 from Task A
TaskB: Sending message 0 from Task B
TaskB: Sending message 1 from Task B
TaskB: Sending message 2 from Task B
This shows that the mutex successfully ensured exclusive access to the UART.
Debugging Tips
- Always check if the mutex was created successfully.
- Avoid holding a mutex for too long, which can block other tasks.
- Be careful with deadlocks—ensure that all code paths release the mutex after acquiring it.
- Do not use mutexes in ISRs—use binary semaphores or queues instead.
Summary
FreeRTOS mutexes are critical for maintaining consistency and reliability in multitasking embedded systems. They allow multiple tasks to safely share access to resources by ensuring that only one task accesses a resource at a time. With features like priority inheritance, mutexes are particularly well-suited for protecting critical sections in systems with real-time constraints.
In this blog post, we demonstrated how to implement and use a mutex in FreeRTOS on the ESP-WROVER-KIT to coordinate UART output between two tasks. This is just the beginning—mutexes are widely applicable in more complex applications, including motor control, file system access, and sensor data processing.
By mastering mutexes, you gain one of the most essential tools for building stable and predictable FreeRTOS-based systems on the ESP32 and other real-time platforms.