Site icon techtutorialsx

ESP32: FreeRTOS counting semaphores

The objective of this post is to provide an introduction to FreeRTOS  counting semaphores, using the ESP32 and the Arduino support.


Introduction

The objective of this post is to provide an introduction to FreeRTOS counting semaphores, using the ESP32 and the Arduino support. We assume a previous installation of the ESP32 support for the Arduino environment.

We will develop a simple application where we will use a counting semaphore as an execution barrier. Note that this is just a simple getting started example and we can implement this kind of barriers more efficiently using other mechanisms, such as task notifications [1].

Semaphores are typically used for both synchronization and mutual exclusion [2] in access to resources. I will leave some more content on semaphores on the related content section. In our example program we are going to use FreeRTOS counting semaphores.

So, in our program, we will have the setup function launching a configurable amount of tasks, and then it will wait on a semaphore for all the tasks to finish. This coordination will be achieved using the previously mentioned counting semaphores.

So, after launching the tasks, the setup function will try to hold a semaphore as many times as tasks launched, and only continue execution after that point. Note that, when a task tries to take a semaphore without units (semaphore counter equal to 0), it will block until the semaphore count is incremented by other task or a defined timeout occurs.


The code

We will start our code by declaring a global variable to hold the number of tasks to launch. This will give some more flexibility to our use case. We will perform our example with 4 tasks, but you can change it to another number.

Then, we will declare a counting semaphore as a global variable, so it can be accessed by both the setup function and the tasks. We create the semaphore with the xSemaphoreCreateCounting function.

This function receives as input arguments the maximum count the semaphore can achieve and the initial value  [3]. Since we are using this for synchronization purposes, we will specify a maximum count equal to the number of tasks to launch, and initialize it at 0.

int nTasks=4;
SemaphoreHandle_t barrierSemaphore = xSemaphoreCreateCounting( nTasks, 0 );

Next, on the setup function, we will start by opening the serial connection and printing a simple debug message, so we now the point of execution of the code.

Next, we will do a loop to launch our tasks. Note that we can use the same task function to launch multiple tasks, so we don’t need to implement code for every new task. We will check the code for the functions latter.

Note that we will create the tasks pinned to core 1 of the ESP32. This is because, as seen in this previous post, the setup function is pinned to core 1. It will be easier for us to understand the functioning of the semaphores without introducing the complexity of multi core execution. If you need more information about launching tasks pinned to a specific ESP32 core, check this previous tutorial. If you need help on the basics and arguments needed for launching a FreeRTOS task, check this tutorial.

Also, we will create the tasks with priority 0. We will do this because, as seen in this tutorial, the setup function runs with a priority of 1 (remember, higher numbers mean higher priorities). So, since the tasks will have lower priority, they will only run if the setup function blocks somewhere, and we expect it to block on our semaphore.

Just for the sake of differentiating the tasks, we will pass the number of the iteration of the loop as an input parameter, so they can print it in their code. Check this previous tutorial if you need more information about passing parameters to FreeRTOS tasks.

Update: In the original code, we were passing the variable by reference, which was causing concurrency problems (thanks to gecame for the warning). We will be passing the task argument by value.

Serial.begin(112500);
delay(1000);

Serial.println("Starting to launch tasks..");

int i;

for(i= 0; i< nTasks; i++){
    xTaskCreatePinnedToCore(
                    genericTask,    /* Function to implement the task */
                    "genericTask",  /* Name of the task */
                    10000,          /* Stack size in words */
                    (void *)i,      /* Task input parameter */
                    0,              /* Priority of the task */
                    NULL,           /* Task handle. */
                    1);             /* Core where the task should run */
}

After launching the tasks, we will do a for loop trying to get the semaphore as many times as the tasks launched. So, the setup function should only pass from that execution point when there are as many units in the semaphore as there are tasks. Since there will be the tasks incrementing  the semaphore, we should guarantee that the Arduino setup function will only finish after all the tasks finish.

