Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 52

Protecting Shared Resources with Mutexes

Recommended resources

Hello! Welcome back to our FreeRTOS module.

In our last lesson, we focused on synchronization, using binary semaphores as a lightweight signal to wake up a task from an interrupt. This is a powerful pattern for deferred processing. Today, we shift from signaling to another fundamental challenge in concurrent systems: protecting shared resources.

This lesson directly addresses the learning outcome: Use mutexes to protect shared resources from race conditions. We will explore what happens when multiple tasks try to use the same thing—be it a peripheral like a UART, a global variable, or a sensor on a shared bus—at the same time, and how to prevent the chaos that can ensue.

1. The Problem: Race Conditions

Imagine you have two tasks, and both need to update a global variable that counts a total number of events.

  • Task A reads the counter (value is 10).
  • The RTOS scheduler preempts Task A and switches to Task B.
  • Task B reads the counter (value is still 10), increments it to 11, and writes it back.
  • The scheduler switches back to Task A. Task A, which already read the value 10, now increments it to 11 and writes it back.

Both tasks ran, but the counter only incremented once. This is a race condition: the program's output depends on the unpredictable sequence of task scheduling. The section of code that accesses the shared resource (the counter, in this case) is called a critical section. To prevent race conditions, we must ensure that only one task can be inside its critical section at any given time. This principle is called mutual exclusion.

To see this problem explained visually, let's watch a section of a helpful video.

Introduction to RTOS Part 6 - Mutex | Digi-Key Electronics

This video from DigiKey provides an excellent conceptual and visual breakdown of what a race condition is and why it happens.

Please watch the following two segments: Introduction (00:00 - 00:39): A quick overview of the race condition problem. How a Race Condition Occurs (02:11 - 04:07): A clear, diagrammatic explanation of how two tasks incrementing a shared variable can lead to data corruption.


2. The Solution: The Mutex

To enforce mutual exclusion, FreeRTOS provides a specific tool: the Mutex (short for Mutual Exclusion). Think of a mutex as a key to a room (the critical section). A task must acquire the key before it can enter the room. Since there is only one key, only one task can be in the room at a time. When it leaves, it returns the key for another task to use.

Let's explore the core concepts and API.

FreeRTOS mutex a developers guide to race free embedded systems

This article from BugProve offers a professional-grade overview of FreeRTOS mutexes. We'll start by reading the introduction and the section on implementation.

Read the following sections: "Introduction to FreeRTOS Mutexes": This sets the stage by explaining why mutexes are essential. "Implementing Mutexes in FreeRTOS": Focus on the code example. It introduces the three key API functions we will use: xSemaphoreCreateMutex(): To create the mutex. xSemaphoreTake(): To acquire the 'key'. xSemaphoreGive(): To release the 'key'.


3. Mutex vs. Binary Semaphore

At first glance, a mutex might seem like a binary semaphore, which you learned about in the last lesson. Both can be "taken" or "given". However, their intended use and internal mechanics are very different. Using them interchangeably is a common source of bugs.

Here is a summary of the key differences:

Feature Mutex Binary Semaphore
Primary Use Case Resource Protection Synchronization (Signaling)
Ownership Yes. Only the task that takes a mutex can give it back. No. Any task or ISR can give it.
Priority Inheritance Yes. Solves priority inversion automatically. No. Can lead to priority inversion.
ISR Compatibility No. Cannot be used in an ISR. Yes. Use ...FromISR versions.

The most critical distinctions are ownership and priority inheritance.

  • Ownership: The strict ownership rule of mutexes prevents a situation where one task accidentally releases a lock held by another.
  • Priority Inheritance: This is a crucial mechanism that solves a dangerous problem in real-time systems called priority inversion.

4. Priority Inversion and Priority Inheritance

Priority inversion is a subtle but critical problem. Let's imagine this scenario:

  1. A Low Priority Task (LPT) takes a mutex to access a shared resource.
  2. A High Priority Task (HPT) needs the same resource, tries to take the mutex, and blocks (it must wait for the LPT to release it).
  3. A Medium Priority Task (MPT), which is ready to run and does not need the mutex, preempts the LPT.

The result: The HPT is stuck waiting for the LPT, but the LPT can't run to release the mutex because the MPT is running instead. The high-priority task is effectively blocked by a medium-priority task. This can cause catastrophic deadline misses.

FreeRTOS mutexes solve this automatically with priority inheritance.

When the HPT blocks on the mutex held by the LPT, the RTOS scheduler temporarily boosts the LPT's priority to that of the HPT. Now, the MPT cannot preempt the LPT. The LPT finishes its critical section, releases the mutex, and its priority is returned to normal. The HPT can then immediately take the mutex and run.

FreeRTOS Mutex Tutorial for STM32

The following article from ControllersTech demonstrates this exact scenario with code. It first shows how a binary semaphore leads to priority inversion and then how a mutex solves it through priority inheritance.

Please read these two sections back-to-back: "Priority Inversion in Semaphore": Observe how the Medium Priority Task runs while the High Priority Task is waiting. "Priority Inheritance using Mutex": See how changing just one line (using a mutex instead of a semaphore) fixes the problem. The LPT's priority is inherited, preventing the MPT from running.

