Hello! Welcome back.
In our last lesson, we learned how to create, manage, and prioritize tasks in FreeRTOS. This ability to run multiple tasks concurrently is the core power of an RTOS, but it also introduces a fundamental challenge: how do we manage access to shared resources safely? When two or more tasks try to read or write to the same global variable, memory buffer, or peripheral at the same time, the result is often data corruption and unpredictable system behavior. This is known as a race condition.
Today's lesson directly addresses this critical problem, aligning with the learning outcome: Implement task synchronization using mutexes to protect shared resources and prevent race conditions. We'll explore what race conditions are, how mutexes provide a solution, and how to implement them correctly in FreeRTOS. Your experience with communication protocols and shared memory in AUTOSAR will provide a solid foundation for understanding the importance of these synchronization mechanisms.
1. The Problem: Race Conditions and Critical Sections
In a multitasking environment, a "shared resource" can be anything that multiple tasks need to access, such as:
- A global variable used as a counter or status flag.
- A communication peripheral like a UART, SPI, or I2C bus.
- A shared memory buffer for data exchange.
A race condition occurs when the system's behavior depends on the unpredictable sequence or timing of events, like how the RTOS scheduler interleaves the execution of multiple tasks. If two tasks access a shared resource without any coordination, the last one to write "wins," potentially overwriting changes made by the other and corrupting the data.
Introduction to RTOS Part 6 - Mutex | Digi-Key Electronics
To see exactly why this is a problem, let's watch a short video from Digi-Key that provides an excellent explanation of a race condition.
Watch from the beginning to 04:07. The video uses a simple diagram to show how two tasks incrementing a shared variable can lead to an incorrect final value if their operations are interleaved by a context switch. This is the classic example of a race condition.
As the video explained, the issue arises because an operation like shared_variable++ is not atomic. In assembly, it's a three-step process:
- Read the variable's value from memory into a CPU register.
- Modify the value in the register.
- Write the new value from the register back to memory.
If a context switch occurs after one task has read the value but before it has written it back, another task can perform the same operation on the old value, leading to lost updates.
The section of code that accesses the shared resource is called a critical section. To prevent race conditions, we must enforce mutual exclusion, which means ensuring that only one task can execute its critical section at any given time.

