ESP32 FreeRTOS Mutex Semaphore using ESP-IDF

In this tutorial, we will learn to use ESP32 FreeRTOS mutex semaphore and how to use it with ESP-IDF. Resource management is an important and critical feature in RTOS based applications. In RTOS, we may have more than one tasks that share resources with each other such as peripherals, data, or external devices, etc. For this purpose, FreeRTOS provides a mutex semaphore to share resources between tasks securely and without data corruption.

Before we move ahead, make sure you have the latest version of VS Code installed on your system with the ESP-IDF extension configured.

Mutex Introduction

What is Mutex?

Mutex is special kind of binary semaphore which serves resource management between FreeRTOS tasks. Resource management means it gives access of shared resources to two or more tasks serially. The word mutex is also known as mutual exclusion. 

Mutex FreeRTOS

Mutex means Mutual Exclusion. It makes sure that a single task has access to the resource at a particular time, thereby preventing several tasks from accessing a resource mutually. It acts as a flag whose aim is to grant access to a thread to a particular part of code at a time. The rest of the threads are locked out from acquiring that particular code/resource. This way the mutex ensures that no other threads corrupt the information and the execution for that code/resource is thread-safe. A mutex acts like a locking mechanism whereby it restricts multiple threads to simultaneously read and modify a common resource. A single thread is allowed to work on the resource and the rest wait for the key (mutex) to returned which may be used by another thread afterwards.

Why do we need to use mutex? 

There are many cases in a multitasking system where more than one task uses a single resource to complete their execution. Therefore, a use of a single shared resource between multiple tasks can introduce multiple issues such as deadlocks, data corruption, priority inversion etc.

Let’s suppose, we use two tasks in our FreeRTOS application such as Task_A and Task_B. Both tasks write string operation on a LCD. Furthermore, the priority of Task_B is higher than the Task_A. That means Task_B can preempt Task_A.  

For example, if a task being its execution by accessing a shared resource, but will not be able to complete its execution before a high priority task takes the shared resource. If the task leaves the resource in an inconsistent state, then access to the same resource by any other task or interrupt could result in data corruption, or other similar issue.

FreeRTOS where are mutex used

Let’s take an example of above diagram where Task_B and Task_A both required access to an external peripheral LCD to display a string. For example, Task_A is in running state and begin printing a string text “ESP32 FreeRTOS Mutex” on the LCD. In the middle of Task_A execution, Task_B (high priority task)  preempts Task_A and TaskA has only printed “ESP32 FreeRTOS Mu” on the LCD. But Task_B has acquired the shared resource LCD. Task_B starts to execute and displays “LAB_1” on the LCD. After TaskB completes its execution, Task_A again starts its execution from the point where it was preempted by Task_B and prints “tex” on the LCD.  

Now final output on LCD will be “ESP32 FreeRTOS MuLAB_1tex”. But this is not what we wanted to display on LCD. Instead we wants to display “FreeRTOS Mutex LAB_1”. Hence, this is an issue of data corruption. We can resolve this issue by providing serialized access to shared resource (LCD) using FreeRTOS mutex semaphore.

How to use mutex to serialize shared resources access? 

In the last example, we have seen an issue of data corruption where two tasks access the shared resource in an unexpected way. We will fix that issue with FreeRTOS mutex semaphore.

Let’s consider mutex as a token and only the process which has this token can acquire the shared resource regardless of what is priority of the task. For instance, the shared resource in this example is an LCD. Any task which wants to access LCD to write string, first it must access the token. In other words, it must become the token (mutex) holder. No other task will be able to access LCD, because it does not hold a token. Once a token holder completes its execution and finishes using the resource, it must release the token by giving it back to the resource. Only when the token has been returned can another task successfully take the token, and then safely access the same shared resource. A task is not permitted to access the shared resource unless it holds the mutex. 

FreeRTOS Mutex example

Mutex Example

The following picture depicts its use case.,Task_1 and Task_2 should take mutex token before sending something to LCD. For example, if Task_1 have this mutex token, Task_2 will not be able to preempt Task_1,  even if Task_2 pocess higher priority than Task_1. This is because priority inheritance mechanism of FreeRTOS mutex and this is what makes mutex different from binary and counting semaphores.

FreeRTOS Mutex example case

Priority inheritance means if a lower priority task holds a mutex token and a high priority task tries to preempt a lower priority task, then the priority of the lower priority task that holds a mutex will raise to that of the higher priority task. It will prevent high priority task to preempt lower priority task and also high priority task also enters the blocking state unless the lower priority task gives back the mutex. Priority inheritance is used to minimize the effect of priority inversion. 

ESP-IDF FreeRTOS Mutex Library

The ESP-IDF provides the freertos/semphr.h library to use mutex semaphore. This is a special type of binary semaphore that includes task deletion safety, ownership, recursive access and dealing with issues that arise while using mutex. One key difference between a mutex and semaphore is that in the case of a mutex, the task which has access to it must return it as well for the other tasks to use the same mutex. However, in semaphore that is not the case as any task has the ability of taking and giving the semaphore.

Although binary semaphores and mutexes are similar, but they are not the same. Where mutexes include a priority inheritance mechanism, binary semaphores do not. This makes mutexes the better option where there is an implementation of simple mutual exclusion whereas binary semaphores are better when synchronizing tasks.

ESP32 FreeRTOS Mutex with ESP-IDF

In this section, let us demonstrate the working of mutex using FreeRTOS semaphore library APIs.

Create Project

Open your VS Code and head over to View > Command Palette. Type ESP-IDF: New Project in the search bar and press enter.

