ESP32 - using 2nd core

I would like to use the 2nd core on my ESP32.

I wrote the following sketch to try and confirm my understanding of how multitasking is handled. So 2 tasks, one on each core.

TaskHandle_t Core0Task;
TaskHandle_t Core1Task;


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

  // Set up Core 0 task handler
  xTaskCreatePinnedToCore(
    codeForCore0Task,
    "Core 0 task",
    10000,
    NULL,
    1,
    &Core0Task,
    0);


  // Set up Core 1 task handler
  xTaskCreatePinnedToCore(
    codeForCore1Task,
    "Core 1 task",
    10000,
    NULL,
    1,
    &Core1Task,
    1);
}


void loop()
{
  Serial.print("Main loop on core ");
  Serial.println(xPortGetCoreID());
  delay(100);
}


void codeForCore0Task(void *parameter)
{
  for (;;)
  {
    Serial.print("Task 0 loop on core ");
    Serial.println(xPortGetCoreID());
    delay(100);
  }
}


void codeForCore1Task(void *parameter)
{
  for (;;)
  {
    Serial.print("Task 1 loop on core ");
    Serial.println(xPortGetCoreID());
    delay(100);
  }
}

Here is the output...

12:12:56.921 -> Task 0 loop on core 0
12:12:56.921 -> Task 1 loop on core 1
12:12:56.921 -> Main loop on core 1
12:12:57.026 -> Task 0 loop on core 0
12:12:57.026 -> Main loop on core 1
12:12:57.026 -> Task 1 loop on core 1
12:12:57.128 -> Task 0 loop on core 0
12:12:57.128 -> Task 1 loop on core 1
12:12:57.128 -> Main loop on core 1
12:12:57.233 -> Task 0 loop on core 0
12:12:57.233 -> Main loop on core 1
12:12:57.233 -> Task 1 loop on core 1

What I don't quite get is - the main loop, and the loop within task 1, seem to be running in parallel.

Can someone explain how this works - it appears core 1 is somehow jumping between these 2 endless loops?

1 Like

loop() is running on core1

Running more than one task on a single core is multitasking or multi-threading. Running task on differing cores is multiprocessing.

The issue with using code in loop() under freeRTOS is that when loop() runs if there is no code in loop() housekeeping chores are ran, like memory cleanup. If loop() has code that code may run instead. As the programs gets more complicated loop() has the lowest priority. Any code in loop() ins NOT guaranteed to run under freeRTOS.

ESP32 Arduino core from here:
https://github.com/espressif/arduino-esp32

by default runs the RF/radio/protocols on CORE0 while the Arduino code runs on CORE1. For most scenarios, this scheme works reasonably well.

I reworked Neil Kolban's example a few years ago for an article I wrote, I think it simplifies things la bit by using an Arduino multi-tab example:

/*
   This sketch runs the same load on both cores of the ESP32
   ArduinoIDE 1.8.5
   Linux Mint 18.3
    Sketch uses 162569 bytes (12%) of program storage space. Maximum is 1310720 bytes.
    Global variables use 11068 bytes (3%) of dynamic memory, leaving 283844 bytes for local variables. Maximum is 294912 bytes.
*/

#include <Streaming.h>  // Ref: http://arduiniana.org/libraries/streaming/
#include "Workload.h"   // a tab in this sketch
#include "Task1.h"      // ditto
#include "Task2.h"      // ditto

TaskHandle_t TaskA, TaskB;

void setup() {
  Serial.begin(115200);
  delay(500);  // small delay

  // Ref: http://esp32.info/docs/esp_idf/html/db/da4/task_8h.html#a25b035ac6b7809ff16c828be270e1431

  xTaskCreatePinnedToCore(
    Task1,       /* pvTaskCode */
    "Workload1", /* pcName */
    1000,        /* usStackDepth */
    NULL,        /* pvParameters */
    1,           /* uxPriority */
    &TaskA,      /* pxCreatedTask */
    0);          /* xCoreID */

  xTaskCreatePinnedToCore(
    Task2,
    "Workload2",
    1000,
    NULL,
    1,
    &TaskB,
    1);
}

