HomeArduino TutorialsArduino FreeRTOSFreeRTOS on Arduino (Part 5): Semaphores and Mutex Explained with Practical Examples

FreeRTOS Semaphores and Mutex Explained with Practical Examples

FreeRTOS makes Arduino far more powerful by allowing multiple tasks to run at the same time, which is extremely useful for building real-world embedded applications. However, once multitasking is introduced, new problems also start to appear, especially when different tasks try to access the same hardware or software resources.

When two or more tasks use a shared resource without proper control, the behavior becomes unpredictable. Serial output may get mixed or corrupted, button inputs may trigger multiple times, and in some cases the entire application may freeze or crash. These issues are very common when working with FreeRTOS for the first time.

This is where semaphores and mutex play a critical role. They provide a safe way for tasks to communicate with each other and to protect shared resources from being accessed at the same time. Without them, writing reliable FreeRTOS-based Arduino applications becomes very difficult.

In this tutorial, you will learn how FreeRTOS semaphores and mutex work on Arduino, explained in simple language and supported by practical examples. Instead of focusing on heavy theory, the goal is to show how these tools are actually used in real projects and why they are necessary.

FreeRTOS Semaphores and Mutex Explained with Practical Examples

Understanding Semaphores in FreeRTOS on Arduino

In FreeRTOS, tasks often need a way to coordinate with each other. Sometimes a task must wait for an event, and sometimes it needs a signal from another task or an interrupt. Semaphores provide this signaling mechanism. They help tasks work together without constantly checking conditions, which keeps the system efficient and responsive.

On Arduino, semaphores are especially useful when dealing with inputs like buttons, sensors, or interrupts. Instead of writing blocking code or using delays, FreeRTOS semaphores allow tasks to sleep until something important happens. This makes your application cleaner and more reliable.

What Is a Semaphore in FreeRTOS

A semaphore in FreeRTOS is a synchronization object used to signal events between tasks or between an interrupt and a task. It does not protect data by itself. Instead, it tells a task that something has happened and that it can continue execution.

Think of a semaphore as a flag or token. One part of the code “gives” the semaphore when an event occurs. Another task “takes” the semaphore and runs only after the signal is received. If the semaphore is not available, the task simply waits without wasting CPU time.


Binary Semaphore vs Counting Semaphore

FreeRTOS provides two main types of semaphores: binary semaphores and counting semaphores. The difference lies in how many signals they can store.

A binary semaphore can have only two states: available or unavailable. It is either given or taken. This makes it ideal for simple event signaling, such as detecting a button press or notifying a task that an interrupt has occurred.

A counting semaphore, on the other hand, can hold multiple counts. Each time the semaphore is given, its count increases. Each time it is taken, the count decreases. This is useful when multiple events may occur before a task gets a chance to handle them, such as counting pulses or tracking available resources.

The table below highlights the difference between Binary and Counting semaphore in FreeRTOS.

FeatureBinary SemaphoreCounting Semaphore
Maximum count1Greater than 1
StatesAvailable or unavailableMultiple counts
PurposeSignal a single eventTrack multiple events or resources
Typical use caseButton press, interrupt signalPulse counting, resource availability
Event storageCan store only one eventCan store multiple events
Task behaviorTask waits until semaphore is givenTask waits until count is greater than zero
Common Arduino exampleButton press notificationCounting sensor triggers
Missed eventsPossible if given multiple timesNo, events are counted
ComplexitySimple to useSlightly more complex

When Should You Use a Semaphore

You should use a semaphore when:

  • A task needs to wait for an event instead of continuously checking for it in a loop. This avoids busy waiting and keeps the CPU free for other tasks.
  • You want to improve system performance by allowing tasks to sleep until an event actually occurs.
  • You are handling button inputs, where a task should run only when the button is pressed.
  • You need to signal a task from an interrupt, such as a timer interrupt or external interrupt.
  • You want to enable basic task-to-task communication, where one task notifies another that some work is ready.

Semaphores work best when the goal is event notification, not resource protection.

You should not use a semaphore when:

  • You need to protect a shared resource like Serial, I2C, SPI, or shared memory.

