Using FreeRTOS with ESP32 and Arduino

About this post

With this article, I try to offer users with less experience a reasonably understandable introduction to FreeRTOS. I know from my own experience that the topic takes some getting used to, but it is just as exciting.

What is FreeRTOS?

FreeRTOS is an open-source real-time operating system (RTOS) that has been specially developed for embedded systems. It offers a preemptive multitasking environment to fulfill real-time requirements in various applications. What? OK, this explanation probably needs a few explanations itself:

  • RTOS stands for Real-Time Operating System. In a real-time system, the focus is on the time frame for the execution of tasks.
  • Embedded systems are – among other things – microprocessors that are installed in certain devices or just microcontrollers. In contrast to conventional computers, which are suitable for a wide range of applications, embedded systems are designed for specific tasks or functions.
  • Preemptive means that a task can be interrupted to allow another task to be carried out.

FreeRTOS was developed by Richard Barry in the early 2000s. It may be freely used, modified and redistributed. FreeRTOS supports various architectures and processors, including AVR, ARM, ESP, x86 and more. So there is not just one FreeRTOS, but various customizations.

FreeRTOS Documentation

For more in-depth information, I recommend that you take a look at the FreeRTOS documentation in parallel to this article, in particular the API reference(Application Programming Interface), which you can find here. It is clearly organized and should be easy to understand in conjunction with this article.

Is Real Multitasking Possible with FreeRTOS?

Yes and no. Most microcontrollers only have one core. In these cases, so-called time slicing is used, which means that a certain processor time is allocated to each task. After the allocated time has elapsed, the task is interrupted, and it is the next task’s turn. These changes happen in quick succession, giving the impression of multitasking. One exception is the ESP32 because it has two cores. Tasks can actually be processed in parallel here.

Which FreeRTOS implementations do we look at in this article?

In this article, I will look at the ESP32 and the AVR-based Arduino boards (e.g. the UNO R3, the classic Nano or the Pro Mini). The ESP32 already uses FreeRTOS in the Arduino environment. It is integrated via the esp32 board package, so you don’t have to worry about the inclusion of FreeRTOS libraries. For the AVR Arduinos, there is the Arduino_FreeRTOS_Library, which you can find and install under the name “FreeRTOS” in the Arduino library manager.

Since there are a few minor differences in the use of FreeRTOS on the ESP32 and the AVR-based Arduinos, I have written and tested all sketches in two versions.

There are many other FreeRTOS libraries, for example for SAMD21, SAMD51 and STM32 boards. I couldn’t try them all out, and certainly couldn’t cover them in detail here. But once you have familiarized yourself with FreeRTOS, further variants should not cause you any major problems.

Creating and Using Tasks with FreeRTOS

We start quite simply with a blink sketch. Three LEDs should each flash at different frequencies. Without FreeRTOS you could achieve this with a delay() construction, but that would be the worst option. A solution à la if((millis() - lastToggle) > blinkPeriod){...} would be even better, as it would not be blocking. However, this can also cause problems if other processes delay the millis() query. Alternatively, timer interrupts would be an option – provided there are enough timers available.

With FreeRTOS we simply avoid these problems by giving each LED its own task. In principle, there are three blink sketches running in parallel.

First, let us take a look at the code:

#define LED1 25
#define LED2 26
#define LED3 17

void setup() {
    pinMode(LED3, OUTPUT);
  
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        2048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *parameter) {
    pinMode(LED1, OUTPUT);
    while(1){
        digitalWrite(LED1, HIGH);
        delay(500); // Delay for Tasks 
        digitalWrite(LED1, LOW);
        delay(500);
    }
}

void blink2(void *parameter) {
    pinMode(LED2, OUTPUT);
    while(1) {
        digitalWrite(LED2, HIGH);
        delay(333);
        digitalWrite(LED2, LOW);
        delay(333);
    }
}

void loop(){
    digitalWrite(LED3, HIGH);
    delay(1111);
    digitalWrite(LED3, LOW);
    delay(1111);
}
#include<Arduino_FreeRTOS.h>
#define LED1 7
#define LED2 8
#define LED3 9

void setup() {
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,         // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );

    xTaskCreate(
        blink3,     // Function name of the task
        "Blink 3",  // Name of the task (e.g. for debugging)
        128,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        digitalWrite(LED1, HIGH);
        delay(500); 
        digitalWrite(LED1, LOW);
        delay(500);
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        digitalWrite(LED2, HIGH);
        delay(333);
        digitalWrite(LED2, LOW);
        delay(333);
    }
}

void blink3(void *pvParameters){
    pinMode(LED3, OUTPUT);
    while(1){
        digitalWrite(LED3, HIGH);
        delay(1111); 
        digitalWrite(LED3, LOW);
        delay(1111);
    }
}

void loop(){}

 

Explanation of the code

Creating the Task

If you want to execute an operation in a separate task, you must first create the task with xTaskCreate(). However, what you are creating is just a kind of empty shell that you will only fill with life in a second step.  Here is the general form of xTaskCreate():

BaseType_t xTaskCreate(    TaskFunction_t pvTaskCode,
                            const char * const pcName,
                            configSTACK_DEPTH_TYPE usStackDepth,
                            void *pvParameters,
                            UBaseType_t uxPriority,
                            TaskHandle_t *pxCreatedTask
                          );

xTaskCreate Parameters

The six parameters to be passed are:

  • pvTaskCode: This is the name of the function that contains the code to be executed in the task. The procedure is similar to assigning an interrupt service routine in attachInterrupt().
  • pcName: This parameter allows you to give the task an easily understandable name, for example, “My favorite task”. pcName is passed as a pointer.
  • usStackdepth: The creation of your tasks also includes the definition of its size in the stack, which is a part of the SRAM. I have written an article about SRAM, stack and heap here. We will come back to how you determine the size of the task.
  • pvParameters: You can pass parameters to the task as pointer. The variable type “void” may be unusual for many people. It makes the function very flexible. In this example, we pass nothing, i.e. NULL (a pointer to nowhere).
  • uxPriority: You can use this parameter to set the priority of the task.
  • pxCreatedTask: A task handle is an optional variable that you can use, for example, to query the properties of the task. You could say it is an identifier for the task. 

The naming of the prefixes of the variables and functions results from the variable type or the variable type of the return value. A “c” stands for “char”, an “s” for “short”, “v” for “void”, a “u” for “unsigned” and “x” for all non-standard types. If a variable is a pointer, a “p” is placed in front of it. You can find out more about the naming conventions here.

