ESP32 ESP-IDF FreeRTOS Semaphore – Tasks Synchronization Examples

In this ESP32 ESP-IDF FreeRTOS Semaphore tutorial, we will learn how to use FreeRTOS Semaphore with ESP32 ESP-IDF. Semaphores are used to send a notification to other tasks or to synchronize different tasks whereas queues are used for intertask communication. Firstly, we will look at a sample code to learn how to use binary semaphores to perform task synchronization. Secondly, we will see how to release a semaphore from an interrupt service routine for task synchronization.

Semaphore Introduction

A semaphore is a kernel object that a task can acquire for the purpose of task synchronization. It is a signaling process whereby a waiting task is signaled by another task to continue execution. For example, we have two tasks: Demo_Task and Demo_Task2. When Demo_Task finishes a particular job on a data, it gives out a flag (Semaphore) which is an integer and increases by 1. This flag is received by Demo_Task2 which was waiting for it. Now it starts its execution. After Demo_Task2 finishes its execution, the flag is decreased back to 1.

One task provides a semaphore and other tasks acquire it to move from a blocking state to a running state. If two or more tasks are waiting on the semaphore, the highest priority task acquires it first in case of binary semaphore.

There are two types of Semaphores: Binary Semaphore and Counting semaphores.

binary semaphore

Binary Semaphore

Binary semaphores are mostly used for task synchronization such as synchronizing an interrupt service routine in a task. Its value can be either one indicating that it is available or zero indicating that it is empty (already acquired by another task).

Within a single application, a binary semaphore can be used by several tasks. However, only one task may acquire it at a time. Any task, including one that did not initially acquire the semaphore, is able to return it by making the semaphore a global resource. Suppose task 2 seeks to access a binary semaphore that task 1 has previously obtained, but it is already in use. Therefore, until task 1 releases the binary semaphore, task 2 will enter the blocking state and stay in the blocking state.

Counting Semaphore

Counting semaphores are used for counting events. Think of counting semaphores as a queue with a length greater than one. The number of items that are currently in the queue will be tracked by tasks that want to employ counting semaphores. The number of items in the queue become less each time a process uses a counting semaphore. In other words, the semaphore count is determined by the quantity of items in the queue.

counting semaphores types

Types

It has two types. It is typically used for two things such as counting events and for resource management. 

Counting Events 

In case of events counting, we can consider it as a counter which defines the maximum value and also keeps track of the number of times the event has occurred. Usually, an event handler is used to give semaphore access and keep track of count value. Whenever a task takes a semaphore value, the count value is decremented by one. 

For example, we created a counting semaphore of maximum value of 4 and initial value set to zero. In this case, an event handler will ‘give’ a semaphore each time an event occurs but the maximum events it can handle are upto four. 

Resource Management 

In case of resource management, counting semaphore defines the count of the number of shared resources available and that can be used by tasks at the same time.

For example, there are two FreeRTOS tasks that want to write data to a serial monitor of Arduino through the UART communication module of Arduino. But as you know that Arduino IDE has only one serial monitor. Also, Arduino uno has only one on-board UART module. Therefore, we must manage UART and Arduino IDE serial  monitor resources between two tasks by using counting semaphore for resource management. We must first initialize the counting semaphore equal to the number of resources. After that, a task which wants to use a resource must take a semaphore by decrementing the semaphore’s count value. 

Sample Code Demonstrating FreeRTOS Semaphore (Send Notify from one task to another)

Let us look at an example in order to learn about binary Semaphore for normal tasks. We will work on the code that we used previously when creating two tasks, and incorporate Semaphore in it. Refer to the article FreeRTOS Tasks with ESP-IDF before moving ahead. Creation of tasks is already discussed in that article. Here, we will show you how to use Semaphore.

This is the complete code that we are using.

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

SemaphoreHandle_t xSemaphore = NULL;

TaskHandle_t myTaskHandle = NULL;
TaskHandle_t myTaskHandle2 = NULL;

