ESP32: Running code on a specific core

The objective of this post is to explain how to run code on a specific core of the ESP32, using the Arduino environment support.


Introduction

The objective of this post is to explain how to run code on a specific core of the ESP32, using the Arduino environment support. More precisely, we will analyze how to create a FreeRTOS task pinned to a specific core.

One of the many interesting characteristics of the ESP32 is that it has two Tensilica LX6 cores [1], which we can take advantage of to execute our code with greater performance and more versatility.

On this previous post, we already covered a way of knowing in which core a task is executing, by using the xPortGetCoreID function. We also saw that when we create a task with the xTaskCreate function, it may be assigned to any of the two ESP32 cores [2][3].

So, to pin the execution of a task to a specific ESP32 core, we need to execute the xTaskCreatePinnedToCore function, which receives as one of its arguments the core ID where to execute. We will analise it in more detail in the coding sections.

For now, we will give a very simple example on how to use the previously mentioned function to launch a very basic task on the two different cores of the ESP32.


Confirming the multi core execution

Since we will start playing with ESP32 multi core code execution, we need to have a way to confirm that the APIs are correctly working and we are getting the desired results.

So, we will first go back to two previous posts to gather some evidences and designing a use case suitable to confirm that everything is fine. So, from the post where we analysed how to get a task’s priority, we got that both the setup and the main loop functions execute with a priority of 1. We didn’t not analyse it further, so we don’t know yet if the functions are two separated FreeRTOS tasks or not, but this is irrelevant for now.

From the post about getting a task’s execution core, we also saw that both the setup and the main loop functions execute on core 1. Also, we can se here that the execution is pinned, so it’s not expected that the core will change during execution of the program.

Other important aspect is that, on FreeRTOS, tasks have an assigned priority which the scheduler uses to decide which task will run [4]. High priority tasks ready to run will have preference over lower priority tasks [4], which means that as long as a higher priority task can run, a lower priority task will not have the CPU.

So, if we take this to an extreme situation where the high priority task never leaves the CPU, a starvation situation will occur, meaning that the lower priority task will never gain access to the CPU and progress on the execution of its code [5].

But if there are two cores available, both of the tasks should be able to run, even if the higher priority task never blocks its execution, as long as they are assigned to the different cores.

For implementation purposes, we need to take in consideration that FreeRTOS priorities are assigned from 0 to N, where lower numbers correspond to lower priorities [4]. So, the lowest priority is 0.

With this in mind, the application we will design will allow us to launch a FreeRTOS task on core 0 or 1, in the setup function. This task will have priority 0, which is lower than both the setup and main loop functions. This means that this FreeRTOS task we will create will have lower priority.

Then, on the Arduino main loop function, we will do an infinite loop, without any type of delay or yielding mechanism, in order for it to maintain the CPU. Remember that the Arduino main loop runs on core 1.

Finally, we will analise if the task will starve when executing on ESP32 core 1 and will be able to run when executing on core 0, which is the expected behavior.


The setup function

First of all, we will declare a global variable that will contain the number of the core where the FreeRTOS task we will launch will be pinned. This ensures that we can easily change it when testing the code.

Note that we will run the code twice, assigning both the values 0 and 1 to this variable. We could have created a more sophisticated approach that receives as a serial input the core where the task should be pinned, but I wanted to keep the code as simple as possible to focus on the multi core functionality.

static int taskCore = 0;

Then, we will go for the setup function. In the beginning, we open a serial connection with a baud rate of 112500, in order to send some informative messages to the Arduino IDE serial monitor.

After that we will print the number of the core we are testing, which is stored on the previously declared global variable.

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

Serial.print("Starting to create task on core ");
Serial.println(taskCore);

Now we will launch the FreeRTOS task, assigning it to a specific core of the ESP32. As mentioned before, we will use the xTaskCreatePinnedToCore function. This function takes exactly the same arguments of the xTaskCreate and an additional argument at the end to specify the core where the task should run. If you need help with the process of creating a task and the inputs needed, please check this previous post that contains a detailed explanation on how to use the xTaskCreate function.

We will implement the task in a function called coreTask. We will check the code for it latter. Also, as stated in the previous section, we should attribute priority 0 to the task, so it is lower than both the setup and the main loop. We don’t need to worry about passing any input parameters or storing the task handle.

After this call, we will just print another informative message to the serial port indicating the task is created.

xTaskCreatePinnedToCore(
                    coreTask,   /* Function to implement the task */
                    "coreTask", /* Name of the task */
                    10000,      /* Stack size in words */
                    NULL,       /* Task input parameter */
                    0,          /* Priority of the task */
                    NULL,       /* Task handle. */
                    taskCore);  /* Core where the task should run */

Serial.println("Task created...");

With this we finish the setup function.


The main loop

Now we will design the main loop, which will be very simple. We start by printing a message to the serial port indicating we are starting the main loop, so we now in which point of execution of the code we are.

After that we will do an infinite while loop, without any code inside it. It’s crucial that we don’t put any type of yielding or delaying function inside it so the best approach is to leave it empty. This way, we now the scheduler will maintain the CPU execution assigned to it.

void loop() {

  Serial.println("Starting main loop...");
  while (true){}

}


The task function

The task function will also be very simple. We will just print a message indicating the core assigned to it, obtained with the xPortGetCoreID. Naturally, this must correspond to the core specified in the global variable.

We will first build the string with this information.

String taskMessage = "Task running on core ";
taskMessage = taskMessage + xPortGetCoreID();