To take a unit from a semaphore, we will call the xSemaphoreTake function. This function receives as first argument the handle of the semaphore (the global variable we declared and initialized in the beginning) and a maximum number of FreeRTOS ticks to wait [4]. Since we want it to block indefinitely until it can get the semaphore, we pass to this last argument the value portMAX_DELAY [4].

After that, we will execute a final message, indicating the setup function has passed that execution point.

for(i= 0; i< nTasks; i++){
    xSemaphoreTake(barrierSemaphore, portMAX_DELAY);
}

Serial.println("Tasks launched and semaphore passed...");

This finishes the setup function. Now we will implement the main loop where, just as an example, we will use a function called vTaskSuspend to suspend the main loop task. This will block its execution no matter what is its priority [5]. This function receives as input the handle of the task to be suspended [5]. In this case, since we want to suspend the calling task, we pass NULL [5].

void loop() {
  vTaskSuspend(NULL);
}

Finally, we will implement the code for our tasks. They will simply start, print their number (from the argument passed when they were launched), increment the semaphore and then finish.

So, we start by building and printing the string indicating the number of the task. Then, we will increment the semaphore using the xSemaphoreGive function. This function receives as input the handle for the semaphore [6]. Note that there is also a xSemaphoreGiveFromISR to be used in the context of an Interrupt Service Routine.

Check the code for the function bellow, which already includes the call to delete the task after the execution.

void genericTask( void * parameter ){

    String taskMessage = "Task number:";
    taskMessage = taskMessage + ((int)parameter);

    Serial.println(taskMessage);

    xSemaphoreGive(barrierSemaphore); 

    vTaskDelete(NULL);
}

Check the full source code for this tutorial bellow.

int nTasks=4;
SemaphoreHandle_t barrierSemaphore = xSemaphoreCreateCounting( nTasks, 0 );

void genericTask( void * parameter ){

    String taskMessage = "Task number:";
    taskMessage = taskMessage + ((int)parameter);

    Serial.println(taskMessage);

    xSemaphoreGive(barrierSemaphore); 

    vTaskDelete(NULL);
}

void setup() {

  Serial.begin(112500);
  delay(1000);

  Serial.println("Starting to launch tasks..");

  int i;

  for(i= 0; i< nTasks; i++){
    xTaskCreatePinnedToCore(
                    genericTask,    /* Function to implement the task */
                    "genericTask",  /* Name of the task */
                    10000,          /* Stack size in words */
                    (void *)i,      /* Task input parameter */
                    0,              /* Priority of the task */
                    NULL,           /* Task handle. */
                    1);             /* Core where the task should run */
  }    

  for(i= 0; i< nTasks; i++){
    xSemaphoreTake(barrierSemaphore, portMAX_DELAY);
  }

  Serial.println("Tasks launched and semaphore passed...");
}

void loop() {
  vTaskSuspend(NULL);
}


Testing the code

To test the code, just upload it to your ESP32 board using the Arduino IDE. Then open the serial monitor to check the results. You should get an output similar to figure 1.

Figure 1 – Output of the semaphore synchronization program.

Note that although the setup function as a higher priority than any of the launched tasks, it only finished the execution (printing the “Tasks launched and semaphore passed…” message) after all the tasks executed their code (printing their number).

Just as an example, figure 2 shows the output if we comment the semaphore obtaining code in the Arduino setup function.

Figure 2 – Execution of the code without semaphore synchronization.

Note that now the setup function executes all of its code first, since it has higher priority and never blocks. The tasks will only be able to run after the setup finishes (remember that we suspended the execution of the main loop so, although it has higher priority, it gives away the CPU).

Besides no longer having the desired synchronization, we can see that the tasks are now outputting the wrong task number. This is because they are accessing the parameter after the setup function end. So, they are accessing a memory position no longer valid since the iteration variable was a local variable to the setup function.


Related content


Related posts

 

References

[1] http://www.freertos.org/a00124.html

[2] http://www.freertos.org/Embedded-RTOS-Binary-Semaphores.html

[3] http://www.freertos.org/CreateCounting.html

[4] http://www.freertos.org/a00122.html

[5] http://www.freertos.org/a00130.html

[6] http://www.freertos.org/a00123.html

Exit mobile version