void Demo_Task(void *arg)
{
    while(1){
        printf("Message Sent! [%d] \n", xTaskGetTickCount());
        xSemaphoreGive(xSemaphore);
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

void Demo_Task2(void *arg)
{
    while(1){
     if(xSemaphoreTake(xSemaphore, portMAX_DELAY))
     {
      printf("Received Message [%d] \n", xTaskGetTickCount());
     }
    }
}

void app_main(void)
{
   xSemaphore = xSemaphoreCreateBinary();
   xTaskCreate(Demo_Task, "Demo_Task", 4096, NULL, 10, &myTaskHandle);
   xTaskCreatePinnedToCore(Demo_Task2, "Demo_Task2", 4096, NULL,10, &myTaskHandle2, 1);
 }

How the Code Works?

To use the functionalities of the Semaphore library provided by FreeRTOS, let us first includes its header file:

#include "freertos/semphr.h"

Next create a Semaphore Handle as shown below:

SemaphoreHandle_t xSemaphore = NULL;

In this code, we are sending semaphore from Demo_Task to Demo_Task2. Therefore in the Demo_Task() function, we use the xSemaphoreGive() to give a semaphore. It takes in a single argument that is the semaphore handle that we are using. In our case, it is ‘xSemaphore.’ This function is called from any task that has to be synchronized. The printf() function will be used to display that the Semaphore is successfully sent. Moreover, the xTaskGetTickCount() will display the time that has passed since the application started. We are adding a delay of 1 second after the while loop continues again.

void Demo_Task(void *arg)
{
    while(1){
        printf("Message Sent! [%d] \n", xTaskGetTickCount());
        xSemaphoreGive(xSemaphore);
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

In Demo_Task2, we are using the function, xSemaphoreTake(), which is used to take a semaphore. It takes in two arguments. The first argument is the Semaphore handle and 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 print() function will be used to display that the Semaphore is successfully received. Moreover, the xTaskGetTickCount() will display the time that has passed since the application started.

void Demo_Task2(void *arg)
{
    while(1){
     if(xSemaphoreTake(xSemaphore, portMAX_DELAY))
     {
      printf("Received Message [%d] \n", xTaskGetTickCount());
     }
    }
}

Inside the main function we will create the binary semaphore using xSemaphoreCreateBinary(). It does not take any argument inside it. It returns a variable of type SemaphoreHandle_t. xSemaphore is a global variable that we are using to store the semaphore. We had initially set its value to NULL.

   xSemaphore = xSemaphoreCreateBinary();

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. Demo_Task informs Demo_Task2 after every second. When the printf and semaphore give functions run, semaphore take function is triggered immediately as the execution times are same.

ESP32 FreeRTOS Semaphore Demo1

FreeRTOS Semaphore as Interrupt Example

In this example, we will use Semaphore to create an interrupt. For demonstration, we will incorporate this in the ESP32 ESP-IDF FreeRTOSInterrupt example we saw in a previous article where an interrupt was used to toggle a LED connected with the ESP32 board with a pushbutton.

We will use Semaphore to create a software interrupt whereby a function is run whenever another function runs. First add all the required functions and libraries to create the interrupt just as we did in the previous article. Then include Semaphore in it.

This is the complete that we will be using.

#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "driver/gpio.h"
#include "freertos/semphr.h"

SemaphoreHandle_t xSemaphore = NULL;

#define ESP_INR_FLAG_DEFAULT 0
#define LED_PIN  27
#define PUSH_BUTTON_PIN  33

TaskHandle_t ISR = NULL;

TaskHandle_t myTaskHandle = NULL;
TaskHandle_t myTaskHandle2 = NULL;


void IRAM_ATTR button_isr_handler(void *arg){
  xSemaphoreGiveFromISR(xSemaphore, NULL);
  xTaskResumeFromISR(ISR);
}

void interrupt_task(void *arg){
  bool led_status = false;
  while(1){
    vTaskSuspend(NULL);
    led_status = !led_status;
    gpio_set_level(LED_PIN, led_status);

  }
    }

void Demo_Task(void *arg)
{
    while(1){
        printf("Message Sent! [%d] \n", xTaskGetTickCount());
        xSemaphoreGive(xSemaphore);
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

void Demo_Task2(void *arg)
{
    while(1){
     if(xSemaphoreTake(xSemaphore, portMAX_DELAY))
     {
      printf("Received Message [%d] \n", xTaskGetTickCount());
     }
    }
}

void app_main(void)
{
  gpio_pad_select_gpio(PUSH_BUTTON_PIN);
  gpio_pad_select_gpio(LED_PIN);

  gpio_set_direction(PUSH_BUTTON_PIN, GPIO_MODE_INPUT);
  gpio_set_direction(LED_PIN ,GPIO_MODE_OUTPUT);

  gpio_set_intr_type(PUSH_BUTTON_PIN, GPIO_INTR_POSEDGE);

  gpio_install_isr_service(ESP_INR_FLAG_DEFAULT);

  gpio_isr_handler_add(PUSH_BUTTON_PIN, button_isr_handler, NULL);

   xSemaphore = xSemaphoreCreateBinary();
   xTaskCreate(Demo_Task, "Demo_Task", 4096, NULL, 10, &myTaskHandle);
   xTaskCreatePinnedToCore(Demo_Task2, "Demo_Task2", 4096, NULL,10, &myTaskHandle2, 1);

   xTaskCreate(interrupt_task, "interrupt_task", 4096, NULL, 10, &ISR);
}

How the Code Works?

We have already discussed in the FreeRTOS Interrupts article on how to generate the an external interrupt to toggle the LED. Let us therefore focus on the parts where we are using Semaphore.

Firstly, we will start by including the necessary libraries for this project. This includes the driver/gpio.h library as we have to work with GPIO pins and FreeRTOS libraries as we want to add delays, create tasks and use Semaphore.

#include <stdio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "driver/gpio.h"
#include "freertos/semphr.h"

Define the Semaphore Handle and set it as NULL.

SemaphoreHandle_t xSemaphore = NULL;

Inside the button_isr_handler() we use the function xSemaphoreGiveFromISR() for giving a semaphore for interrupt. It takes in two arguments. The first argument is the Semaphore handle and the second argument is the priority. This function will be used to synchronize the ISR and the task.

void IRAM_ATTR button_isr_handler(void *arg){
  xSemaphoreGiveFromISR(xSemaphore, NULL);
  xTaskResumeFromISR(ISR);
}

Compiling the Sketch

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, you can view the output on the serial terminal. Both the tasks are notifying after every 5 seconds. When the push button is pressed, the interrupt is triggered and the LED connected with GPIO27 turns ON. Pressing the pushbutton again, turns it OFF. Moreover the function xSemaphoreGiveFromISR() will be called from the ISR function whereas xSemaphoreTake() will be called from Demo_Task2(). Therefore, whenever the pushbutton is pressed, Demo_Task() prints its message on the serial terminal as well.

ESP32 FreeRTOS Semaphore Demo2

You may also like to read:

1 thought on “ESP32 ESP-IDF FreeRTOS Semaphore – Tasks Synchronization Examples”

  1. In the code below we need to replace %d with %ld, because it’s a Long Int printing.

    Replace:
    printf(“Received Message [%d] \n”, xTaskGetTickCount());
    printf(“Message Sent! [%d] \n”, xTaskGetTickCount());

    With:
    printf(“Received Message [%ld] \n”, xTaskGetTickCount());
    printf(“Message Sent! [%ld] \n”, xTaskGetTickCount());

    Reply

Leave a Comment