The xTaskCreate() function returns pdPASS when the task has been successfully created. You could therefore check this with if(xTaskCreate(......)==pdPASS)){....}. If the task could not be created, the function returns errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY.

Filling the Task with Content – the Task Function

To fill the previously created task with life, use the task function that you have defined in xTaskCreate() (pvTaskCode). It is necessary to put void *pvParameters again in the receiving function, even if you pass NULL. For example: void blink1(void *pvParameters){....}.

The following applies to the task function:

  • It has no return value.
  • Like a normal sketch, the task function consists of a kind of setup, i.e. code that is only executed once and a continuous loop that you realize with while(1){....} or for(;;){....}.
  • A task can delete itself with vTaskDelete( NULL );. You can delete the task “from outside” using the task handle, i.e. vTaskDelete(pxCreatedTask).

The behavior of delay() is interesting. Within the task, delay() is blocking as usual. When switching tasks, however, the delay() is interrupted.

Using loop()

As you can see, I have placed the code for LED3 in the ESP32 sketch in loop(). It’s not the best style, but I wanted to show that it works. If you do the same with an AVR-based Arduino, the program will hang. This is why the code for LED3 is outsourced to a third task.

Ticks and vTaskDelay()

The tasks must share the processor core(s). To organize this, FreeRTOS divides the time into ticks. The task can use the processor undisturbed for the duration of one tick. After the tick has expired, the system checks whether another task is due.

A tick is preset to 1 millisecond on the ESP32, which you check using Serial.println(portTICK_PERIOD_MS). In other words, the tick frequency is 1 kHz. You can query the tick frequency with Serial.println(configTICK_RATE_HZ). The FreeRTOS version for the AVR-based Arduinos specifies a default tick frequency of 62 Hz, i.e. a tick has a length of approx. 16 milliseconds.

If you search the net for FreeRTOS code examples, you will often come across the function vTaskDelay(). In principle, it does the same as delay(), with the difference that you pass vTaskDelay() ticks instead of milliseconds. This makes no difference with the ESP32, but it does with the AVR Arduinos. To achieve a waiting time of 500 milliseconds, you would have to write: vTaskDelay(500/portTICK_PERIOD_MS).

I stick to the good old delay() in all examples.

Determining the Memory Space Requirements of a FreeRTOS Task

Calculating the memory space required by the task is not trivial. Instead, we first reserve sufficient memory space and then use the function uxTaskGetStackHighWaterMark() to query the remaining free memory in the reserved area. To tell the function uxTaskGetStackHighWaterMark() which task we want to determine the free memory from, we pass it the task handle. You first create the handle with TaskHandle_t name and assign it to the task in xTaskCreate().

I have reduced the following sketch to two blink tasks. To make it more obvious that uxTaskGetStackHighWaterMark() actually displays the free memory, I have varied the reserved memory for the tasks.

If we were to implement the memory requirement query in the task to be examined, this would falsify the result. The memory query therefore has its own task.

#define LED1 25
#define LED2 26

TaskHandle_t taskBlink1Handle; // create task handle
TaskHandle_t taskBlink2Handle;

void setup() {
    Serial.begin(115200);
    pinMode(LED1, OUTPUT);
    pinMode(LED2, OUTPUT);
  
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        &taskBlink1Handle   // Assign task handle
      );

    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        1048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        &taskBlink2Handle   // Assign task handle
    );

    xTaskCreate(
        printWatermark,     // Function name of the task
        "Print Watermark",  // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    while(1){
        digitalWrite(LED1, HIGH);
        delay(500); 
        digitalWrite(LED1, LOW);
        delay(500);
    }
}

void blink2(void *pvParameters){
    while(1){
        digitalWrite(LED2, HIGH);
        delay(333);
        digitalWrite(LED2, LOW);
        delay(333);
    }
}

void printWatermark(void *pvParameters){
    while(1){
        delay(2000);
        Serial.print("TASK: ");
        Serial.print(pcTaskGetName(taskBlink1Handle)); // Get task name with handler
        Serial.print(", High Watermark: ");
        Serial.print(uxTaskGetStackHighWaterMark(taskBlink1Handle));
        Serial.println();
        Serial.print("TASK: ");
        Serial.print(pcTaskGetName(taskBlink2Handle)); // Get task name with handler
        Serial.print(", High Watermark: ");
        Serial.print(uxTaskGetStackHighWaterMark(taskBlink2Handle));
        Serial.println();
    }
}

void loop(){}
#include<Arduino_FreeRTOS.h>
#define LED1 7
#define LED2 8

TaskHandle_t taskBlink1Handle; // create task handle
TaskHandle_t taskBlink2Handle;

void setup() {
    Serial.begin(9600);
    pinMode(LED1, OUTPUT);
    pinMode(LED2, OUTPUT);
  
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,         // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        &taskBlink1Handle   // Assign task handle
      );

    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        98,         // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        &taskBlink2Handle   // Assign task handle
    );

    xTaskCreate(
        printWatermark,     // Function name of the task
        "Print Watermark",  // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    while(1){
        digitalWrite(LED1, HIGH);
        delay(500); 
        digitalWrite(LED1, LOW);
        delay(500);
    }
}

void blink2(void *pvParameters){
    while(1){
        digitalWrite(LED2, HIGH);
        delay(333);
        digitalWrite(LED2, LOW);
        delay(333);
    }
}

void printWatermark(void *pvParameters){
    while(1){
        delay(2000);
        Serial.print("TASK: ");
        Serial.print(pcTaskGetName(taskBlink1Handle)); // Get task name with handler
        Serial.print(", High Watermark: ");
        Serial.print(uxTaskGetStackHighWaterMark(taskBlink1Handle));
        Serial.println();
        Serial.print("TASK: ");
        Serial.print(pcTaskGetName(taskBlink2Handle)); // Get task name with handler
        Serial.print(", High Watermark: ");
        Serial.print(uxTaskGetStackHighWaterMark(taskBlink2Handle));
        Serial.println();
    }
}

void loop(){}

 

And here is the output:

Output of the FreeRTOS high watermark sketches, left: ESP32, right: AVR Arduino (Nano)

We can deduce from this:

  • The tasks require the following memory space on the ESP32: 1048 – 500 or 2048 – 1500 = 548 byte.
  • On the AVR Arduino it is: 128 – 78 or 98 – 48 = 50 byte.