The trace visualization below shows priority inheritance in action. A task holding a mutex (Tmr Svc) has its priority elevated ("Inherited by Rx") when a higher-priority task (Rx) needs the same mutex.

FreeRTOS Mutex and Priority Inheritance Trace
A professional trace tool showing a task's priority being temporarily elevated (inherited) while it holds a mutex needed by a higher-priority task.

5. Hands-On: Protecting UART Output

Let's demonstrate a race condition and fix it with a mutex. We'll create two tasks that both write to the UART serial port. Without protection, their messages will get mixed together, creating a garbled mess.

Step 1: Set up the Tasks

In main.c, add the following code. First, create two task functions. One will have a higher priority than the other.

/* USER CODE BEGIN 4 */
void vHighPriorityTask(void *pvParameters)
{
    while(1)
    {
        // This task will try to print 'A's
        printf("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n");
        vTaskDelay(pdMS_TO_TICKS(100)); // Small delay
    }
}

void vLowPriorityTask(void *pvParameters)
{
    while(1)
    {
        // This task will try to print 'B's
        printf("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\r\n");
        vTaskDelay(pdMS_TO_TICKS(100)); // Small delay
    }
}
/* USER CODE END 4 */

In main(), before the scheduler starts, create these two tasks. Notice the priority difference (2 vs 1).

/* USER CODE BEGIN RTOS_TASKS */
/* Create the thread(s) */
/* definition and creation of defaultTask */
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);

/* USER CODE HERE */
xTaskCreate(vHighPriorityTask, "HighPriTask", 128, NULL, 2, NULL);
xTaskCreate(vLowPriorityTask, "LowPriTask", 128, NULL, 1, NULL);
/* USER CODE END RTOS_TASKS */

Note: Make sure your project's printf is redirected to UART, which we covered in a previous lesson.

Compile and run this code. Open your serial terminal. You will see a mess of jumbled 'A's and 'B's, because the high-priority task is constantly preempting the low-priority task mid-printf. This is our race condition.

Step 2: Implement the Mutex

Now, let's protect the printf call.

  1. Declare a mutex handle globally in main.c:

    /* USER CODE BEGIN PV */
    SemaphoreHandle_t xUartMutex;
    /* USER CODE END PV */
    
  2. Create the mutex in main() before the scheduler starts:

    /* USER CODE BEGIN 2 */
    xUartMutex = xSemaphoreCreateMutex();
    if(xUartMutex == NULL)
    {
        // Mutex creation failed. Handle error.
        while(1);
    }
    /* USER CODE END 2 */
    
  3. Protect the critical sections. Modify both task functions to take the mutex before printing and give it back immediately after.

    /* USER CODE BEGIN 4 */
    void vHighPriorityTask(void *pvParameters)
    {
        while(1)
        {
            if(xSemaphoreTake(xUartMutex, portMAX_DELAY) == pdTRUE)
            {
                // We have the mutex, so we can safely print.
                printf("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n");
                xSemaphoreGive(xUartMutex);
            }
            vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
    
    void vLowPriorityTask(void *pvParameters)
    {
        while(1)
        {
            if(xSemaphoreTake(xUartMutex, portMAX_DELAY) == pdTRUE)
            {
                // We have the mutex, so we can safely print.
                printf("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\r\n");
                xSemaphoreGive(xUartMutex);
            }
            vTaskDelay(pdMS_TO_TICKS(100));
        }
    }
    /* USER CODE END 4 */
    

    Using portMAX_DELAY as the timeout means the task will wait indefinitely if the mutex isn't available. In a real application, you should use a sensible timeout to detect if a task is holding the mutex for too long.

Step 3: Verify the Fix

Compile and flash this new code. Now, when you view the serial terminal, you will see clean, uninterrupted lines of 'A's and 'B's. The mutex has successfully protected the shared UART resource.

For a video walkthrough of a very similar example, you can watch this.

FreeRTOS Tutorial 7 || MUTEX || STM32 || CubeIDE

This video from ControllersTech provides a clear, practical demonstration of creating tasks and protecting a shared UART with a mutex in STM32CubeIDE.

Watch from 03:00 to 06:28. This covers: Defining mutex and task handles. Creating the mutex and tasks. Writing the task functions that take and give the mutex to protect a Send_Uart function.


Conclusion

You have now learned how to solve one of the most common and dangerous problems in embedded systems. By understanding and correctly applying mutexes, you can write robust, thread-safe code where multiple tasks can coexist without corrupting shared data or peripherals.

Key Takeaways:

  • A race condition occurs when multiple tasks access a shared resource, and the result depends on the unpredictable order of execution. The code accessing the resource is a critical section.
  • A mutex is a locking mechanism used to ensure mutual exclusion, allowing only one task to enter its critical section at a time.
  • Mutexes have ownership: only the task that takes a mutex can release it. This makes them ideal for resource protection, unlike binary semaphores which are for signaling.
  • Priority inversion is a critical issue where a high-priority task is blocked by a lower-priority task.
  • FreeRTOS mutexes automatically solve this using priority inheritance, temporarily boosting the priority of the mutex-holding task.

In our next and final lesson of this module, we will put everything together. You will refactor a simple, polling-based application into a sophisticated, event-driven, multi-tasked system using the FreeRTOS tools you have now mastered: tasks, queues, semaphores, and mutexes.