Then we do an infinite loop where we print this message, with a small delay in each iteration. In this case, there is no problem in putting a delay in this task since it has lower priority. So, it won’t affect our application and the purpose of the demonstration.

while(true){
        Serial.println(taskMessage);
        delay(1000);
}


The full code

To make it easier to test, the complete code is shown bellow. You can copy and paste it and it should compile and execute fine. We start with the global variable assigned to core 1.

static int taskCore = 1;

void coreTask( void * pvParameters ){

    String taskMessage = "Task running on core ";
    taskMessage = taskMessage + xPortGetCoreID();

    while(true){
        Serial.println(taskMessage);
        delay(1000);
    }

}

void setup() {

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

  Serial.print("Starting to create task on core ");
  Serial.println(taskCore);

  xTaskCreatePinnedToCore(
                    coreTask,   /* Function to implement the task */
                    "coreTask", /* Name of the task */
                    10000,      /* Stack size in words */
                    NULL,       /* Task input parameter */
                    0,          /* Priority of the task */
                    NULL,       /* Task handle. */
                    taskCore);  /* Core where the task should run */

  Serial.println("Task created...");
}

void loop() {

  Serial.println("Starting main loop...");
  while (true){}

}


Testing the code

To test the code, just upload it to your ESP32 board, using the Arduino IDE. We will start by testing the creation of the task on core 1. You should get a result similar to figure 1.

ESP32 running code on core 1

Figure 1 – Output of the program, when assigning the task to the core 1 of ESP32.

As can be seen, there is no output from the FreeRTOS task launched on the setup function. Since we pinned it to core 1 and assigned it a priority of 0, the Arduino main loop will always be executing because it has higher priority (equal to 1) and also runs on core 1.

Now, change the global variable to 0 and re-upload the code. You should now get a different result, as shown in figure 2.

ESP32 running code on core 0

Figure 2 – Output of the program, when assigning the task to the core 0 of ESP32.

In this case, we get the prints from the task launched in the setup function, which is running on core 0. This fact is coherent with the information obtained with the xPortGetCoreID function and printed to the serial monitor.

In this case, although the main loop is executing and locking the resources of core 1 and has higher priority, the new task can execute on core 0 because it is free. So, there is no starvation and everything works fine.


Final notes

As we can see from the results of this tutorial, the execution of code in both cores of the ESP32 is working. Nevertheless, at the time of writing, we need to use FreeRTOS and ESP32 IDF primitives in order to make it work, although we are using the Arduino environment.

The possibility of executing code in both cores opens a wide range of possibilites,  including a lot of optimization of the execution of code. Hopefully we will start seeing more applications exploring the full potencial of the ESP32 dual core execution.


Related posts

 

Related content


References

[1] https://espressif.com/en/products/hardware/esp32/overview

[2] https://www.esp32.com/viewtopic.php?t=764

[3] http://esp32.info/docs/esp_idf/html/dd/d3c/group__xTaskCreate.html

[4] http://www.freertos.org/RTOS-task-priority.html

[5] http://www.math.uni-hamburg.de/doc/java/tutorial/essential/threads/deadlock.html

26 thoughts on “ESP32: Running code on a specific core”

  1. Pingback: ESP32 | Andreas' Blog

  2. Pingback: ESP32 | Andreas' Blog

    1. Hi! The best place to find information about FreeRTOS is on their API:
      http://www.freertos.org/a00106.html

      You can also get more information on Espressif’s IDF documentation:
      http://esp-idf.readthedocs.io/en/latest/api-guides/freertos-smp.html#tasks-and-task-creation

      I think the xTaskCreatePinnedToCore function is ESP specific, or at least I haven’t yet found any reference to it on the FreeRTOS website.

      So, to sum up, I usually go to the official FreeRTOS documentation and then complement with a search to that same function on the IDF documentation, to check if it has some different behavior on the ESP32.

      What do you mean with memory blocking? Like if memory is shared amongst tasks running on different cores?

      Best regards,
      Nuno Santos

    1. Hi! The best place to find information about FreeRTOS is on their API:
      http://www.freertos.org/a00106.html
      You can also get more information on Espressif’s IDF documentation:
      http://esp-idf.readthedocs.io/en/latest/api-guides/freertos-smp.html#tasks-and-task-creation
      I think the xTaskCreatePinnedToCore function is ESP specific, or at least I haven’t yet found any reference to it on the FreeRTOS website.
      So, to sum up, I usually go to the official FreeRTOS documentation and then complement with a search to that same function on the IDF documentation, to check if it has some different behavior on the ESP32.
      What do you mean with memory blocking? Like if memory is shared amongst tasks running on different cores?
      Best regards,
      Nuno Santos

  3. Pingback: ESP32 Arduino: Using the pthreads library | techtutorialsx

  4. Pingback: ESP32 Arduino: Using the pthreads library | techtutorialsx

  5. Hi,
    Is it possible to attach an external interrupt to a specific core. I have two gpio interrupts which interferes with each other. I hope attaching each interrupt to a specific core would prevent interrupt nesting. I am not sure how to do this.

  6. What will happen when we create two tasks with both 0 priority? I tried it but first task only running. Please explain.

  7. Thank you techtutorialsx – I was having trouble getting started with esp32 smt, this helped a lot. (now have pro core blinking blue led, app core blinking yellow led.) Life is good.

  8. How is functions called from a task running on e.g core #1 handled? Are they run on the same core as the caller or is it random where that code is executed?

    Nice airtichel by the way.

Leave a Reply to EricCancel reply

Discover more from techtutorialsx

Subscribe now to keep reading and get access to the full archive.

Continue reading