In such cases, a mutex is the correct solution. A mutex ensures exclusive access and supports priority inheritance. We will explore this difference in detail in the next section.

Hands-On: Using Binary Semaphore for Button Input

This section demonstrates event-based programming in FreeRTOS using a binary semaphore. A binary semaphore is ideal for signaling discrete events, such as a button press, from one task to another. Here, we’ll see how pressing a button can safely toggle an LED.

Creating a Binary Semaphore

A binary semaphore allows one task (or interrupt) to signal another task that an event has occurred. On Arduino, we create it like this:

#include <semphr.h>

SemaphoreHandle_t ButtonSemaphore;

ButtonSemaphore = xSemaphoreCreateBinary();

Initially, the semaphore is empty. It will be “given” by the button task and “taken” by the LED task.


Button Task: Giving the Semaphore

The button task monitors a digital input and gives the semaphore whenever the button is pressed:

void ButtonTask(void *pvParameters) {
  const int buttonPin = 2;
  pinMode(buttonPin, INPUT_PULLUP);

  while (1) {
    if (digitalRead(buttonPin) == LOW) { // Button pressed
      xSemaphoreGive(ButtonSemaphore);
      vTaskDelay(150 / portTICK_PERIOD_MS); // Debounce delay
    }
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}

This simple debounce ensures the semaphore is only given once per press.


LED Task: Taking the Semaphore

The LED task waits for the semaphore. When it receives it, it toggles the LED:

void LEDTask(void *pvParameters) {
  const int ledPin = 13;
  pinMode(ledPin, OUTPUT);

  while (1) {
    if (xSemaphoreTake(ButtonSemaphore, portMAX_DELAY) == pdTRUE) {
      digitalWrite(ledPin, !digitalRead(ledPin)); // Toggle LED
    }
  }
}

The task blocks until the semaphore is given, ensuring event-driven execution without busy-waiting.


Full Code: Binary Semaphore Example

#include <Arduino_FreeRTOS.h>
#include <semphr.h>

SemaphoreHandle_t ButtonSemaphore;

void ButtonTask(void *pvParameters);
void LEDTask(void *pvParameters);

void setup() {
  Serial.begin(9600);

  ButtonSemaphore = xSemaphoreCreateBinary();

  xTaskCreate(ButtonTask, "Button", 128, NULL, 2, NULL);
  xTaskCreate(LEDTask, "LED", 128, NULL, 1, NULL);

  vTaskStartScheduler();
}

void loop() {
  // Not used
}

void ButtonTask(void *pvParameters) {
  const int buttonPin = 2;
  pinMode(buttonPin, INPUT_PULLUP);

  while (1) {
    if (digitalRead(buttonPin) == LOW) {
      xSemaphoreGive(ButtonSemaphore);
      vTaskDelay(150 / portTICK_PERIOD_MS);  // debouncing
    }
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}

void LEDTask(void *pvParameters) {
  const int ledPin = 13;
  pinMode(ledPin, OUTPUT);

  while (1) {
    if (xSemaphoreTake(ButtonSemaphore, portMAX_DELAY) == pdTRUE) {
      digitalWrite(ledPin, !digitalRead(ledPin));
    }
  }
}

Output: Binary Semaphore

  • Press the button once -> LED toggles once.
  • Press the button again -> LED toggles again.
  • The CPU remains free while waiting for the event.

The gif below shows the LED toggling in response to button presses, demonstrating event-driven communication using a binary semaphore.

gif shows the LED toggling in response to button presses, demonstrating event-driven communication using a binary semaphore on Arduino FreeRTOS.

Hands-On: Counting Semaphore with Multiple Tasks in FreeRTOS on Arduino

In this hands-on example, we use a counting semaphore to control access to a limited shared resource across multiple FreeRTOS tasks on Arduino. Unlike a binary semaphore, a counting semaphore allows more than one task to access the resource at the same time, up to a defined limit.

This example uses three tasks with different priorities competing for two shared resources.

Scenario Overview

  • We have three tasks:
    • High Priority Task (HPT)
    • Medium Priority Task (MPT)
    • Low Priority Task (LPT)
  • We have two shared resources stored in an array: int resource[2] = {111, 222};
  • A counting semaphore with two tokens controls access to these resources.

Only two tasks can access the resource at the same time. Any additional task must wait until a token becomes available.


Creating the Counting Semaphore

The counting semaphore is created with a maximum count of 2 and an initial count of 0:

CountingSem = xSemaphoreCreateCounting(2, 0);

At startup, the semaphore has no tokens, so no task can proceed until tokens are given.


Initializing the Semaphore Tokens

The High Priority Task (HPT) gives both semaphore tokens at the beginning:

xSemaphoreGive(CountingSem);
xSemaphoreGive(CountingSem);

This makes two tokens available, matching the number of shared resources. From this point onward, tasks can start acquiring the semaphore.


Task Behavior: Acquiring the Semaphore

Each task follows the same pattern:

  1. Print entry message
  2. Check how many semaphore tokens are available
  3. Try to acquire the semaphore
  4. Access the shared resource
  5. Do not release the semaphore

Example from the HPT task:

Serial.println("Entered HPT Task\n About to ACQUIRE the Semaphore");
Serial.print("Tokens available are: ");
Serial.println(uxSemaphoreGetCount(CountingSem));

xSemaphoreTake(CountingSem, portMAX_DELAY);

If no token is available, the task blocks automatically until one becomes free.


Accessing the Shared Resource

Once the semaphore is taken, the task accesses the resource array:

Serial.print("Leaving HPT Task\n Data ACCESSED is:: ");
Serial.println(resource[indx]);

The index is then incremented and wrapped safely:

indx++;
if (indx > 1) indx = 0;

This simulates controlled access to limited shared data.


Why the Semaphore Is Not Released

In this example, tasks do not give the semaphore back intentionally:

Serial.println("Not releasing the Semaphore");

This is done to demonstrate how:

  • Semaphore tokens get exhausted
  • Lower-priority tasks get blocked
  • Resource availability directly affects task execution

This behavior helps visualize how counting semaphores limit concurrency.


Task Priorities and Scheduling

Tasks are created with descending priorities:

xTaskCreate(HPT_TASK, "HPT", 128, NULL, 3, &HPThandler);
xTaskCreate(MPT_TASK, "MPT", 128, NULL, 2, &MPThandler);
xTaskCreate(LPT_TASK, "LPT", 128, NULL, 1, &LPThandler);
  • Higher-priority tasks run first
  • Lower-priority tasks wait when tokens are unavailable
  • FreeRTOS handles blocking and scheduling automatically

Full Code: Counting Semaphore Example on Arduino

#include <Arduino_FreeRTOS.h>
#include <semphr.h>

TaskHandle_t HPThandler;
void HPT_TASK (void *pvParameters);

TaskHandle_t MPThandler;
void MPT_TASK (void *pvParameters);

TaskHandle_t LPThandler;
void LPT_TASK (void *pvParameters);

// semaphore related
SemaphoreHandle_t CountingSem;

// resource related
int resource[2] = {111,222};
int indx = 0;

void setup()
{
  Serial.begin(9600);
  while (!Serial);

  CountingSem = xSemaphoreCreateCounting(2,0);
  if (CountingSem == NULL)
    Serial.println("Unable to Create Semaphore\n\n");
  else
    Serial.println("Counting Semaphore created successfully\n\n");

  xTaskCreate(HPT_TASK, "HPT", 128, NULL, 3, &HPThandler);
  xTaskCreate(MPT_TASK, "MPT", 128, NULL, 2, &MPThandler);
  xTaskCreate(LPT_TASK, "LPT", 128, NULL, 1, &LPThandler);
}

void loop() {}

void HPT_TASK (void *pvParameters)
{
  // Give 2 semaphores at the beginning
  xSemaphoreGive(CountingSem);
  xSemaphoreGive(CountingSem);

  while (1)
  {
    Serial.println("Entered HPT Task\n About to ACQUIRE the Semaphore");
    Serial.print("Tokens available are: ");
    Serial.println(uxSemaphoreGetCount(CountingSem));

    xSemaphoreTake(CountingSem, portMAX_DELAY);

    Serial.print("Leaving HPT Task\n Data ACCESSED is:: ");
    Serial.println(resource[indx]);
    Serial.println("Not releasing the Semaphore\n\n");

    indx++;
    if (indx > 1) indx = 0;

    vTaskDelay(3000);
  }
}

void MPT_TASK (void *pvParameters)
{
  while (1)
  {
    Serial.println("Entered MPT Task\n About to ACQUIRE the Semaphore");
    Serial.print("Tokens available are: ");
    Serial.println(uxSemaphoreGetCount(CountingSem));

    xSemaphoreTake(CountingSem, portMAX_DELAY);

    Serial.print("Leaving MPT Task\n Data ACCESSED is:: ");
    Serial.println(resource[indx]);
    Serial.println("Not releasing the Semaphore\n\n");

    indx++;
    if (indx > 1) indx = 0;

    vTaskDelay(3000);
  }
}

void LPT_TASK (void *pvParameters)
{
  while (1)
  {
    Serial.println("Entered LPT Task\n About to ACQUIRE the Semaphore");
    Serial.print("Tokens available are: ");
    Serial.println(uxSemaphoreGetCount(CountingSem));

    xSemaphoreTake(CountingSem, portMAX_DELAY);

    Serial.print("Leaving LPT Task\n Data ACCESSED is:: ");
    Serial.println(resource[indx]);
    Serial.println("Not releasing the Semaphore\n\n");

    indx++;
    if (indx > 1) indx = 0;

    vTaskDelay(3000);
  }
}

Output

At the beginning, you will see:

Counting Semaphore created successfully

Then, as tasks run:

  • Only two tasks at a time acquire the semaphore
  • The third task blocks
  • Token count decreases and eventually reaches zero

The image below shows how tasks enter, acquire tokens, access the resource, and block when no semaphore tokens are left.

image shows how tasks enter, acquire tokens, access the resource, and block when no counting semaphore tokens are left in Arduino FreeRTOS.

What is a Mutex

A mutex is a special type of synchronization object used to protect shared resources. The word mutex comes from mutual exclusion, which means only one task can use the resource at a time. It prevents several tasks from accessing a resource mutually. It ensures that at one time, only one task have access to the resource.

What Is Priority Inversion in FreeRTOS

In FreeRTOS, each task runs with a defined priority. A task with a higher priority should always preempt and run before a lower-priority task. This is how a real-time system guarantees predictable behavior. However, this rule can break down when multiple tasks share the same resource.

Priority inversion occurs when a low-priority task takes a shared resource, and a high-priority task later needs that same resource. Instead of running immediately, the high-priority task is forced to wait until the low-priority task releases it. At this point, the scheduling order is already inverted.

The situation becomes more serious when a medium-priority task is also present. While the high-priority task is blocked waiting for the resource, the medium-priority task continues to run and preempts the low-priority task. As a result, the low-priority task cannot finish its work and release the resource, and the high-priority task remains blocked for an unpredictable amount of time.

This behavior breaks real-time guarantees and is known as priority inversion. In real-time systems, this is a critical problem that must be handled correctly.


Priority Inversion Example on Arduino

The following Arduino FreeRTOS example demonstrates priority inversion clearly.
Three tasks are created with different priorities, and a binary semaphore is used to protect a shared resource. Since a binary semaphore does not support priority inheritance, priority inversion occurs.

Task Priorities

  • Low Priority Task (LPT): Priority 1
  • Medium Priority Task (MPT): Priority 2
  • High Priority Task (HPT): Priority 3

Arduino FreeRTOS Code Demonstrating Priority Inversion

#include <Arduino_FreeRTOS.h>
#include <semphr.h>

SemaphoreHandle_t BinSemaphore;

void HPT_Task(void *pvParameters);
void MPT_Task(void *pvParameters);
void LPT_Task(void *pvParameters);

void setup()
{
  Serial.begin(9600);
  while (!Serial);

  BinSemaphore = xSemaphoreCreateBinary();
  xSemaphoreGive(BinSemaphore);

  xTaskCreate(LPT_Task, "LPT", 128, NULL, 1, NULL);
  xTaskCreate(MPT_Task, "MPT", 128, NULL, 2, NULL);
  xTaskCreate(HPT_Task, "HPT", 128, NULL, 3, NULL);

  vTaskStartScheduler();
}

void loop() {}

void LPT_Task(void *pvParameters)
{
  while (1)
  {
    Serial.println("Entered LPT and About to take Semaphore\r\n");
    xSemaphoreTake(BinSemaphore, portMAX_DELAY);

    Serial.println("IN LPT...........................\r\n");
    vTaskDelay(3000 / portTICK_PERIOD_MS);   // holds resource

    Serial.println("Leaving LPT\r\n");
    xSemaphoreGive(BinSemaphore);

    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

void MPT_Task(void *pvParameters)
{
  while (1)
  {
    Serial.println("In MPT*******************************\r\n");
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

void HPT_Task(void *pvParameters)
{
  while (1)
  {
    Serial.println("Entered HPT and About to take Semaphore\r\n");
    xSemaphoreTake(BinSemaphore, portMAX_DELAY);  // blocks here
    
    Serial.println("In HPT===========================\r\n");

    Serial.println("Leaving HPT\r\n");
    xSemaphoreGive(BinSemaphore);

    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

What Happens in This Example

First, the low-priority task takes the semaphore and starts using the shared resource. While it is holding the resource, the high-priority task becomes ready and tries to take the semaphore. Since the resource is already locked, the high-priority task blocks and waits.

At the same time, the medium-priority task continues to run repeatedly. Because it does not need the semaphore and has a higher priority than the low-priority task, it keeps preempting it. This prevents the low-priority task from running and releasing the resource.

As a result, the high-priority task remains blocked even though it has the highest priority. This is true priority inversion, caused by using a synchronization mechanism that does not support priority inheritance.


Output

The images below shows that the medium-priority task keeps printing messages while the high-priority task waits. The low-priority task takes a long time to release the resource, creating unexpected delays and breaking real-time behavior.

How priority inversion occurs in FreeRTOS when using binary semaphore.
How priority inversion occurs in FreeRTOS when using binary semaphore.

In the next section, we will fix this exact problem using a mutex and see how priority inheritance completely removes priority inversion.


Priority Inheritance in FreeRTOS

Priority inheritance is the mechanism FreeRTOS uses to solve priority inversion. It works only with a mutex, not with a normal semaphore. When a mutex is used, FreeRTOS automatically adjusts task priorities to make sure high-priority tasks are not blocked for long periods.

In this section, we will use almost the same code as before. The only difference is that we replace the binary semaphore with a mutex. This small change completely fixes the problem.

How Priority Inheritance Works

When a low-priority task holds a mutex and a high-priority task tries to take it, FreeRTOS temporarily raises the priority of the low-priority task to match the high-priority task. This prevents any medium-priority task from preempting it.

Once the low-priority task releases the mutex, its original priority is restored automatically. The high-priority task then runs immediately.

This behavior ensures predictable timing and stable real-time performance.


Priority Inheritance Arduino Example Using Mutex

This example shows how priority inheritance works in practice. We create three tasks with different priorities that all interact with a shared resource protected by a mutex. When the high-priority task requests the mutex, FreeRTOS temporarily raises the priority of the low-priority task so it can finish quickly and release the resource.

Arduino Code for Priority Inheritance

#include <Arduino_FreeRTOS.h>
#include <semphr.h>

SemaphoreHandle_t sharedMutex;

// ---------------- LOW PRIORITY TASK ----------------
void LowPriorityTask(void *pvParameters)
{
  while (1)
  {
    if (xSemaphoreTake(sharedMutex, portMAX_DELAY))
    {
      Serial.print("LowPriorityTask: Holding resource | Priority: ");
      Serial.println(uxTaskPriorityGet(NULL));

      // Simulate long critical section
      vTaskDelay(2000 / portTICK_PERIOD_MS);

      Serial.print("LowPriorityTask: Releasing resource | Priority: ");
      Serial.println(uxTaskPriorityGet(NULL));

      xSemaphoreGive(sharedMutex);
    }

    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

// ---------------- MEDIUM PRIORITY TASK ----------------
void MediumPriorityTask(void *pvParameters)
{
  while (1)
  {
    Serial.println("MediumPriorityTask: Doing background work");
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
}

// ---------------- HIGH PRIORITY TASK ----------------
void HighPriorityTask(void *pvParameters)
{
  while (1)
  {
    // Let Low Priority Task grab mutex first
    vTaskDelay(1000 / portTICK_PERIOD_MS);

    Serial.println("HighPriorityTask: Needs shared resource");

    if (xSemaphoreTake(sharedMutex, portMAX_DELAY))
    {
      Serial.println("HighPriorityTask: Got shared resource");

      vTaskDelay(1000 / portTICK_PERIOD_MS);

      Serial.println("HighPriorityTask: Releasing shared resource");
      xSemaphoreGive(sharedMutex);
    }
  }
}

void setup()
{
  Serial.begin(9600);
  while (!Serial);

  sharedMutex = xSemaphoreCreateMutex();
  if (sharedMutex == NULL)
  {
    Serial.println("Failed to create mutex!");
    while (1);
  }

  // Priority: High > Medium > Low
  xTaskCreate(LowPriorityTask, "LowTask", 128, NULL, 1, NULL);
  xTaskCreate(MediumPriorityTask, "MediumTask", 128, NULL, 2, NULL);
  xTaskCreate(HighPriorityTask, "HighTask", 128, NULL, 3, NULL);
}

void loop() {}

How This Example Shows Priority Inheritance

At first, the low-priority task acquires the mutex and enters a long critical section. While it holds the mutex, the high-priority task wakes up and requests the same resource. Normally, the high-priority task should run first, but it cannot because the resource is locked.

At this point, priority inheritance kicks in. FreeRTOS temporarily raises the priority of the low-priority task to match the high-priority task. This prevents the medium-priority task from preempting it. As a result, the low-priority task finishes faster, releases the mutex, and then returns to its original priority.

Without priority inheritance, the medium-priority task would keep running, delaying the low-priority task and blocking the high-priority task indefinitely. This example clearly shows why mutexes are essential for protecting shared resources and why semaphores alone are not enough in real-time systems.


Output

The image below shows that when the high-priority task requests the mutex, the low-priority task’s reported priority temporarily increases. The medium-priority task stops interfering during this time, proving that priority inheritance is working correctly.

image shows that when the high-priority task requests the mutex, the low-priority task’s reported priority temporarily increases. The medium-priority task stops interfering during this time, proving that priority inheritance is working correctly.

Semaphore vs Mutex in FreeRTOS

Mutex and semaphore are both synchronization tools in FreeRTOS, but they are designed to solve different problems. They may look similar in code, yet using the wrong one can easily break real-time behavior. This section explains the difference in simple terms and helps you decide which one to use in your Arduino FreeRTOS projects.

What a Semaphore Is Used For

A semaphore in FreeRTOS is mainly used for signaling events. It allows one task, or an interrupt, to notify another task that something has happened. Semaphores do not represent ownership of a resource. Any task can give or take a semaphore. Because of this, semaphores do not support priority inheritance.

Common examples include button presses, interrupts, and task-to-task notifications.


What a Mutex Is Used For

A mutex is designed specifically for protecting shared resources. Only one task can own a mutex at a time, and the same task must release it after use. The key feature of a mutex is priority inheritance. If a low-priority task holds a mutex and a high-priority task requests it, FreeRTOS temporarily raises the priority of the low-priority task. This prevents priority inversion and ensures predictable execution.

Mutexes should always be used when tasks share hardware or software resources such as Serial, I2C, SPI, or global data.


Why Mutex and Semaphore Are Not the Same

Although both mutex and semaphore are created using the FreeRTOS API, they behave very differently internally.

A semaphore is about events.
A mutex is about ownership.

Using a semaphore to protect a shared resource may work in simple cases, but it can cause priority inversion and unstable timing as the system grows more complex. This is why FreeRTOS strongly recommends using a mutex for resource protection.


Mutex vs Semaphore in FreeRTOS: Comparison Table

FeatureSemaphoreMutex
Primary purposeEvent signalingResource protection
OwnershipNo ownershipOwned by one task
Priority inheritanceNoYes
Can be used in ISRYesNo
Used for interruptsYesNo
Prevents priority inversionNoYes
Typical Arduino useButton, ISR, task signalingSerial, I2C, SPI
Recommended for shared resourcesNoYes

When to Use a Semaphore

You should use a semaphore when:

  • A task needs to wait for an event to occur
  • You are signaling from an interrupt to a task
  • You are handling button presses or sensor triggers
  • Tasks need simple notification, not resource locking

Semaphores are ideal for event-driven programming.


When to Use a Mutex

You should use a mutex when:

  • Multiple tasks share a critical resource
  • Resource access must be exclusive
  • Task priorities are different
  • Predictable timing is important

Mutexes are the correct tool for resource protection.

Hands-On: Using Mutex for Serial.print() in FreeRTOS

Even though short Serial.println() calls often work fine on Arduino, Serial is still a shared resource. If multiple FreeRTOS tasks access it simultaneously, especially with longer messages, multiple consecutive prints, or more complex task scheduling, unprotected access can lead to unpredictable timing or issues in more complex projects.

Using a mutex guarantees safe access, ensures priority inheritance, and makes your system predictable.

Problem Without Mutex: Why It Matters

Consider two tasks printing to Serial:

Serial.println("Task 1: Start of message");
Serial.println("Task 2: Another message");

Even if the output appears fine, both tasks are competing for the same resource. If your project grows with longer messages, multi-part prints, or high-frequency tasks, the unprotected access may lead to:

  • Messages appearing out of order
  • Unexpected timing delays
  • Potential conflicts with other hardware (I2C, SPI)

So the “problem” is theoretical on short prints, but practical in real projects.


Creating a Mutex for Serial Port

Before accessing Serial, create a mutex:

SemaphoreHandle_t SerialMutex;
SerialMutex = xSemaphoreCreateMutex();

This ensures only one task at a time can use Serial, and FreeRTOS will apply priority inheritance automatically if a high-priority task requests the mutex.


Two Tasks Printing Safely Using Mutex

Each task should take the mutex before printing and release it afterward:

xSemaphoreTake(SerialMutex, portMAX_DELAY);
Serial.print("Task 1: Safe printing");
Serial.println(" - end");
xSemaphoreGive(SerialMutex);

Similarly, Task 2:

xSemaphoreTake(SerialMutex, portMAX_DELAY);
Serial.print("Task 2: Safe printing");
Serial.println(" - end");
xSemaphoreGive(SerialMutex);

Even though short prints may look fine without a mutex, this approach prevents conflicts when tasks become more complex or use other shared resources.

Conclusion

In this tutorial, we covered how semaphores and mutexes work in FreeRTOS on Arduino, focusing on why they are essential in real multitasking systems. We began by understanding semaphores, including the difference between binary and counting semaphores, and learned how they help tasks wait for events efficiently instead of wasting CPU time. Then we explored mutexes, their role in protecting shared resources, and why they are different from semaphores when mutual exclusion is required.

We also discussed priority inversion, a real problem in real-time systems, and explained how priority inheritance in mutexes solves it. Through practical Arduino examples, we saw how tasks with different priorities interact when accessing shared resources. The hands-on sections showed how a mutex keeps Serial.print() access safe and predictable, how a binary semaphore enables clean event-driven designs such as button handling, and how a counting semaphore manages limited resources shared by multiple tasks.

Overall, this knowledge helps you design stable, responsive, and scalable FreeRTOS applications on Arduino. By choosing the correct synchronization mechanism, you can avoid timing issues, protect critical resources, and build systems that behave reliably even as complexity grows. These concepts form a strong foundation for any real-world embedded project that uses FreeRTOS.

Browse More Arduino FreeRTOS Tutorials

Arduino FreeRTOS Project Download

Info

You can help with the development by DONATING Below.
To download the project, click the DOWNLOAD button.

FAQs – FreeRTOS Semaphore & Mutex

Subscribe
Notify of

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments