As it is known, some of the microcontrollers, in order to increase performance provide more than one core. ESP32 is one of them providing two physical cores. In practice, it means that the program developed can run simultaneously on both cores. Thereby it is possible to optimize some of the tasks in a way that they are not waiting for each other but running in parallel instead. This is the main advantage of parallel programming comparing to a sequential one. However, it requires both dedicated program control structures and hardware support.
At the time while this chapter is being written, the simplest way of developing a parallel code on ESP32 is via using FreeRTOS™ [1], which is a widely used real-time library for different microcontrollers. The RTOS allows using most of the real-time and parallel programming features including semaphores, process assignments to cores and more. The following code chunks explain how to apply the most useful parallel programming features.
Let's start with an example of blinking LED and Text output (based on material found here [2]).
The first task is task1, that simply outputs a string “Hi there!” to default serial port with delay of 100 ms, i.e. 10 times per second:
#include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "driver/gpio.h" #define BLINK_GPIO 13 void task1_SayHi(void * parameters) { while(1) { printf("Hi there!\n"); vTaskDelay(100 / portTICK_RATE_MS); } }
The second task is to bilk a LED with a period of 2 seconds (1 second on, 1 second off):
void task2_BlinkLED(void * parameters) { gpio_pad_select_gpio(BLINK_GPIO); gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT); while(1) { /*Sets the LED low for one second*/ gpio_set_level(BLINK_GPIO, 0); vTaskDelay(1000 / portTICK_RATE_MS); /*Sets the LED high for one second*/ gpio_set_level(BLINK_GPIO, 1); vTaskDelay(1000 / portTICK_RATE_MS); } }
Once both task functions are defined, they can be executed simultaneously:
void app_main() { nvs_flash_init(); xTaskCreate(&task1_SayHi, "task1_SayHi", 2000, NULL, 5, NULL); xTaskCreate(&task2_BlinkLED, "task2_BlinkLED", 2000,NULL,5,NULL ); }
To run the code physically in parallel it is necessary to assign task explicitly to the particular core, which requires a slight modification of the main() function:
void app_main() { nvs_flash_init(); xTaskCreatePinnedToCore(&task1_SayHi, "task1_SayHi", 2000, NULL, 5, NULL,0); xTaskCreatePinnedToCore(&task2_BlinkLED, "task2_BlinkLED", 2000,NULL,5,NULL,1); }
While ESP32 provide two computing nodes, other devices like particular serial port or other peripherals are only single devices. In some cases, it might be needed to access those devices by multiple processes in a way that does not disturb the others. In a terminology of parallel programming, those “single” devices are called resources that need to be shared or simply shared resources. To share a resource it is necessary to have a signal that is available to all processes and that determines if the resource is available or not. Those signals are dedicated data structures and are called - semaphores. Depending on the particular platform they might represent a different data structure to address particular use case. RTOS support three main semaphore types – Binary (True/False), Counting (represents a queue) and Mutex (binary semaphore with priority). More details on each type and use examples might be found here [5]. To explain the concept of resource sharing here a simple binary-semaphore example is provided. Example uses two SayHi tasks to share the same output device:
Since we need to define a semaphore at the beginning a setup function is also needed:
#include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "driver/gpio.h" SemaphoreHandle_t xSemaphore = NULL; void setup() { vSemaphoreCreateBinary( xSemaphore ); }
Now it is possible to define the task functions and modify them in a way they use the same resource
void task1_SayHi(void * parameters) { while(1) { /*check and waits for semaphore to be released for 100 ticks. If the semaphore is available it is taken / blocked */ if( xSemaphoreTake( xSemaphore, ( TickType_t ) 100 ) == pdTRUE ) { printf("TASK1: Hi there!\n"); vTaskDelay(100 / portTICK_RATE_MS); xSemaphoreGive( xSemaphore ); } else { //Does something else in case the semaphore is not available } } } void task2_SayHi(void * parameters) { while(1) { /*check and waits for semaphore to be released for 100 ticks. If the semaphore is available it is taken / blocked */ if( xSemaphoreTake( xSemaphore, ( TickType_t ) 100 ) == pdTRUE ) { printf("TASK2: Hi there!\n"); vTaskDelay(100 / portTICK_RATE_MS); xSemaphoreGive( xSemaphore ); } else { //Does something else in case the semaphore is not available } } }
Now both of the tasks are ready to be executed on the same or different cores as explained previously.