void loop() {
  // This task will run in the ESP32 Arduino default context
  unsigned long start = millis();
  Serial << "Task 0 complete running on Core " << (xPortGetCoreID()) << " Time = " << (millis() - start) << " mS" << endl;
  ;
  delay(10);
}

DualCore.zip (2.2 KB)

It depends on the implementation of delay(). In an RTOS a task should release its core while waiting, so that your example would run on a single core controller without blocking each other.

How will it look if you replace the delay() by a loop waiting for the time interval to elapse?

Like this ?

TaskHandle_t Core0Task;
TaskHandle_t Core1Task;


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

  // Set up Core 0 task handler
  xTaskCreatePinnedToCore(
    codeForCore0Task,
    "Core 0 task",
    10000,
    NULL,
    1,
    &Core0Task,
    0);


  // Set up Core 1 task handler
  xTaskCreatePinnedToCore(
    codeForCore1Task,
    "Core 1 task",
    10000,
    NULL,
    1,
    &Core1Task,
    1);
}


void loop()
{
  Serial.print("Main loop on core ");
  Serial.println(xPortGetCoreID());
  unsigned long start = millis();
  while (millis() - start < 100);
  
}


void codeForCore0Task(void *parameter)
{
  for (;;)
  {
    Serial.print("Task 0 loop on core ");
    Serial.println(xPortGetCoreID());
    unsigned long start = millis();
    while (millis() - start < 100);
  }
}


void codeForCore1Task(void *parameter)
{
  for (;;)
  {
    Serial.print("Task 1 loop on core ");
    Serial.println(xPortGetCoreID());
    unsigned long start = millis();
    while (millis() - start < 100);
  }
}

Results were similar...

22:51:23.912 -> Task 0 loop on core 0
22:51:23.912 -> Main loop on core 1
22:51:23.912 -> Task 1 loop on core 1
22:51:24.016 -> Task 0 loop on core 0
22:51:24.016 -> Main loop on core 1
22:51:24.016 -> Task 1 loop on core 1
22:51:24.088 -> Task 0 loop on core 0
22:51:24.123 -> Main loop on core 1
22:51:24.123 -> Task 1 loop on core 1
22:51:24.191 -> Task 0 loop on core 0
22:51:24.191 -> Main loop on core 1
22:51:24.191 -> Task 1 loop on core 1

And then every 6 seconds or so it crashes with...

22:51:30.865 -> E (11876) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
22:51:30.865 -> E (11876) task_wdt:  - IDLE0 (CPU 0)
22:51:30.865 -> E (11876) task_wdt: Tasks currently running:
22:51:30.865 -> E (11876) task_wdt: CPU 0: Core 0 task
22:51:30.865 -> E (11876) task_wdt: CPU 1: Core 1 task
22:51:30.865 -> E (11876) task_wdt: Aborting.
22:51:30.865 -> abort() was called at PC 0x400e111f on core 0

This indicates that your tasks now really leave no room for lower priority tasks, so that the IDLE task could not feed the watchdog in time.

And putting code in loop(), a poor coding practice, when using freeRTOS in the Arduino IDE will cause issues. Code in loop() is not guaranteed to run under freeRTOS.

Ok so should loop just be left completely empty when using freeRTOS?

I've also done this:

void loop() {
  vTaskDelete(nullptr);
}

Can you explain the logic?

If you're not going to have code in the loop() function anyway, there's no point in allowing the task that calls the loop() function to continue running.

Yes.

when freeRTOS runs "loop()" and loop() does not have any code, freeRTOS will do house keeping chores.

Which can cause issues. When vTaskDelete(null) is ran freeRTOS removes the calling task. One might see where if loop() is removed as a task but then loop() is called there would be an issue.

Ok thanks. I'll opt for an empty loop().

I doubt it. What "issue' do you imagine?