Your work with communication stacks like CAN and Ethernet involves managing shared hardware resources. Without mutual exclusion, one task could be in the middle of sending a CAN frame while another task preempts it and tries to reconfigure the CAN controller's bit rate, leading to chaos.
2. The Solution: The Mutex
A Mutex (short for Mutual Exclusion) is a kernel object designed specifically for this purpose. It acts like a key to a shared resource.
- Before entering a critical section, a task must "take" or "acquire" the mutex.
- If the mutex is available, the task takes it and proceeds. The mutex is now considered "locked."
- If another task tries to take the same mutex while it's locked, the RTOS will block that task (move it to the Blocked state) until the mutex becomes available.
- After leaving the critical section, the task must "give" or "release" the mutex, making it available for other tasks.
The key analogy is very effective: imagine a single key for a restroom in a busy coffee shop. Only the person holding the key can enter. Anyone else who wants to use it has to wait for the key to be returned.
3. Implementing Mutexes in FreeRTOS
Let's move from theory to practice. Implementing mutexes in FreeRTOS involves three main API functions.
FreeRTOS mutex for task synchronization
The article 'FreeRTOS mutex for task synchronization' from BugProve provides a fantastic, in-depth guide. We will use it to understand the API and a practical implementation.
First, quickly read the two short FAQ sections, 'How do you implement a mutex in FreeRTOS?' and 'How do you protect shared resources using mutexes in FreeRTOS?'. These provide a high-level summary of the functions we're about to use.
The core FreeRTOS mutex API consists of:
SemaphoreHandle_t xSemaphoreCreateMutex(void): Creates a mutex and returns a handle to it. You must haveconfigUSE_MUTEXESset to 1 in yourFreeRTOSConfig.hfile.BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait): Acquires the mutex. If the mutex isn't available, it will wait for up toxTicksToWaitticks. ReturningpdTRUEmeans success.BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore): Releases the mutex.
Now, let's see a complete, practical example.
FreeRTOS mutex for task synchronization
This same article provides an excellent, well-commented code example of a temperature monitoring system with two tasks sharing data. This is a perfect template for real-world applications.
Study the code in the 'Simple Mutex Operation Example' section. Pay close attention to the numbered comments, as they highlight the key steps: creating the mutex (3), acquiring it with a timeout (5, 11), keeping the critical section short (6, 12), and always releasing the mutex (7, 13).
The example you just read demonstrates several best practices:
- Minimal Critical Section: The tasks copy the shared data to a local variable immediately after taking the mutex, then release the mutex before doing any lengthy processing. This minimizes the time other tasks are kept waiting.
- Use Timeouts: Using a specific timeout (
pdMS_TO_TICKS(100)) withxSemaphoreTakeis safer than blocking forever (portMAX_DELAY). It allows the system to detect if a mutex is held for too long and handle the error gracefully. - Initialization: The mutex is created once during system initialization, before the tasks that use it are started.
This video provides a live coding demonstration that reinforces these steps.
Introduction to RTOS Part 6 - Mutex | Digi-Key Electronics
Let's watch the Digi-Key video again, this time focusing on the practical implementation section.
Watch from 09:32 to 11:36. The video walks through modifying the race condition demo from earlier by adding the xSemaphoreCreateMutex, xSemaphoreTake, and xSemaphoreGive calls to protect the shared variable. Observe how this fixes the faulty counter.
4. Mutex vs. Binary Semaphore: A Critical Distinction
This is a frequent point of confusion for developers and a common interview question to gauge your depth of knowledge. A binary semaphore can also be used for mutual exclusion in a simplistic way (it has two states, 0 or 1, like locked/unlocked), but it lacks crucial features that make mutexes the correct and safer choice for resource protection.
FreeRTOS mutex for task synchronization
Let's return to the BugProve article for a definitive comparison.
Read the section 'Mutex vs Semaphore When to Use Each'. The table comparing the features of a Mutex and a Binary Semaphore is the most important part. Focus on the differences in Ownership, Priority Inheritance, and ISR Compatibility.
Here is a summary of the key differences from that table:
| Feature | Mutex | Binary Semaphore |
|---|---|---|
| Primary Use Case | Resource Protection (Mutual Exclusion) | Task Signaling / Synchronization |
| Ownership | Yes. Only the task that took it can give it. | No. Any task or ISR can "give" the semaphore. |
| Priority Inheritance | Yes. Solves priority inversion. | No. |
| ISR Compatibility | No. Cannot be used inside an ISR. | Yes. Using ...FromISR() API variants. |
The ownership model is a critical safety feature. It prevents a bug where one task accidentally releases a lock held by another. The most important difference, however, is priority inheritance, which is our next topic.
5. The Danger of Priority Inversion (A Preview)
When using mutexes, you can encounter a dangerous condition called priority inversion. Imagine this scenario:
- A low-priority task takes a mutex to access a shared I2C bus.
- A high-priority task preempts it and tries to take the same mutex. It can't, so it blocks.
- A medium-priority task (that doesn't need the I2C bus) becomes ready. Since it has a higher priority than the low-priority task holding the mutex, it preempts it and runs.
The result? The high-priority task is stuck waiting not for the low-priority task, but for the medium-priority task to finish. The high-priority task's execution is now effectively dictated by a medium-priority task, "inverting" the priority scheme and potentially causing it to miss critical deadlines.
Everything You Need To Know About Semaphores And Mutexes
This problem is not just theoretical; it famously caused system resets on NASA's Mars Pathfinder rover. The 'Everything You Need To Know About Semaphores And Mutexes' article by Beningo Embedded provides a fantastic summary of this event.
Read the section 'The Mutexes Priority Inheritance Mechanism'. It explains priority inversion and how the Pathfinder mission suffered from it. This is a classic story in embedded systems.
Fortunately, FreeRTOS mutexes have a built-in solution: priority inheritance. When a high-priority task blocks on a mutex held by a low-priority task, the scheduler temporarily boosts the low-priority task's priority to that of the high-priority task. This ensures the low-priority task can finish its critical section quickly and release the mutex, minimizing the high-priority task's blocking time.
6. Interview Preparation: Key Questions
Mastering mutexes is non-negotiable for an embedded role using an RTOS. Expect questions like these:
-
Question: "What is a race condition? Can you give an example relevant to an automotive system?"
- Your Answer: "A race condition occurs when multiple tasks access a shared resource, and the outcome depends on the unpredictable timing of their execution. For example, if one task is updating a global struct with vehicle speed from a CAN message, while another task is reading that same struct to log it, a context switch could happen mid-update. The logging task might read a struct that is partially old and partially new, resulting in corrupted data. To prevent this, the entire read/write operation must be protected by a mutex."
-
Question: "What is the difference between a mutex and a binary semaphore in FreeRTOS? Why can't you just use a binary semaphore for everything?"
- Your Answer: "While they seem similar, their intended purposes are different. A mutex is for protecting shared resources, while a semaphore is for signaling. The three key differences are: 1) Ownership: A mutex must be released by the same task that acquired it, which is a safety mechanism. 2) Priority Inheritance: Mutexes implement priority inheritance to solve priority inversion, a critical feature for real-time systems that semaphores lack. 3) Recursion: Mutexes can be configured to be taken multiple times by the same task, while semaphores cannot. Using a binary semaphore for resource protection is risky because it's vulnerable to priority inversion and accidental release by the wrong context."
-
Question: "Describe the code pattern you would use to safely write to a shared peripheral, like an I2C device, from a FreeRTOS task."
- Your Answer: "First, I'd create a mutex (
xI2C_Mutex) during initialization. In the task, before any I2C transaction, I would callxSemaphoreTake(xI2C_Mutex, xTicksToWait), checking the return value. If successful, I'd perform the I2C write. Crucially, I'd ensurexSemaphoreGive(xI2C_Mutex)is called on all possible exit paths from that code block, including error conditions, to prevent the mutex from being locked forever. I would also keep the critical section—the code between take and give—as short as possible to minimize blocking time for other tasks."
- Your Answer: "First, I'd create a mutex (
Test your understanding!
A task needs to update two fields in a global struct: g_SystemState.voltage and g_SystemState.current. To be efficient, the developer protects each field with a separate mutex: voltageMutex and currentMutex. The code looks like this:
xSemaphoreTake(voltageMutex, portMAX_DELAY);
g_SystemState.voltage = read_voltage();
xSemaphoreTake(currentMutex, portMAX_DELAY);
g_SystemState.current = read_current();
xSemaphoreGive(currentMutex);
xSemaphoreGive(voltageMutex);
Another task does the same operations but takes the mutexes in the opposite order (currentMutex then voltageMutex). What is the potential major problem with this design?
Show answer
This design is highly susceptible to deadlock.
Here's how it can happen:
- Task 1 takes
voltageMutexand is about to takecurrentMutex. - A context switch occurs, and Task 2 runs.
- Task 2 takes
currentMutex. It then tries to takevoltageMutex. - Task 2 blocks because
voltageMutexis held by Task 1. - The scheduler switches back to Task 1. Task 1 tries to take
currentMutex, but it's held by Task 2. Task 1 now blocks.
Now, Task 1 is waiting for Task 2 to release a mutex, and Task 2 is waiting for Task 1 to release a mutex. Neither can proceed, and they are stuck forever. The solution is to either use a single mutex to protect the entire struct, or to ensure all tasks that need both mutexes always acquire them in the exact same order.
Conclusion
Today we've tackled one of the most fundamental concepts in concurrent programming. You now understand how multiple tasks can create race conditions and how to use mutexes to ensure safe, synchronized access to shared resources.
Key Takeaways:
- Uncontrolled access to shared resources by concurrent tasks leads to race conditions.
- A mutex is a kernel object that provides mutual exclusion to protect critical sections of code.
- The standard pattern is
xSemaphoreCreateMutex,xSemaphoreTake, andxSemaphoreGive. - Mutexes are superior to binary semaphores for resource protection due to ownership and priority inheritance.
- Using mutexes introduces the risk of priority inversion and deadlock, which require careful design to mitigate.
Next Lesson Preview:
We've had a glimpse of priority inversion and its solution, priority inheritance. In our next lesson, we will dive deep into this topic. You will learn precisely how priority inversion occurs, how FreeRTOS's priority inheritance mechanism works under the hood, and how to configure it to build robust, deterministic real-time systems.