ESP32 ESP-IDF FreeRTOS Tutorial: Learn to Create Tasks

In this ESP32 ESP-IDF FreeRTOS tutorial, we will learn to use FreeRTOS with ESP32 using ESP-IDF framework. The good thing about ESP-IDF is that it already has a port of FreeRTOS for ESP32. Therefore, we do not need to install additional libraries and drivers. ESP-IDF FreeRTOS is based on Vanilla FreeRTOS v10.4.3

In this tutorial, we will learn to create FreeRTSO tasks such as task creation, deletion, priority setting, Task interrupts, etc. When using ESP-IDF, the majority of the codes use the functionalities of Vanilla FreeRTOS v10.4.3, therefore it is an important aspect while programming. Let’s start by showing you how to create and delete tasks and then move towards FreeRTOS Interrupt tasks.

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

Components Required:

ESP32 Development board: You can buy from here.

FreeRTOS Tasks Introduction

In a real-time application or RTOS, an application usually consists of a set of independent tasks or subroutine. On a single-core MCU, only one task can run at a time. On the other hand, in a dual-core processor such as ESP32 two tasks can run concurrently given these two tasks have no dependency on each other. FreeRTOS scheduler schedules these tasks based on their priority, time period, and execution time.

The FreeRTOS scheduler will frequently start and stop every task as the application keeps executing. A task does not have an understanding of the RTOS scheduler activity, therefore, it is the responsibility of the FreeRTOS scheduler to confirm that the processor context (register values, stack contents, etc) when a task is switched in is exactly that as when the same task was swapped out. For this purpose, each task is provided with its own stack. When the task is swapped out the execution context is saved to the stack of that task so it can also be exactly restored when the same task is later swapped back in. You can find more information about FreeRTOS tasks here.

FreeRTOS APIs provide features to schedule, create, delete, suspend, resume, and setting tasks priority. We will see examples of each of these throughout this tutorial.

ESP32 ESP-IDF FreeRTOS Create Task Example

Open your VS Code and create a new ESP-IDF project. Now head over to the main.c file. We will define the functions and the program code here.

To start off, include the FreeRTOS header files. The freertos/task.h library will enable us to use the FreeRTOS tasks.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

xTaskCreate() for first task

Inside the main() function, we will create the tasks. To create a task, use the function xTaskCreate(). This function takes in several arguments.

  • The first argument is the name of the function. In our case, we have set it to Demo_Task.
  • The second argument is the name of the task for descriptive purposes. This is usually required to help in debugging and to get a task handle. It is specified in double quotation marks so in our case we set the second argument as “Demo_Task.”
  • The third argument specifies the stack size of the task. This indicates the amount of memory we want to reserve for the particular task. For more efficiency, the user can set the stack size after creating the task and obtaining the stack size of the task. We have set it to ‘4096’.
  • The fourth argument is the parameter. It is a value that is passed as the parameter to the created task. This can be used to specify a variable that is being used in the main function and whose value needs to be added to the task. Another way of implementation is to create a global variable instead. In our case, we will set this argument as NULL which indicates that we are not using this property.
  • The fifth argument is the priority of the task. We have set it to ’10.’
  • The last argument is the handle which is used to change the function of the task eg. suspend, delete, resume, get or set a config of task. This works as a pointer therefore the ampersand symbol is used with it. It is optional so we can also set it to NULL. In our case we have set it to &myTaskHandle.
xTaskCreate(Demo_Task, "Demo_Task", 4096, NULL, 10, &myTaskHandle);
FreeRTOS Create Tasks Example1 1

Task Handle

The task handle will be defined as a global variable outside the main function. We have set it as NULL.

TaskHandle_t myTaskHandle = NULL;
FreeRTOS Create Tasks Example1 2

Function of first task

Now we will create the function definition of Demo_Task. Here inside the infinite while loop we are printing a message on the serial terminal after every second. You have to use an infinite loop otherwise the microcontroller will keep on resetting.