Specify the project name and directory. We have named our project ‘ESP_IDF_FREERTOS_MUTEX.’ For the ESP-IDF board, we have chosen the custom board option. For ESP-IDF target, we have chosen ESP32 module. Click ‘Choose Template’ button to proceed forward.

ESP32 FreeRTOS Mutex using ESP-IDF

In the Extension, select ESP-IDF option:

ESP-IDF in VS Code New Project 2

We will click the ‘sample_project’ under the get-started tab. Now click ‘Create project using template sample_project.’

ESP-IDF in VS Code New Project 3

You will get a notification that the project has been created. To open the project in a new window, click ‘Yes.’

This opens our project that we created inside the EXPLORER tab. There are several folders inside our project folder. This is the same for every project which you will create through ESP-IDF Explorer.

ESP32 FreeRTOS Mutex Code

The main folder contains the source code meaning the main.c file will be found here. Go to main > main.c and open it. Copy the code given below in your main.c file and save it.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

xSemaphoreHandle xMutex;

//shared resourse (printf) function
void write_message_console(char *message)
{
  printf(message);
}

void temperature_task(void *params)
{
  while (true)
  {
    printf("Inside temperature_task \n");
    if (xSemaphoreTake(xMutex, portMAX_DELAY))
    {
      write_message_console("temperature is 35'c\n");
      xSemaphoreGive(xMutex);
    }
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

void humidity_task(void *params)
{
  while (true)
  {
    printf("Inside humidity_task\n");
    if (xSemaphoreTake(xMutex, portMAX_DELAY))
    {
      write_message_console("humidity is 48% \n");
      xSemaphoreGive(xMutex);
    }
    vTaskDelay(2000 / portTICK_PERIOD_MS);
  }
}

void app_main(void)
{
  xMutex = xSemaphoreCreateMutex();
  xTaskCreate(&temperature_task, "temperature_task", 2048, NULL, 2, NULL);
  xTaskCreate(&humidity_task, "humidity_task", 2048, NULL, 2, NULL);
}

How the Code Works?

To use the functionalities of the Semaphore library provided by FreeRTOS to create a mutex, let us first include its header file along with other FreeRTOS libraries to generate delays and create tasks.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

Next create a Semaphore Handle called ‘xMutex’ as shown below.

xSemaphoreHandle xMutex;

The write_message_console() function is responsible for printing the message on the terminal by calling the printf() fucntion.

void write_message_console(char *message)
{
  printf(message);
}

Inside the temperature_task function, we call xSemaphoreTake() if the mutex was obtained. This function is used to take a semaphore and takes in two arguments. The first argument is the Semaphore handle which is ‘xMutex’ in our case. The second argument is the block time. This is the maximum amount of time that the task will wait in blocked state for the semaphore to become available. We have specified the block time as ‘portMAX_DELAY.’ This is the maximum value that can be defined as the ticktype. Therefore, it will wait until it gets the flag from Semaphore. The write_message_console() function will be used to display that the temperature reading is 35’c and the xSemaphoreGive() function will be called to release the mutex. It takes in a single argument that is the semaphore handle that we are using which is ‘xMutex.’ This function is called from any task that has to be synchronized. After that it adds a delay of one second before it continues again.

void temperature_task(void *params)
{
  while (true)
  {
    printf("Inside temperature_task \n");
    if (xSemaphoreTake(xMutex, portMAX_DELAY))
    {
      write_message_console("temperature is 35'c\n");
      xSemaphoreGive(xMutex);
    }
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

Similarly, inside the humidity_task(), we call xSemaphoreTake() if the mutex was obtained. The write_message_console() function will be used to display the humidity reading and the xSemaphoreGive() function will be called to release the mutex. After that it adds a delay of two seconds before it continues again.

void humidity_task(void *params)
{
  while (true)
  {
    printf("Inside humidity_task\n");
    if (xSemaphoreTake(xMutex, portMAX_DELAY))
    {
      write_message_console("humidity is 48% \n");
      xSemaphoreGive(xMutex);
    }
    vTaskDelay(2000 / portTICK_PERIOD_MS);
  }
}

app_main()

Inside the app_main() function, firstly a mutex type semaphore is created using xSemaphoreCreateMutex(). This function returns a handle through which the mutex is referenced. It is ‘xMutex’ which the Semaphore handle that we previously defined.

  xMutex = xSemaphoreCreateMutex();

Then we will create the tasks for temperature_task and humidity_task. To create a task we use the function xTaskCreate(). This function takes in several arguments.

  • The first argument is the name of the function.
  • The second argument is the name of the task for descriptive purposes.
  • The third argument specifies the stack size of the task. This indicates the amount of memory we want to reserve for the particular task.
  • The fourth argument is the parameter. It is a value that is passed as the parameter to the created task. If this property is not being used, then it is set as NULL.
  • The fifth argument is the priority of the task.
  • The last argument is the handle which is used to change the function of the task eg. suspend, delete, resume, get or set a config of task. It is optional so it can be set as NULL.
xTaskCreate(&temperature_task, "temperature_task", 2048, NULL, 2, NULL);
xTaskCreate(&humidity_task, "humidity_task", 2048, NULL, 2, NULL);

Demonstration

To flash your chip, type the following command in the serial terminal. Remember to replace the COM port with the one through which your board is connected.

idf.py -p COMX flash monitor

After the code flashes successfully, this is the output that gets printed in the serial terminal. Continuously after every second, the mutex is taken and released in the temperature_task and the temperature reading gets printed in the ESP-IDF terminal. Likewise, after every 2 seconds, the mutex is taken and released in the humidity_task and the humidity reading gets printed in the ESP-IDF terminal.

ESP32 FreeRTOS Mutex using ESP-IDF

You may also like to read:

Leave a Comment