The AVR Arduino therefore uses the memory more sparingly, which it has to, as its SRAM is only a fraction of the ESP32 SRAM. 

If you apply the exact values just determined, the sketch will probably crash. Reserve a few more bytes for the tasks. 

What you can also learn from the example sketch is how to get the name of the task using pcTaskGetName().

Suspending and Resuming tasks

Tasks can be suspended easily with vTaskSuspend(taskHandle); and resumed with vTaskResume(taskHandle);. In the following sketch, we use one task to make an LED flash and another task to stop it flashing regularly.

#define LED1 25
TaskHandle_t blink1Handle;

void setup() {
    xTaskCreate(
        blink1,       // Function name of the task
        "Blink 1",    // Name of the task (e.g. for debugging)
        2048,         // Stack size (bytes)
        NULL,         // Parameter to pass
        1,            // Task priority
        &blink1Handle // Task handle
    );
    xTaskCreate(
        suspend_resume,       // Function name of the task
        "Suspend Resume",    // Name of the task (e.g. for debugging)
        2048,         // Stack size (bytes)
        NULL,         // Parameter to pass
        1,            // Task priority
        NULL // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        digitalWrite(LED1, HIGH);
        delay(100); 
        digitalWrite(LED1, LOW);
        delay(100);
    }
}

void suspend_resume(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        delay(1999);
        vTaskSuspend(blink1Handle);
        delay(999);
        vTaskResume(blink1Handle);
    }
}

void loop(){}
#include<Arduino_FreeRTOS.h>
#define LED1 7
TaskHandle_t blink1Handle;

void setup() {
    xTaskCreate(
        blink1,       // Function name of the task
        "Blink 1",    // Name of the task (e.g. for debugging)
        128,          // Stack size (bytes)
        NULL,         // Parameter to pass
        1,            // Task priority
        &blink1Handle // Task handle
    );

    xTaskCreate(
        suspend_resume,    // Function name of the task
        "Suspend Resume",  // Name of the task (e.g. for debugging)
        128,               // Stack size (bytes)
        NULL,              // Parameter to pass
        1,                 // Task priority
        NULL      // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        digitalWrite(LED1, HIGH);
        delay(100); 
        digitalWrite(LED1, LOW);
        delay(100);
    }
}

void suspend_resume(void *pvParameters){
    while(1){
        delay(1900);
        vTaskSuspend(blink1Handle);
        delay(900);
        vTaskResume(blink1Handle);
    }
}

void loop(){}

 

Determining the Executing Core

Two cores are available on the ESP32, 0 and 1. We can use the function xTaskCreatePinnedToCore() to determine on which of the cores the task is to be executed. The function is passed the same parameters as xCreateTask(), except that the core is added at the end. Here is a simple example.

void setup() {
    Serial.begin(115200);
    xTaskCreatePinnedToCore(
        print1,      // Function name of the task
        "Print 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL,        // Task handle
        0            // run on Core 0
    );

    xTaskCreatePinnedToCore(
        print2,     // Function name of the task
        "Print 2",  // Name of the task (e.g. for debugging)
        2048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL,       // Task handle
        1           // run on Core 1
    );
}

void print1(void *pvParameters){
    while(1){
        Serial.print("Task is running on core ");
        Serial.println(xPortGetCoreID());
        delay(2500);
    }
}

void print2(void *pvParameters){
    while(1){
        Serial.print("Task is running on core ");
        Serial.println(xPortGetCoreID());
        delay(1500);
    }
}

void loop(){}

 

As you can see, you can use xPortGetCoreID() to check on which core the task is being executed.

Which Core Should I Use?

By default, your Arduino code is executed on core 1. If you use Wi-Fi or BLE, a lot of work is required in the background, which is carried out on core 0. If you assign core 0 too many extra tasks, your Wi-Fi and BLE may no longer work reliably. If you don’t use these functions, you don’t need to worry about this issue.

Synchronizing FreeRTOS Tasks – Semaphore and Mutex

If tasks access the same resources, such as external sensors, EEPROMs, ADCs, serial communication, etc., they could interfere with each other with unforeseeable consequences. If this could potentially happen, then the tasks must be synchronized (in the sense of coordinated, not simultaneously). There are two techniques for this, namely semaphores and mutex.

The term semaphore comes from ancient Greek. Sema means sign and phoros means carrying. Put together, this means “signal transmitter”, like, for example, traffic lights. In other words: the (alternatively: the) semaphore gives the task the signal to be allowed to start.

Mutex stands for “Mutual Exclusion Object”, which means that the tasks are mutually excluded. The mutex is very similar to the semaphore, but there are a few subtle differences (see also here in the FreeRTOS documentation).

You could say it’s like the baton at a relay race. Only those who have it are allowed to start running. However, the comparison is a little off because the handover runner is allowed to continue running.

Semaphores

There are two types of semaphores, namely binary semaphores and counting semaphores. I will explain the difference using examples.

Binary Semaphores – Binary Semaphores

In the first step, you create a semaphore handle with SemaphoreHandle_t xSemaphore;, which you use in the second step to fill the semaphore with life: xSemaphore = xSemaphoreCreateBinary();. You can replace “xSemaphore” with a name of your choice. In the following, I will refer to the semaphore as “sem” for short.

And these are the rules of the game: for the semaphore to be used, it must be released with xSemaphoreGive(sem). To receive the semaphore, there is the function xSemaphoreTake(sem, xTicksToWait) with xTicksToWait as the maximum waiting time in ticks. If the semaphore was accepted within the waiting time, the function return value is pdTRUE, otherwise pdFALSE.

Each task can release the semaphore provided it is in its possession. And every task can accept the semaphore if it is available.

You often want the task to wait as long as necessary. In this case, you pass portMAX_DELAY as xTicksToWait. On the ESP32 portMAX_DELAY is 232 – 1, on the AVR Arduinos it is 216 – 1.

Binary Semaphores – Example Sketch I

We flash two LEDs again, but this time alternately. First one LED should flash 10 times, then the other and so on alternately.

#define LED1 25
#define LED2 26

SemaphoreHandle_t sem; // Create semaphore handle