void Demo_Task(void *arg)
{
    while(1){
        printf("Demo_Task printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}
FreeRTOS Create Tasks Example1 3

xTaskCreatePinnedToCore() for second task

Similarly, let us create another task Demo_Task2 and define its function so that we can see both the tasks running simultaneously. To do that we will use the xTaskCreatePinnedToCore() function instead. It will enable the user to select which ESP32 core (core 0 or core 1) will run the particular task. Let us select core1 for the second task. The xTaskCreatePinnedToCore() takes in one extra argument as compared to xTaskCreate() function. It is the seventh argument that indicates the core that will be used for the created task. The user can specify ‘0’ or 1′ as an argument.

Let us select core1 for the second task which is Demo_Task2.

xTaskCreatePinnedToCore(Demo_Task2, "Demo_Task2", 4096, NULL,10, &myTaskHandle2, 1);

Similarly, we define the task handle for the second task as shown below:

TaskHandle_t myTaskHandle2 = NULL;

This is the function definition for this task.

void Demo_Task2(void *arg)
{
    while(1){
        printf("Demo_Task2 printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}
FreeRTOS Create Tasks Example1 4

Complete Code & Demonstration

This is the complete code that we are using. Here we have created two tasks which will run simultaneously. Demo_Task1 prints “Demo_Task printing..” and Demo_Task2 prints “Demo_Task printing..” in the serial terminal.

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

TaskHandle_t myTaskHandle = NULL;
TaskHandle_t myTaskHandle2 = NULL;

void Demo_Task(void *arg)
{
    while(1){
        printf("Demo_Task printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

void Demo_Task2(void *arg)
{
    while(1){
        printf("Demo_Task2 printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

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

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

The serial terminal will display the messages as our tasks keep on running infinitely.

FreeRTOS Create Tasks Example1 demo serial terminal

ESP32 ESP-IDF FreeRTOS Delete Task Example

Now let us modify the above code to show you the delete task functionality.

Look at the modified Demo_Task2() function below. Instead of using an infinite while loop, we have incorporated a for loop that will print the message after every second. This loop will finish after 5s thus, the ESP32 will reset at that point.

void Demo_Task2(void *arg)
{
    for(int i=0;i<5;i++){
        printf("Demo_Task2 printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

Let us delete this task after 5s so that the ESP32 will not reset. To delete a task we use the vTaskDelete() function and specify the handle of the task to be deleted as a parameter inside it. If we pass NULL, then will cause the calling task to be deleted.

vTaskDelete(myTaskHandle2)

Inside the Demo_Task() function, first we will create an integer ‘count’ that will hold the number of seconds. When count reaches 3 then we will call the vTaskDelete() function and specify the handle of Demo_Task2 as a parameter inside it. This function will be called inside the Demo_Task() function hence, the first task will delete the second task after 5s. This way the ESP32 will not reset and the first task keeps on running.

void Demo_Task(void *arg)
{
    int count = 0;
    while(1){
        count++;
        printf("Demo_Task printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
        if (count == 3)
        {
          vTaskDelete(myTaskHandle2);
          printf("Demo_Task2 is deleted!\n");
        }
    }
}

Complete Code & Demonstration

Below you can access the complete code in which we are demonstrating the vDeleteTask() function.

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

TaskHandle_t myTaskHandle = NULL;
TaskHandle_t myTaskHandle2 = NULL;

void Demo_Task(void *arg)
{
    int count = 0;
    while(1){
        count++;
        printf("Demo_Task printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
        if (count == 5)
        {
          vTaskDelete(myTaskHandle2);
          printf("Demo_Task2 is deleted!\n");
        }
    }
}

void Demo_Task2(void *arg)
{
    for(int i=0;i<5;i++){
        printf("Demo_Task2 printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

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

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

The output of the serial terminal is shown below. Notice that Demo_Task2 ran for 5 seconds and then it was deleted. Demo_Task continues to run infinitely without the ESP32 resetting.

FreeRTOS Delete Tasks Example

ESP32 ESP-IDF FreeRTOS Suspend & Resume Task Example

To suspend a task we use the vTaskSuspend() function and specify the handle of the task to be suspended as a parameter inside it. If we pass NULL, then will cause the calling task to be suspended

vTaskSuspend(myTaskHandle2);

To resume a task we use the vTaskResume() function and specify the handle of the task to be resumed as a parameter inside it. If we pass NULL, then will cause the calling task to be resumed.

 vTaskResume(myTaskHandle2);

Code Modifications

First, we will modify the code above to increase the limit of Demo_Task2 to 10 seconds.


void Demo_Task2(void *arg)
{
    for(int i=0;i<10;i++){
        printf("Demo_Task2 printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

Then, inside the Demo_Task() function, we will suspend Demo_Task2 after 5 seconds, then resume it after 3 seconds. After 10 seconds it will be deleted so that the ESP32 does not reset.

void Demo_Task(void *arg)
{
    int count = 0;
    while(1){
        count++;
        printf("Demo_Task printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
        if (count == 5)
        {
          vTaskSuspend(myTaskHandle2);
          printf("Demo_Task2 is suspended!\n");
        }
        if (count == 8)
        {
          vTaskDelete(myTaskHandle2);
          printf("Demo_Task2 is resumed!\n");
        }
        if (count == 10)
        {
          vTaskDelete(myTaskHandle2);
          printf("Demo_Task2 is deleted!\n");
        }
    }
}

Complete Code & Demonstration

Below you can access the complete code in which we are demonstrating the vSuspendTask(), vResumeTask() and vDeleteTask() functions

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

TaskHandle_t myTaskHandle = NULL;
TaskHandle_t myTaskHandle2 = NULL;

void Demo_Task(void *arg)
{
    int count = 0;
    while(1){
        count++;
        printf("Demo_Task printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
        if (count == 5)
        {
          vTaskSuspend(myTaskHandle2);
          printf("Demo_Task2 is suspended!\n");
        }
        if (count == 8)
        {
          vTaskResume(myTaskHandle2);
          printf("Demo_Task2 is resumed!\n");
        }
        if (count == 10)
        {
          vTaskDelete(myTaskHandle2);
          printf("Demo_Task2 is deleted!\n");
        }
    }
}

void Demo_Task2(void *arg)
{
    for(int i=0;i<10;i++){
        printf("Demo_Task2 printing..\n");
        vTaskDelay(1000/ portTICK_RATE_MS);
    }
}

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

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

The output of the serial terminal is shown below. Notice that Demo_Task2 ran for 5 seconds and then it was suspended for three seconds. It got resumed after the three seconds were over. After 10 seconds, the Demo_Task2 get deleted but Demo_Task continues to run infinitely without the ESP32 resetting.

FreeRTOS Suspend and Resume Tasks Example

Get Tick Count

In order to calculate the time of the application we use the function, xTaskGetTickCount(). This function returns us the number of ticks since the application started.

ESP32 ESP-IDF FreeRTOSInterrupt

Now let us look at how to use FreeRTOS interrupts. For demonstration, we will incorporate the interrupt to toggle a LED connected with the ESP32 board with a pushbutton.

ESP-IDF provides a gpio.h library that provides helpful functions to control the digital input and output pins of ESP32 and to generate interrupts. For this project, we want to successfully setup an interrupt to toggle the LED state.

The first step is to include the header file:

#include "driver/gpio.h"

To set the GPIO interrupt trigger type, we use the function, gpio_set_intr_type() which takes in two arguments. The first argument is the GPIO number whose trigger type we want to set. The second argument is the interrupt type.

gpio_set_intr_type(gpio_num_t gpio_num, gpio_int_type_t intr_type)

Next, to install the driver’s GPIO ISR handler service, we use the function, gpio_install_isr_service(). This handler service permits GPIO interrupt handlers per pin. This function takes in a single argument which is the flag that is responsible for allocating the interrupt.

gpio_install_isr_service(int intr_alloc_flags)

To add the ISR handler for a particular GPIO pin, we use the function, gpio_isr_handler_add(). This takes in three arguments. The first argument is the GPIO number, the second argument is the ISR handler for that particular GPIO and the last argument is the parameter for the ISR handler.

gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void *args)

Interrupt using a Push Button to toggle LED Schematic diagram

Let us now demonstrate how to generate interrupts in the ESP32 board using a push button to toggle an LED. The push button will be connected to an interrupt pin of ESP32 and configured as an input. Whereas the LED will be set up as a digital output. The LED will be toggled on each positive edge (0 to 1).

The following components are required:

  • ESP32 development board
  • Push button
  • One 5mm LED
  • One 220 ohm resistor
  • One 10k ohm resistor
  • Breadboard
  • Connecting Wires

Assemble your circuit as shown below:

ESP32 ESP-IDF Control LED with Push Button schematic diagram

When the pushbutton is not pressed, logic low will appear on GPIO33, or push button state will be low and when the push button is pressed, a logic high will be on GPIO33. That means a rising edge occurs when a push button is pressed. We can detect this rising edge with the help of interrupt pins of ESP32.

Note: GPIO6, GPIO7, GPIO8, GPIO9, GPIO10, and GPIO11 cannot be used as ESP32 interrupt pins.

Follow the diagram below to select the ESP32 GPIO pin that you can use to connect with the pushbutton.

ESP32 Interrupt Pins

ESP32 Toggle LED with Pushbutton using FreeRTOS Interrupts

Open your VS Code and create a new ESP-IDF project. Now head over to the main.c file. We will define the functions and the program code here.

ESP32 Interrupt Code

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

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

TaskHandle_t ISR = NULL;

void IRAM_ATTR button_isr_handler(void *arg){
  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);
    printf("Button pressed!\n");
  }
}

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);

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

How the Code Works?

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 and create tasks.

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

Next, we will define ESP_INR_FAG_DEFAULT and set it to zero . This will be used when we will install the interrupt.

#define ESP_INR_FLAG_DEFAULT 0

We have connected the LED at GPIO27. Therefore, we will define a variable called ‘LED_PIN’ that will hold the GPIO pin 27. This will be used later on in the code to control the digital output.

We have connected the push button at GPIO33. Therefore, we will define a variable called ‘PUSH_BUTTON_PIN’ that will hold the GPIO pin 33. This will be used later on in the code to set up the interrupt.

#define LED_PIN  27
#define PUSH_BUTTON_PIN  33

The task handle will be defined as a global variable outside the main function. We have set it as NULL.

TaskHandle_t ISR = NULL;

Inside the interrupt_task() function, we first create a bool variable called ‘led_status’ and set it to false. Then using a continuous while loop we first suspend the current task so that it runs once, toggle the led_status, set the LED_PIN to the state held in the variable ‘led_status’ and print ‘Button pressed!’ in the serial terminal.

void interrupt_task(void *arg){
  bool led_status = false;
  while(1){
    vTaskSuspend(NULL);
    led_status = !led_status;
    gpio_set_level(LED_PIN, led_status);
    printf("Button pressed!\n");
  }
}

The button_isr_handler() will resume the interrupt_task from this point.

void IRAM_ATTR button_isr_handler(void *arg){
  xTaskResumeFromISR(ISR);
}

app_main()

Inside the main function we will first configure the PUSH_BUTTON_PIN and the LED_PIN as GPIO pin by using the function, gpio_pad_select_gpio() and specify the pin as a parameter inside it.

gpio_pad_select_gpio(PUSH_BUTTON_PIN);
gpio_pad_select_gpio(LED_PIN);

Then, the next step is to configure the LED_PIN as an output and PUSH_BUTTON_PIN as an input. We will set the direction of the pin as an input or output using the gpio_set_direction() function. This function takes in two arguments. The first argument is the GPIO pin and the second argument is the mode (input or output) we want to set the pin in. In our case, we want to set the LED_PIN as an output pin and the PUSH_BUTTON_PIN as an output pin.

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

The next step is to set the interrupt on the push button pin on the positive edge. As we are using a pull-down resistor with the button, hence when it will be pressed, it will be in high state. When it will be released it will be in a low state. When the state goes from low to high then the interrupt will occur. The following function sets interrupt for the PUSH_BUTTON_PIN on the positive (rising) edge:

gpio_set_intr_type(PUSH_BUTTON_PIN, GPIO_INTR_POSEDGE);

Then we will install the ISR service with the default configuration.

 gpio_install_isr_service(ESP_INR_FLAG_DEFAULT);

Next we attach the interrupt service routine (ISR). Here we are adding the interrupt handler.

gpio_isr_handler_add(PUSH_BUTTON_PIN, button_isr_handler, NULL);

Finally, we create the interrupt_task, using the function xTaskCreate().

xTaskCreate(interrupt_task, "interrupt_task", 4096, NULL, 10, &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, press the push button to turn the LED ON. Now press the push button again, to turn it OFF. Whenever the state of the push button goes from HIGH to LOW, the LED toggles.

Video Demo:

ESP32 FreeRTOS Interrupt Demo

Next FeeRTOS tutorials to read:

You may also like to read:

7 thoughts on “ESP32 ESP-IDF FreeRTOS Tutorial: Learn to Create Tasks”

  1. This is a great demo of FreeRTOS task creation, handling, etc. I learned a LOT!

    Thank you!

    It would be helpful to see some of what `(void *arg)` can be used for.

    Also, I found one place where the code is different than what is being described, and it might confuse people: In one of the void DemoTask definitions, there is a `if (count == 3)` when, at that point, it should be 5.

    Reply
    • Thank you for pointing out a typo, we have fixed it. The (void *arg)` is used when we want to pass some data to a task when we create it.

      In FreeRTOS, you can pass data to a task during task creation by using the arg argument in the xTaskCreate function. This argument is of type void * and can be used to pass a pointer to any data structure you want to pass to the task.

      int taskData = 42;
      xTaskCreate(vTaskFunction, “Task”, configMINIMAL_STACK_SIZE, &taskData, 1, NULL);

      Reply
      • Example code:

        #include "FreeRTOS.h"
        #include "task.h"

        void vTaskFunction(void *pvParameter)
        {
        int *taskData = (int *)pvParameter;
        // Use the data passed in pvParameter
        // ...
        vTaskDelete(NULL);
        }

        int main(void)
        {
        int taskData = 42;
        xTaskCreate(vTaskFunction, "Task", configMINIMAL_STACK_SIZE, &taskData, 1, NULL);
        // Start the FreeRTOS scheduler
        vTaskStartScheduler();
        return 0;
        }

        Reply
  2. Ive noticed that using 1024 bytes as the Stack Depth in the xTaskCreate crashes the chip. Why is that and why do we use 4096?

    Reply
  3. I’ve copied and pasted your code;

    1) Sometimes the led is on when pushing the button and sometimes it’s on when the button is released.

    2) Why the vTaskSuspend(NULL) at the interrupt_task? Removing it causes and infinite loop, turning the led on/off. Wasn’t it supposed to execute the code only when the interrupt occurred?

    Thanks,

    Edevald.

    Reply
  4. Hi,

    great tutorial. I learned a lot about tasks. Thank you so much.
    I found a copy&paste typo in:
    ESP32 ESP-IDF FreeRTOS Suspend & Resume Task Example.

    There should be a vTaskResume(); function, but instead there is another vTaskDelete().
    I marked the line in the code example:

    void Demo_Task(void *arg)
    {
    int count = 0;
    while(1){
    count++;
    printf(“Demo_Task printing..\n”);
    vTaskDelay(1000/ portTICK_RATE_MS);
    if (count == 5)
    {
    vTaskSuspend(myTaskHandle2);
    printf(“Demo_Task2 is suspended!\n”);
    }
    if (count == 8)
    {
    vTaskDelete(myTaskHandle2); <–This SHOULD BE vTaskResume();
    printf("Demo_Task2 is resumed!\n");
    }
    if (count == 10)
    {
    vTaskDelete(myTaskHandle2);
    printf("Demo_Task2 is deleted!\n");
    }
    }
    }

    Greatings

    Viktor

    Reply

Leave a Comment