ESP32 FreeRTOS, Dual Core Programming, and Multi Tasking
Table of Contents
ESP32 Dual Core Programming:
ESP32 FreeRTOS, Dual Core Programming, and Multi Tasking- ESP32 WiFi + Bluetooth module is a great microcontroller made by Espressif Systems. It has a powerful dual-core Xtensa LX6 microprocessor that runs at a fast 240 MHz.
But the question is, how many of you have actually used the dual cores of the ESP32? Or have done multi-tasking on the ESP32? I am sure 85% of you have not used the dual cores of the ESP32 or done any multi-tasking.
First of all, let me tell you that you should not mix up FreeRTOS, dual-core programming, and Multi tasking.
Although Dual core programming and multi tasking in ESP32 are related to FreeRTOS, but not exactly the same thing.
FreeRTOS is a real-time operating system that helps run multiple tasks at the same time, even when they share the same resources. To be more specific; FreeRTOS manages the tasks, schedules them, and provides mechanisms for inter-task communication and synchronization.
Whereas, Dual-core programming in ESP32 means you can run two different tasks or programs on the two cores of the ESP32 processor (Core 0 and Core 1). This way, each core can do its own job at the same time.
Multi-tasking in ESP32, on the other hand, refers to the ability to run multiple tasks concurrently on a single core, using FreeRTOS or other scheduling mechanisms.
So,
- FreeRTOS is used to manage multiple tasks running on one core.
- Dual-core programming lets you run two different tasks at the same time on the two cores.
- Multi-tasking means you can have several tasks running on each core, managed by FreeRTOS or other tools.
I will not make things too complicated so that you can easily understand everything. Today, we will only cover the dual cores (Core 0 and Core 1). In upcoming articles, we will talk about FreeRTOS and multi-tasking.
So, without any further delay, let’s get started!!!
Amazon Links:
ESP32 WiFi + Bluetooth Module (Recommended)
Disclosure: These are affiliate links. As an Amazon Associate I earn from qualifying purchases.
ESP32 Default Core:
By default, Arduino sketches run on ESP32’s Core 1. We can easily find out which function is running on which core by using the xPortGetCoreID() function. As you can see, I have used this function in both void setup() and void loop().
Check the ESP32 Default Core:
1 2 3 4 5 6 7 8 9 10 11 12 |
void setup() { Serial.begin(115200); Serial.print("setup() is running on core "); Serial.println(xPortGetCoreID()); delay(1000); } void loop() { Serial.print("loop() is running on core "); Serial.println(xPortGetCoreID()); delay(1000); } |
- Upload this program to confirm the default core of the ESP32.
- Open the Serial monitor and select the Baud rate.
If you are not able to see the core information for the setup() function then simply press the Reset button.
You can see both the setup() and loop() functions are running on the ESP32 Core 1.
The ESP32 microcontroller comes with two cores: Core 0 and Core 1. This means you can run two tasks simultaneously on separate cores. This feature is useful when you want to split your workload, such as running time-sensitive tasks on one core while handling less critical tasks on the other.
What is a Task?
A task in the context of programming is a single, independent piece of code that performs a specific function. In FreeRTOS (which is often used for managing tasks on the ESP32), tasks can be thought of as lightweight processes that the operating system schedules and runs. Each task can run on either Core 0 or Core 1.
Single-Core Execution
Let’s start with a basic example where both tasks (blinking an LED and printing a message to the Serial Monitor) run on a single core. Let me tell you, this variant of the ESP32 has its onboard LED connected to GPIO 5.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int myLed = 5; void setup() { // Initialize serial communication Serial.begin(115200); // Initialize LED pin pinMode(myLed, OUTPUT); } void loop() { // Task 1: Blink the LED digitalWrite(myLed, HIGH); delay(500); digitalWrite(myLed, LOW); delay(500); // Task 2: Print message to Serial Monitor Serial.println("Electronic Clinic"); delay(1000); } |
Code Explanation:
In this example, both the LED blinking task and the message printing task are executed on Core 1 by default. The loop() function runs these tasks one after the other, meaning that the LED blinks, and then a message is printed to the Serial Monitor.Let’s upload this program.
Since both tasks are running on the same core, so; that’s why they are executed sequentially. The microcontroller completes the LED blink, and then moves on to printing the message.
Dual-Core Execution:
I modified the program; and now, the LED blinking task runs on one core and the message printing task runs on the other core.
ESP32 Dual Cores Programming:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
TaskHandle_t Task1; TaskHandle_t Task2; int myLed = 5; void setup() { // Initialize serial communication Serial.begin(115200); // Initialize LED pin pinMode(myLed, OUTPUT); // Create two tasks, each assigned to a different core xTaskCreatePinnedToCore( Task1Code, // Task function "Task 1", // Name of the task 2000, // Stack size (in words, not bytes) NULL, // Task input parameter 2, // Priority of the task (higher priority) &Task1, // Task handle 0); // Core to run the task on (Core 0) xTaskCreatePinnedToCore( Task2Code, // Task function "Task 2", // Name of the task 2000, // Stack size (in words, not bytes) NULL, // Task input parameter 2, // Priority of the task (higher priority) &Task2, // Task handle 1); // Core to run the task on (Core 1) } void loop() { // Empty loop as tasks run independently } // Task 1: Blinks the LED (Runs on Core 0) void Task1Code(void * pvParameters) { for (;;) { digitalWrite(myLed, HIGH); delay(500); digitalWrite(myLed, LOW); delay(500); } } // Task 2: Prints message to Serial Monitor (Runs on Core 1) void Task2Code(void * pvParameters) { for (;;) { Serial.println("Electronic Clinic"); delay(1000); // Delay between messages } } |
ESP32 Dual Cores Code Explanation:
1 2 3 |
TaskHandle_t Task1; TaskHandle_t Task2; |
These lines create two variables to keep track of the tasks. Task1 and Task2 will store information about the tasks we create.
1 |
int myLed = 5; |
The ESP32 onboard Led is connected to the gpio 5.
1 2 3 4 5 6 7 8 9 10 11 12 |
void setup() { // Initialize serial communication Serial.begin(115200); // Initialize LED pin pinMode(myLed, OUTPUT); |
The setup() function runs once when the ESP32 starts up. Next, I activated the serial communication and selected 115200 as the baud rate. And I set the LED as output.
Next, I created two tasks, each assigned to a different core.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
xTaskCreatePinnedToCore( Task1Code, // Task function "Task 1", // Name of the task 2000, // Stack size (in words, not bytes) NULL, // Task input parameter 2, // Priority of the task (higher priority) &Task1, // Task handle 0); // Core to run the task on (Core 0) |
xTaskCreatePinnedToCore(…);: This function creates a new task and assigns it to a core.
Task1Code: This is the function that will run as Task 1.
“Task 1”: This is a name for the task.
2000: This is the amount of memory (stack size) Task 1 will use. It is measured in words.
NULL: This means no extra input is passed to Task 1.
2: This sets Task 1’s priority. A higher number means higher priority.
&Task1: This saves information about Task 1 in the Task1 variable.
0: This tells the ESP32 to run Task 1 on Core 0.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
xTaskCreatePinnedToCore( Task2Code, // Task function "Task 2", // Name of the task 2000, // Stack size (in words, not bytes) NULL, // Task input parameter 2, // Priority of the task (higher priority) &Task2, // Task handle 1); // Core to run the task on (Core 1) } |
xTaskCreatePinnedToCore(…);: This function creates another task.
Task2Code: This is the function that will run as Task 2.
“Task 2”: This is a name for the task.
2000: This is the amount of memory (stack size) Task 2 will use.
NULL: This means no extra input is passed to Task 2.
2: This sets Task 2’s priority.
These three lines are exactly the same.
&Task2: This saves information about Task 2 in the Task2 variable.
1: This tells the ESP32 to run Task 2 on Core 1.
1 2 3 4 5 |
void loop() { // Empty loop as tasks run independently } |
The loop() function is empty; because the tasks are running on separate cores.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Task 1: Blinks the LED (Runs on Core 0) void Task1Code(void * pvParameters) { for (;;) { digitalWrite(myLed, HIGH); delay(500); digitalWrite(myLed, LOW); delay(500); } } |
void Task1Code(void * pvParameters) {: This defines what Task 1 does.
for (;;) {: This creates an infinite loop.
digitalWrite(myLed, HIGH);: This turns the LED on.
delay(500);: This waits for 500 milliseconds.
digitalWrite(myLed, LOW);: This turns the LED off.
delay(500);: This waits for another 500 milliseconds.
All these other instructions are used to ON and OFF the Led at 500 millisconds.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Task 2: Prints message to Serial Monitor (Runs on Core 1) void Task2Code(void * pvParameters) { for (;;) { Serial.println("Electronic Clinic"); delay(1000); // Delay between messages } } |
Similarly, task 2 sends the Electronic Clinic message to the Serial monitor at a delay of 1000 milliseconds. Let’s upload this program.
Practical Demonstration:
For the practical demonstration watch the video tutorial given at the end of this article. The LED is blinking on its own, and the message is printing separately. In the previous example, when both tasks were running on the same core, the LED would blink first and then the message would print. This happened because the tasks were executed one after the other. But now, with both tasks running on separate cores, they run independently of each other.
Let’s take a look at another example.
Two LEDs on Core 0 and Core 1:
This time, we will make the two LEDs blink at different speeds. One Led is connected to the ESP32 GPIO 2 and the other LED is connected to the GPIO 4. For the connections you can follow this circuit diagram.
ESP32 Dual Cores Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
// LED pins const int fastBlinkLedPin = 2; const int slowBlinkLedPin = 4; // Task handles TaskHandle_t Task1; TaskHandle_t Task2; void setup() { // Initialize serial communication Serial.begin(115200); pinMode(fastBlinkLedPin, OUTPUT); pinMode(slowBlinkLedPin, OUTPUT); // Create the fast blink LED task on core 0 xTaskCreatePinnedToCore( blinkFastLED, // Task function "Task1", // Name of the task 1000, // Stack size (bytes) NULL, // Parameter to pass to the task 2, // Task priority &Task1, // Task handle 0); // Core to run the task (0 or 1) // Create the slow blink LED task on core 1 xTaskCreatePinnedToCore( blinkSlowLED, // Task function "Task2", // Name of the task 1000, // Stack size (bytes) NULL, // Parameter to pass to the task 2, // Task priority &Task2, // Task handle 1); // Core to run the task (0 or 1) } void loop() { // Do nothing here } // Function to blink LED fast void blinkFastLED(void *pvParameters) { while (true) { digitalWrite(fastBlinkLedPin, HIGH); delay(200); // On for 200ms digitalWrite(fastBlinkLedPin, LOW); delay(200); // Off for 200ms } } // Function to blink LED slow void blinkSlowLED(void *pvParameters) { while (true) { digitalWrite(slowBlinkLedPin, HIGH); delay(1000); // On for 1000ms (1 second) digitalWrite(slowBlinkLedPin, LOW); delay(1000); // Off for 1000ms (1 second) } } |
I have slightly modified the previous code. This time, I am not using the onboard LED; instead, I am using external LEDs that are connected to pins 2 and 4 of the ESP32. The rest of the code is the same. We are just turning one LED ON and OFF every 200 milliseconds and the other LED ON and OFF every 1 second. I have already uploaded this program so let’s go ahead and watch this in action.
Practical Demonstration:
Now you can see even more clearly how we can run two different tasks on two different cores independently. Watch the video tutorial on my YouTube channel “Electronic Clinc”.
Running tasks independently does not mean we cannot link them together. In fact, we can also make the cores communicate with each other. let’s do it.
Inter Task Communication Between two Cores:
For this example, you can see I have also added a potentiometer. Its middle leg is connected to the ESP32 GPIO 36 which may also be labeled as “A0 or VP”. Whereas the two LEDs are still connected to the GPIOs 2 and 4.
For the Inter Task Communication between the two cores “Core 0 and Core 1”. I want the Led1 and Potentiometer to be on Core 1; and the other LED Led 2 on the Core 0.
So, what I want do is, I want to control the Delay time of Led 2 from Core 1. So, we will read the potentiometer on the Core 1 and then send its value to the Core 0 to increase and decrease the delay time of Led 2. Instead of using Potentiometer, you can use any other Analog or digital sensor.
Inter-core communication Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
// LED pins const int fastBlinkLedPin = 2; // LED controlled by potentiometer (Core 0) const int fixedBlinkLedPin = 4; // LED with fixed blinking (Core 1) const int potPin = 36; // A0, VP // Task handles TaskHandle_t Task1; TaskHandle_t Task2; // Queue handle for inter-core communication QueueHandle_t potValueQueue; void setup() { // Initialize serial communication Serial.begin(115200); pinMode(fastBlinkLedPin, OUTPUT); pinMode(fixedBlinkLedPin, OUTPUT); pinMode(potPin, INPUT); // Create a queue capable of holding one integer value potValueQueue = xQueueCreate(1, sizeof(int)); // Create the fast blink LED task on core 0 xTaskCreatePinnedToCore( blinkFastLED, // Task function "Task1", // Name of the task 1000, // Stack size (bytes) NULL, // Parameter to pass to the task 2, // Task priority &Task1, // Task handle 0); // Core to run the task (Core 0) // Create the slow blink LED and potentiometer read task on core 1 xTaskCreatePinnedToCore( readPotAndBlinkLED, // Task function "Task2", // Name of the task 1000, // Stack size (bytes) NULL, // Parameter to pass to the task 2, // Task priority &Task2, // Task handle 1); // Core to run the task (Core 1) } void loop() { // Do nothing here } // Function to blink LED fast based on potentiometer value (Runs on Core 0) void blinkFastLED(void *pvParameters) { int potValue = 200; // Initial delay value while (true) { // Check if a new potentiometer value is available in the queue if (xQueueReceive(potValueQueue, &potValue, 0) == pdPASS) { // Received a new potentiometer value } // Use the latest potentiometer value for delay digitalWrite(fastBlinkLedPin, HIGH); delay(potValue); digitalWrite(fastBlinkLedPin, LOW); delay(potValue); } } // Function to read potentiometer, send value to Core 0, and blink LED (Runs on Core 1) void readPotAndBlinkLED(void *pvParameters) { int potValue; while (true) { // Read potentiometer value and map it to a suitable delay range potValue = map(analogRead(potPin), 0, 4095, 50, 1000); // Map the potentiometer value to 50-1000ms Serial.println(potValue); // Send the potentiometer value to the queue xQueueSend(potValueQueue, &potValue, portMAX_DELAY); // Blink the LED at a fixed rate (1 second on/off) digitalWrite(fixedBlinkLedPin, HIGH); delay(1000); digitalWrite(fixedBlinkLedPin, LOW); delay(1000); } } |
Code Explanation:
1 |
QueueHandle_t potValueQueue; |
This time for inter-core communication, I also defined a Queue Handle which allows Core 1 to send the potentiometer value to core 0.
1 2 3 |
// Create a queue capable of holding one integer value potValueQueue = xQueueCreate(1, sizeof(int)); |
in the setup() function, I also created a queue that can hold one integer value, which will be used to pass the potentiometer value from Core 1 to Core 0.
1 2 3 4 5 |
if (xQueueReceive(potValueQueue, &potValue, 0) == pdPASS) { // Received a new potentiometer value } |
This checks if a new potentiometer value is available in the queue. If a new value is available, it updates the delay time.
1 |
potValue = map(analogRead(potPin), 0, 4095, 50, 1000); |
On core 1 we read the Potentiometer and map its value from 50 to 1000 milliseconds.
1 |
xQueueSend(potValueQueue, &potValue, portMAX_DELAY); |
Using this line of code, we send the Potentiometer value to Core 0.
I have already uploaded this program, so let’s go ahead and watch the inter task communication between two cores on the ESP32 in action.
Practical Demonstration:
The Led 1 and potentiometer are on Core 1, and the other LED is on Core 0. By using the potentiometer, I can control the ON and OFF time of the Led 2 on Core 0.
Whereas, the Led1 keeps turning ON and OFF every 1 second.
By using Core 0 and Core 1, you can make amazing projects. For example, you can use Bluetooth on one core and WiFi on the other. Or you can use Bluetooth for short-range communication on one core and LoRa transceiver modules for long-range communication on the other core. You can use these cores in a myriad of different ways. So, that’s all for now.