void setup() {
    sem = xSemaphoreCreateBinary(); // Create binary semaphore
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        2048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
    xSemaphoreGive(sem);
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        xSemaphoreTake(sem,portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        xSemaphoreGive(sem);
        delay(100); // Short delay is needed!
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        xSemaphoreTake(sem,portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        xSemaphoreGive(sem);
        delay(100); // Short delay is needed!
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <semphr.h>  // Needed in case if you want to use semaphores

#define LED1 7
#define LED2 8

SemaphoreHandle_t sem;  // Create semaphore handle

void setup() {
    sem = xSemaphoreCreateBinary(); // Create binary semaphore
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,         // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
    xSemaphoreGive(sem);  // Release semaphore
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        xSemaphoreTake(sem,portMAX_DELAY); // Take semaphore
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        xSemaphoreGive(sem); // Release semaphore
        delay(100); // Short delay is needed!
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        xSemaphoreTake(sem,portMAX_DELAY); // Take semaphore
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        xSemaphoreGive(sem);  // Release semaphore
        delay(100); // Short delay is needed!
    }
}

void loop(){}

 

The semaphore is created and released in the setup. One of the two tasks takes it, and this task can start. As long as the semaphore is not released again, the second task must wait. If the semaphore was released by the first task and accepted by the second, the first task must wait again when it encounters the xSemaphoreTake() function.

Passing the semaphore does not work without a short delay() after xSemaphoreGive(). Apparently, the task currently executing “grabs” the semaphore itself. The delay() must therefore be at least one tick long.

In this example, we have not clearly defined which of the two tasks is executed first after the program starts. You can control this by giving one of the two tasks in xTaskCreate() a higher priority.

For the AVR Arduino version, you need to include the library file semphr.h in order to be able to use the semaphore functions.

Binary Semaphores – Example Sketch II

The last example sketch may have given the impression that the binary semaphore is exclusive , i.e. it only allows one of the tasks to be executed. The next sketch shows that this is not the case:

#define LED1 25
#define LED2 26

SemaphoreHandle_t sem; // Create semaphore handle 
void setup() {
    sem = xSemaphoreCreateBinary(); // Create a binary semaphore
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        2048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
       for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        xSemaphoreGive(sem); // Release semaphore
        delay(7000); // Give time to execute blink2
    }
}

void blink2(void *pvParameters){
    while(1){
    pinMode(LED2, OUTPUT);
        xSemaphoreTake(sem,portMAX_DELAY); // Take semaphore
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <semphr.h>

#define LED1 7
#define LED2 8

SemaphoreHandle_t sem; // Create semaphore handle

void setup() {
    sem = xSemaphoreCreateBinary(); // Create binary semaphore
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,         // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL   // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
       for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        xSemaphoreGive(sem); // Release semaphore
        delay(7000); // Give time to execute blink2
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        xSemaphoreTake(sem, portMAX_DELAY); // Take semaphore
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
    }
}

void loop(){}

 

In contrast to the previous example, the semaphore is not released in the setup here. The blink2 task is therefore initially blocked by xSemaphoreTake(). However, this does not apply to the blink1 task. After LED1 has flashed ten times, the semaphore is released, but blink1 continues to run for another seven seconds with a delay(). In these seven seconds, the while loop in blink2 is run through once before it blocks due to a missing free semaphore. So it’s blink1’s turn and the game starts all over again.

Binary Semaphores – Example Sketch III

I would like to present a third example sketch to deepen the knowledge. Here we let LED1 flash ten times again, but after the fifth flash, LED2 flashes ten times quickly while LED 1 continues to flash. This perhaps shows a little more clearly how the tasks are controlled in terms of time by semaphores.

#define LED1 25
#define LED2 26

SemaphoreHandle_t sem;  // Create semaphore handle 

void setup() {
    sem = xSemaphoreCreateBinary(); // Create binary semaphore 
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        2048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
       for(int i=0; i<5; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        xSemaphoreGive(sem); // Release semaphore
        for(int i=0; i<5; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        delay(2000);
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        xSemaphoreTake(sem,portMAX_DELAY); // take semaphore
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(50);
            digitalWrite(LED2, LOW);
            delay(50);   
        }
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <semphr.h>
#define LED1 7
#define LED2 8

SemaphoreHandle_t sem; // create semaphore handle 

void setup() {
    sem = xSemaphoreCreateBinary(); // create binary semaphore
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        128,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL   // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
       for(int i=0; i<5; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        xSemaphoreGive(sem);  // release semaphore
        for(int i=0; i<5; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        delay(2000);
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        xSemaphoreTake(sem,portMAX_DELAY); // take semaphore
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(50);
            digitalWrite(LED2, LOW);
            delay(50);   
        }
    }
}

void loop(){}

 

Binary Semaphores – Interrupt-Controlled

In the last example sketch for binary semaphores, I would like to show you how to release semaphores in an interrupt routine (ISR). To try out the following sketch, connect two buttons to your ESP32 or AVR Arduino. Connect the buttons to an interrupt pin on one side and to GND on the other. Otherwise, the LEDs are used again. One push-button should cause LED1 to flash, the other push-button controls LED2.

There is only one new aspect to this task. You do not use xSemaphoreGive(xSemaphore) in the ISR as before, but the function xSemaphoreGiveFromISR(xSemaphore,*pxHigherPriorityTaskWoken). For the meaning of the second parameter, see here, in the FreeRTOS documentation. For the sake of simplicity, we set the parameter in the example to NULL.

The rest of the sketch should be understandable without further explanation.

#define LED1 25
#define LED2 26
#define INT_PIN_1 15
#define INT_PIN_2 16

SemaphoreHandle_t interruptSemaphore1; // Create semaphore handle
SemaphoreHandle_t interruptSemaphore2;

void IRAM_ATTR keyISR1() {   // ISR definition
    xSemaphoreGiveFromISR(interruptSemaphore1, NULL);
}

void IRAM_ATTR keyISR2() {
    xSemaphoreGiveFromISR(interruptSemaphore2, NULL);
}

void setup() {
    interruptSemaphore1 = xSemaphoreCreateBinary(); // Create semaphore
    interruptSemaphore2 = xSemaphoreCreateBinary();

    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

    xTaskCreate(
        blink2,      // Function name of the task
        "Blink 2",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

    if (interruptSemaphore1 != NULL) {
        attachInterrupt(digitalPinToInterrupt(INT_PIN_1), keyISR1, FALLING);
    }
    if (interruptSemaphore2 != NULL) {
        attachInterrupt(digitalPinToInterrupt(INT_PIN_2), keyISR2, FALLING);
    }
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    pinMode(INT_PIN_1, INPUT_PULLUP);
    while(1){
        if (xSemaphoreTake(interruptSemaphore1, portMAX_DELAY)) {
            for(int i=0; i<10; i++){
                digitalWrite(LED1, HIGH);
                delay(50); // Delay for Tasks 
                digitalWrite(LED1, LOW);
                delay(50);
            }
        }
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    pinMode(INT_PIN_2, INPUT_PULLUP);
    while(1){
        if (xSemaphoreTake(interruptSemaphore2, portMAX_DELAY)) {
            for(int i=0; i<10; i++){
                digitalWrite(LED2, HIGH);
                delay(50);  
                digitalWrite(LED2, LOW);
                delay(50);
            }
        }
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <semphr.h>
#define LED1 7
#define LED2 8
#define INT_PIN_1 2
#define INT_PIN_2 3

SemaphoreHandle_t interruptSemaphore1; // Create semaphore handle
SemaphoreHandle_t interruptSemaphore2;

void keyISR1() {  // ISR definition
    xSemaphoreGiveFromISR(interruptSemaphore1, NULL);
}

void keyISR2() {
    xSemaphoreGiveFromISR(interruptSemaphore2, NULL);
}

void setup() {
    interruptSemaphore1 = xSemaphoreCreateBinary();  // Create semaphore
    interruptSemaphore2 = xSemaphoreCreateBinary();

    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,         // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

    xTaskCreate(
        blink2,      // Function name of the task
        "Blink 2",   // Name of the task (e.g. for debugging)
        128,         // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

    if (interruptSemaphore1 != NULL) {
        attachInterrupt(digitalPinToInterrupt(INT_PIN_1), keyISR1, FALLING);
    }
    if (interruptSemaphore2 != NULL) {
        attachInterrupt(digitalPinToInterrupt(INT_PIN_2), keyISR2, FALLING);
    }
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    pinMode(INT_PIN_1, INPUT_PULLUP);
    while(1){
        if (xSemaphoreTake(interruptSemaphore1, portMAX_DELAY)) {
            for(int i=0; i<10; i++){
                digitalWrite(LED1, HIGH);
                delay(50);  
                digitalWrite(LED1, LOW);
                delay(50);
            }
        }
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    pinMode(INT_PIN_2, INPUT_PULLUP);
    while(1){
        if (xSemaphoreTake(interruptSemaphore2, portMAX_DELAY)) {
            for(int i=0; i<10; i++){
                digitalWrite(LED2, HIGH);
                delay(50);  
                digitalWrite(LED2, LOW);
                delay(50);
            }
        }
    }
}

void loop(){}

 

Counting Semaphores

You can create counting semaphores with xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ). The parameter uxMaxCount specifies how many copies of this semaphore may be in circulation. The parameter uxInitialCount defines how many copies of the semaphore are initially available. Each xSemaphoreTake() reduces the number of available semaphores by 1, and each xSemaphoreGive() increases it by 1, provided that the limits 0 and uxMaxCount are not undercut or exceeded.

The expression xSemaphoreCreateCounting(1,0) is therefore similar to xSemaphoreCreateBinary(). You can try this out by making the corresponding substitution in freertos_esp32_semaphores_binary_I.ino or freertos_avr_semaphores_binary_I.ino. With xSemaphoreCreateCounting(1,1) you could also save xSemaphoreGive(sem) in the setup.

Counting Semaphores – Example Sketch I

In the first example sketch, we use two counting semaphores to make two LEDs flash ten times alternately. We use the first semaphore (countingSem) like a binary semaphore to prevent simultaneous flashing. The other semaphore (countingSem2) limits the total number of flashing sequences to four. We achieve this by setting uxMaxCount and uxInitialCount to 4 and by the tasks only taking the countingSem2 but not releasing it.

We keep ourselves informed about how many of the countingSem2 semaphores are still available via uxSemaphoreGetCount(countingSem2).

#define LED1 25
#define LED2 26

SemaphoreHandle_t countingSem; // Create handle 
SemaphoreHandle_t countingSem2;

void setup() {
    Serial.begin(115200);
    pinMode(LED1, OUTPUT);
    pinMode(LED2, OUTPUT);

    countingSem = xSemaphoreCreateCounting(1,1); // Create counting semaphore
    countingSem2 = xSemaphoreCreateCounting(4,4); // Create second counting semaphore  

    Serial.print("countingSem2 left: ");
    Serial.println(uxSemaphoreGetCount(countingSem2));
    
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        2048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    while(1){
        /* Take the semaphore, no semaphore (countingSem) will be left */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        /* Take the semaphore countingSem2 if still available */
        xSemaphoreTake(countingSem2, portMAX_DELAY);
        Serial.print("countingSem2 left: ");
        Serial.println(uxSemaphoreGetCount(countingSem2));
        
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250); 
        }
        /* Give only semaphore countingSem */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed!
    }
}

void blink2(void *pvParameters){
    while(1){
        /* Take the semaphore, no semaphore (countingSem) will be left */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        /* Take the semaphore countingSem2 if still available */
        xSemaphoreTake(countingSem2, portMAX_DELAY);
        Serial.print("countingSem2 left: ");
        Serial.println(uxSemaphoreGetCount(countingSem2));

        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        /* Give only semaphore countingSem */
        xSemaphoreGive(countingSem);
        delay(200);  // Short delay is needed!
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <semphr.h>
#define LED1 7
#define LED2 8

SemaphoreHandle_t countingSem; // Create handle 
SemaphoreHandle_t countingSem2; 

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

    countingSem = xSemaphoreCreateCounting(1,1); // Create counting semaphore
    countingSem2 = xSemaphoreCreateCounting(4,4); // Create second counting semaphore 

    Serial.print("countingSem2 left: ");
    Serial.println(uxSemaphoreGetCount(countingSem2));
    
    xTaskCreate(
        blink1,     // Function name of the task
        "Blink 1",  // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
    
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        /* Take the semaphore, no semaphore (countingSem) will be left */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        /* Take the semaphore countingSem2 if still available */
        xSemaphoreTake(countingSem2, portMAX_DELAY);
        Serial.print("countingSem2 left: ");
        Serial.println(uxSemaphoreGetCount(countingSem2));
        
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);
            digitalWrite(LED1, LOW);
            delay(250);
        }
        /* Give only semaphore countingSem */
        xSemaphoreGive(countingSem);
        delay(200);  // Short delay is needed!
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        /* Take the semaphore, no semaphore (countingSem) will be left */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        /* Take the semaphore countingSem2 if still available */
        xSemaphoreTake(countingSem2, portMAX_DELAY);
        Serial.print("countingSem2 left: ");
        Serial.println(uxSemaphoreGetCount(countingSem2));

        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        /* Give only semaphore countingSem */
        xSemaphoreGive(countingSem);
        delay(200);  // Short delay is needed!
    }
}

void loop(){}

 

A little tip: If you no longer need tasks after running them x times, you should delete them with vTaskDelete( taskHandle ) so that you release the memory space. I have refrained from doing this in the example to focus on the essentials.

Counting Semaphores – Example sketch II

In the second example sketch, we let four LEDs flash, but limit the number of LEDs that can flash simultaneously to two. We realize this with a counting semaphore, which we set to uxMaxCount = 2.

#define LED1 25
#define LED2 26
#define LED3 17
#define LED4 18
SemaphoreHandle_t countingSem; // Create semaphore handle

void setup() {
    countingSem = xSemaphoreCreateCounting(2,2); // Create counting semaphore
   
    xTaskCreate(blink1, "Blink 1", 2048, NULL, 1, NULL); 
    xTaskCreate(blink2, "Blink 2", 2048, NULL, 1, NULL);
    xTaskCreate(blink3, "Blink 3", 2048, NULL, 1, NULL);
    xTaskCreate(blink4, "Blink 4", 2048, NULL, 1, NULL);   
}

void blink1(void *parameter){
    pinMode(LED1, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY); 
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250); 
            digitalWrite(LED1, LOW);
            delay(250); 
        }
        /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void blink2(void *parameter){
    pinMode(LED2, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void blink3(void *parameter){
    pinMode(LED3, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED3, HIGH);
            delay(123);//delay(333);
            digitalWrite(LED3, LOW);
            delay(123);   
        }
        /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void blink4(void *parameter){
    pinMode(LED4, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED4, HIGH);
            delay(444);//delay(333);
            digitalWrite(LED4, LOW);
            delay(444);   
        }
        /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <semphr.h>
#define LED1 7
#define LED2 8
#define LED3 9
#define LED4 10
SemaphoreHandle_t countingSem; // Create semaphore handle

void setup() {
    countingSem = xSemaphoreCreateCounting(2,2); // Create counting semaphore
   
    xTaskCreate(blink1, "Blink 1", 128, NULL, 1, NULL); 
    xTaskCreate(blink2, "Blink 2", 128, NULL, 1, NULL);
    xTaskCreate(blink3, "Blink 3", 128, NULL, 1, NULL);
    xTaskCreate(blink4, "Blink 4", 128, NULL, 1, NULL);   
}

void blink1(void *parameter){
    pinMode(LED1, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY); 
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250); 
            digitalWrite(LED1, LOW);
            delay(250); 
        }
        /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void blink2(void *parameter){
    pinMode(LED2, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);//delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void blink3(void *parameter){
    pinMode(LED3, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED3, HIGH);
            delay(123);//delay(333);
            digitalWrite(LED3, LOW);
            delay(123);   
        }
         /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void blink4(void *parameter){
    pinMode(LED4, OUTPUT);
    while(1){
        /* Take a semaphore if available */
        xSemaphoreTake(countingSem, portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED4, HIGH);
            delay(444);//delay(333);
            digitalWrite(LED4, LOW);
            delay(444);   
        }
         /* Release the semaphore */
        xSemaphoreGive(countingSem);
        delay(200); // Short delay is needed
    }
}

void loop(){}

 

Mutex Objects

Once you have understood the binary and counting semaphores, the mutex objects are no longer a challenge for you. You create a mutex with xSemaphoreCreateMutex(). Otherwise, you deal with it in the same way as you are used to with semaphores. The mutex does not have to be made available via xSemaphoreGive(). In this respect, it behaves similarly to a counting semaphore that you create with xSemaphorCreateCounting(1,1)

Another interesting feature is that you can use xSemaphoreGetMutexHolder() to find out which task currently owns the mutex. There is no such function for the semaphore.

Here is an example sketch that illustrates the close relationship to the semaphore.

#define LED1 25
#define LED2 26

SemaphoreHandle_t mutex; // Create handle

void setup() {
    mutex = xSemaphoreCreateMutex(); //Create the mutex object
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        2048,       // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        xSemaphoreTake(mutex,portMAX_DELAY); // Take the mutex
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);  
            digitalWrite(LED1, LOW);
            delay(250); 
        }
        xSemaphoreGive(mutex); // Releases the mutex
        delay(200); // Short delay is needed!
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        xSemaphoreTake(mutex,portMAX_DELAY); // Take the mutex
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        xSemaphoreGive(mutex); // // Release the mutex
        delay(200); // Short delay is needed!
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <semphr.h>
#define LED1 7
#define LED2 8

SemaphoreHandle_t mutex;

void setup() {
    mutex = xSemaphoreCreateMutex();
         
    xTaskCreate(
        blink1,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,         // Stack size (bytes)
        NULL,        // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    
    xTaskCreate(
        blink2,     // Function name of the task
        "Blink 2",  // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        NULL,       // Parameter to pass
        1,          // Task priority
        NULL        // Task handle
    );
}

void blink1(void *pvParameters){
    pinMode(LED1, OUTPUT);
    while(1){
        xSemaphoreTake(mutex,portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED1, HIGH);
            delay(250);  
            digitalWrite(LED1, LOW);
            delay(250); 
        }
        xSemaphoreGive(mutex);
        delay(200); // Short delay is needed!
    }
}

void blink2(void *pvParameters){
    pinMode(LED2, OUTPUT);
    while(1){
        xSemaphoreTake(mutex,portMAX_DELAY);
        for(int i=0; i<10; i++){
            digitalWrite(LED2, HIGH);
            delay(333);
            digitalWrite(LED2, LOW);
            delay(333);   
        }
        xSemaphoreGive(mutex);
        delay(200); /// Short delay is needed!
    }
}

void loop(){}

 

Passing parameters to tasks

One-time Passing as *pvParameters

We have not yet used one of the parameters of xCreateTask(), namely *pvParameters. We want to change that now. *pvParameters allows us to pass parameters to the task to be created. It is in the nature of things that we can only use this access once, namely when creating the task.

In the example sketches, we have used blink task functions that differ only in the flashing period and the LED to be controlled. This is where parameter passing can help us to streamline the code. However, we have to juggle a little with pointers.

We play this out in a simple sketch. First, let’s take a look at the code.

#define LED1 25
#define LED2 26

struct genericBlink{
    int pin;
    int period;
};

genericBlink ledBlink1 = {LED1, 500};
genericBlink ledBlink2 = {LED2, 333};

void setup() {
    xTaskCreate(
        &blink,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        (void*) &ledBlink1,  // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        &blink,      // Function name of the task
        "Blink 2",   // Name of the task (e.g. for debugging)
        2048,        // Stack size (bytes)
        (void*) &ledBlink2,  // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

}

void blink(void *ledx){
    genericBlink *ledBlink = (genericBlink *) ledx;
    pinMode(ledBlink->pin, OUTPUT);
    while(1){
        digitalWrite(ledBlink->pin, HIGH);
        delay(ledBlink->period);
        digitalWrite(ledBlink->pin, LOW);
        delay(ledBlink->period);
    }
}

// void blink(void *ledx){
//     genericBlink ledBlink = *(genericBlink *) ledx;
//     pinMode(ledBlink.pin, OUTPUT);
//     while(1){
//         digitalWrite(ledBlink.pin, HIGH);
//         delay(ledBlink.period);
//         digitalWrite(ledBlink.pin, LOW);
//         delay(ledBlink.period);
//     }
// }

void loop(){}
#include<Arduino_FreeRTOS.h>
#define LED1 7
#define LED2 8

struct genericBlink{
    int pin;
    int period;
};

genericBlink ledBlink1 = {LED1, 500};
genericBlink ledBlink2 = {LED2, 333};

void setup() {
    xTaskCreate(
        &blink,      // Function name of the task
        "Blink 1",   // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        (void*) &ledBlink1,  // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );
    xTaskCreate(
        &blink,      // Function name of the task
        "Blink 2",   // Name of the task (e.g. for debugging)
        128,        // Stack size (bytes)
        (void*) &ledBlink2,  // Parameter to pass
        1,           // Task priority
        NULL         // Task handle
    );

}

void blink(void *ledx){
    genericBlink *ledBlink = (genericBlink *) ledx;
    pinMode(ledBlink->pin, OUTPUT);
    while(1){
        digitalWrite(ledBlink->pin, HIGH);
        delay(ledBlink->period);
        digitalWrite(ledBlink->pin, LOW);
        delay(ledBlink->period);
    }
}

// void blink(void *ledx){
//     genericBlink ledBlink = *(genericBlink *) ledx;
//     pinMode(ledBlink.pin, OUTPUT);
//     while(1){
//         digitalWrite(ledBlink.pin, HIGH);
//         delay(ledBlink.period);
//         digitalWrite(ledBlink.pin, LOW);
//         delay(ledBlink.period);
//     }
// }

void loop(){}

 

Explanations to the sketch

Arrays or structures (struct) can be used to pass data as pointers. Structures are the method of choice if several parameters of different data types are to be passed. In our example sketch, we define a “genericBlink” structure that contains the LED (pin) and the flashing period. The variables ledBlink1 and ledBlink2 are implementations of this structure. ledBlink1 and ledBlink2 are passed to the tasks in xCreateTask() as a reference, i.e. with the preceding &.

The advantage of passing parameters is that we can limit ourselves to just one blink function.

The blink function does not “know” which data type the pointer *ledx represents. Through the line:

genericBlink *ledBlink = (genericBlink *) ledx;

we create the pointer ledBlink from the undefined pointer ledx. For ledBlink, the function knows that it points to a structure of the type genericBlink. This allows us to access the elements of the structure. However, since ledBlink is not the structure itself, but only the pointer to the structure, the access is indirect, and we must therefore use the arrow operator -> instead of the dot operator.

The alternative would be to create a copy of the structure (which costs more memory). I have shown this in the commented-out lines of the example sketch. In this case, the pointer must be dereferenced using *:

genericBlink ledBlink = *(genericBlink *) ledx;

Enough of the pointers at this point. If you would like to know more about the topic, take a look here, for example.

Passing Parameters with Queues

Passing Simple Data Types with Queues

If tasks are to exchange data during their runtime, you have to use so-called queues. Let’s take a look at this using a simple, rather pointless example. We are creating two tasks for this. One task determines the time period since the program was started and communicates it to the other task, which then displays it on the screen.

Four new elements and functions are used:

  • QueueHandle_t xQueue:  generates the queue handle xQueue.
  • xQueueCreate(uxQueueLength, uxItemSize):  creates a queue. uxQueueLength is the maximum number of elements to be sent. uxItemSize defines the size of the individual elements in bytes.
  • xQueueSend(xQueue, *pvItemToQueue, xTicksToWait):  sends the queue. Here, xQueue is the queue handle and *pvItemToQueue is the pointer to the data to be sent. xTicksToWait is the maximum time in ticks that the task should wait to get sending time – or even more figuratively: to get a place in the queue.
  • xQueueReceive(xQueue, *pvBuffer, xTicksToWait):  receives the queue with the handle xQueue. *pvBuffer is the pointer to the address to which the data is copied. xTicksToWait is the maximum time in ticks that the receiving task should wait until it receives data.
QueueHandle_t queue1; // Create handle

void setup() {
    Serial.begin(115200);
    queue1 = xQueueCreate(1, sizeof(unsigned long));  // Create queue
  
    xTaskCreate(
        measureTime,    // Function name of the task
        "Measure Time", // Name of the task (e.g. for debugging)
        2048,           // Stack size (bytes)
        NULL,           // Parameter to pass
        1,              // Task priority
        NULL            // Task handle
    );
    xTaskCreate(
        printTime,    // Function name of the task
        "Print time", // Name of the task (e.g. for debugging)
        2048,         // Stack size (bytes)
        NULL,         // Parameter to pass
        1,            // Task priority
        NULL          // Task handle
    );
}

void measureTime(void *pvParameters){
    while(1){
        static unsigned long startTime = millis();
        unsigned long timeSinceStart = (millis() - startTime)/1000;
        xQueueSend(queue1, &timeSinceStart, 0);  // Send queue
        delay(100);
    }
}

void printTime(void *pvParameters){
    while(1){
        unsigned long buf;
        xQueueReceive(queue1, &buf, 0); // Receive queue
        Serial.print("Time since start: ");
        Serial.println(buf);
        delay(2000);
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <queue.h>

QueueHandle_t queue1;  // Create handle

void setup() {
    Serial.begin(9600);
    queue1 = xQueueCreate(1, sizeof(unsigned long));  // Create queue
  
    xTaskCreate(
        measureTime,    // Function name of the task
        "Measure Time", // Name of the task (e.g. for debugging)
        128,            // Stack size (bytes)
        NULL,           // Parameter to pass
        1,              // Task priority
        NULL            // Task handle
    );
    
    xTaskCreate(
        printTime,    // Function name of the task
        "Print time", // Name of the task (e.g. for debugging)
        128,          // Stack size (bytes)
        NULL,         // Parameter to pass
        1,            // Task priority
        NULL          // Task handle
    );
}

void measureTime(void *pvParameters){
    while(1){
        static unsigned long startTime = millis();
        unsigned long timeSinceStart = (millis() - startTime)/1000;
        xQueueSend(queue1, &timeSinceStart, 0);  // Send queue
        delay(100);
    }
}

void printTime(void *pvParameters){
    while(1){
        unsigned long buf;
        xQueueReceive(queue1, &buf, 0);  // Receive queue
        Serial.print("Time since start: ");
        Serial.println(buf);
        delay(2000);
    }
}

void loop(){}

 

I don’t think the sketch needs much explanation, do you? Again, please note that the data is sent or received as a pointer, so please remember the & operators. And just try out what happens if you set delay() in measureTime() to 10000, for example. In a second step, you could also set xTicksToWait in printTime() to 15000.

Passing structures with queues

As the last point of this FreeRTOS introduction, we will look at passing a structure to a task. In the example, the structure consists of the result of a analogRead() (i.e. some imaginary sensor measurement) and the time of the measurement.

#define SENSOR_PIN 34

QueueHandle_t queue1;  // Create handle

struct dataPack{    // define struct for the variables to be sent
    unsigned long sensorTime;
    int sensorValue;
};

void setup() {
    Serial.begin(115200);
    queue1 = xQueueCreate(1, sizeof(dataPack));  // Create queue
    
    xTaskCreate(
        getSensorData,      // Function name of the task
        "Get Sensor Data",  // Name of the task (e.g. for debugging)
        2048,               // Stack size (bytes)
        NULL,               // Parameter to pass
        1,                  // Task priority
        NULL                // Task handle
    );
    xTaskCreate(
        printSensor,        // Function name of the task
        "Print sensor",     // Name of the task (e.g. for debugging)
        2048,               // Stack size (bytes)
        NULL, // Parameter to pass
        1,                  // Task priority
        NULL                // Task handle
    );
}

void getSensorData(void *pvParameters){
    static unsigned long startTime = millis();
    while(1){
        dataPack sensorData = {0,0};
        sensorData.sensorTime = (millis() - startTime)/1000;
        sensorData.sensorValue = (analogRead(SENSOR_PIN));
        xQueueSend(queue1, &sensorData, 0);  // Send queue
        delay(500);
    }
}

void printSensor(void *pvParameters){
    while(1){
        dataPack currentData;
        xQueueReceive(queue1, &currentData,0); // Receive queue
        Serial.print("Time: ");
        Serial.println(currentData.sensorTime);
        Serial.print("Value: ");
        Serial.println(currentData.sensorValue);
        delay(2000);
    }
}

void loop(){}
#include <Arduino_FreeRTOS.h>
#include <queue.h>
#define SENSOR_PIN A0

QueueHandle_t queue1;  // Create handle

struct dataPack{  // Create struct for the data to be sent
    unsigned long sensorTime;
    int sensorValue;
};

void setup() {
    Serial.begin(9600);
    queue1 = xQueueCreate(1, sizeof(dataPack)); // Create queue
    
    xTaskCreate(
        getSensorData,     // Function name of the task
        "Get Sensor Data", // Name of the task (e.g. for debugging)
        128,               // Stack size (bytes)
        NULL,              // Parameter to pass
        1,                 // Task priority
        NULL               // Task handle
    );
    
    xTaskCreate(
        printSensor,       // Function name of the task
        "Print sensor",    // Name of the task (e.g. for debugging)
        128,               // Stack size (bytes)
        NULL, // Parameter to pass
        1,                 // Task priority
        NULL               // Task handle
    );
}

void getSensorData(void *pvParameters){
    while(1){
        static unsigned long startTime = millis();
        dataPack sensorData = {0,0};
        sensorData.sensorTime = (millis() - startTime)/1000;
        sensorData.sensorValue = (analogRead(SENSOR_PIN));
        xQueueSend(queue1, &sensorData, 0); // Send queue
        delay(500);
    }
}

void printSensor(void *pvParameters){
    while(1){
        dataPack currentData;
        xQueueReceive(queue1, &currentData, 0);  // Receive queue
        Serial.print("Time: ");
        Serial.println(currentData.sensorTime);
        Serial.print("Value: ");
        Serial.println(currentData.sensorValue);
        delay(2000);
    }
}

void loop(){}

 

The sketch should not really need any further explanation.

8 thoughts on “Using FreeRTOS with ESP32 and Arduino

  1. Thank you for the clear explanations and the examples!

    I was already using FreeRTOS in my projects, and some of the parts I’ve written I don’t think I’ve fully understood (especially in regards to semaphores). Furthermore, I haven’t even thought of using xQueue so far (I just triggered an interrupt if a buffer became full in one function, and then to read the contents in the buffer from another function when interrupt occurred), so thank you for everything!

    Just a quick question, is there a big advantage of using xQueue instead of the method I’ve described above? It’s much simpler for sure, and my second function has to continuously check to see if an interrupt has occurred. Is there anything you can advise on this?

    Much thank you for the post and your reply!

    1. Can you explain how you create a buffer which is accessible by different tasks? Do you define it as a global variable? Sounds very pragmatic.

  2. Invaluable resource. Was struggling to implement RTOS tasks, your implementation works as opposed to the other official documentation I was reading before. Thank you sir!

  3. Hello.
    Your explanation is very useful and very interesting.
    Thank you very much for sharing this content.

  4. This is an extraordinarily useful introduction to multitasking on Arduino! I’m using an ESP32-S3 and this post is the most enlightening and thorough description I’ve found. I am an experienced web programmer but C/C++ is sometimes tricky for me to wrap my head around. Your detail is in all the right places. Thank you so much!

Leave a Reply

Your email address will not be published. Required fields are marked *