Dans ce tutoriel nous allons découvrir FreeRTOS sur un ESP32 en utilisant le framework officiel d’Espressif ESP-IDF (IoT Development Framework) qui est basé sur le noyau FreeRTOS.
FreeRTOS (Wikipedia) est un système d’exploitation temps réel open source pour microcontrôleurs. Un système d’exploitation permet de gérer de manière abstraite les ressources matérielles (CPU, mémoire, Entrées/Sorties…). Un système temps réel est un système qui respecte les contraintes de temps en délivrant les résultats d’un process dans des délais imposés. Par exemple le temps maximum entre un stimulus d’entrée et une réponse de sortie est précisément déterminé. En général un système d’exploitation temps réel est multitâches, c’est à dire qu’il permet d’exécuter plusieurs tâches (processus informatique) de façon simultanée.
La documentation du framework ESP-IDF est disponible sur le site internet d’Espressif : https://docs.espressif.com/projects/esp-idf/en/stable/esp32/index.html
Création de notre premier projet
Pour programmer l’ESP32 nous utiliserons l’éditeur Visual Studio Code et la plateforme PlatformIO.
Installez Visual Studio Code et ajoutez l’extension PlatformIO IDE.
Créez un nouveau projet PlatformIO en sélectionnant votre carte ESP32 (j’utilise la carte uPesy ESP32 Wrover DevKit dans ce tutoriel) et le Framework Espidf :
Le fichier de configuration du projet PlatformIO est crée automatiquement, nous y ajoutons uniquement la vitesse du moniteur série afin de pouvoir afficher des traces sur le terminal de Visual Studio Code.
Notre premier programme affiche les informations du microcontrôleur sur le moniteur série, en bouclant à l’infini avec une attente de 1 seconde entre chaque boucle.
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
const char *model_info(esp_chip_model_t model)
{
switch (model)
{
case CHIP_ESP32:
return "ESP32";
case CHIP_ESP32S2:
return "ESP32S2";
case CHIP_ESP32S3:
return "ESP32S3";
case CHIP_ESP32C3:
return "ESP32C3";
case CHIP_ESP32H2:
return "ESP32H2";
case CHIP_ESP32C2:
return "ESP32C2";
default:
return "Unknown";
}
}
void print_chip_info()
{
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
printf("Chip model %s with %d CPU core(s), WiFi%s%s, ",
model_info(chip_info.model),
chip_info.cores,
(chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : "");
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d\n", major_rev, minor_rev);
}
void app_main()
{
for (;;)
{
print_chip_info();
vTaskDelay(1000 / portTICK_PERIOD_MS);
};
}
La fonction vTaskDelay offerte par FreeRTOS est décrite dans la documentation en ligne disponible sur le site de l’OS https://www.freertos.org/a00127.html. Cette fonction spécifie le nombre de top d’horloge (Ticks) pendant lequel la tâche attend, ce nombre est passé comme paramètre. Si on veut comptabiliser le temps d’attente en ms, il suffit de diviser le nombre de Ticks par la constance portTICK_PERIOD_MS.
Une fois le code ci-dessus copié dans le fichier main.c, vous pouvez le téléverser sur votre ESP32 et voir les traces s’afficher dans le moniteur série.
Multitâche
Comme je l’ai indiqué plus haut, FreeRTOS est un OS multitâche. Il est donc en mesure d’exécuter simultanément plusieurs tâches. Nous allons créer un programme dont la fonction principale app_main va créer 2 tâches. Nous aurons ainsi 3 tâches qui vont s’exécuter simultanément : la tâche principales dans laquelle la fonction principale app_main est exécutée et les 2 autres tâches Task1 et Task2 créées par app_main.
Pour créer les tâches nous allons utiliser la fonction xTaskCreate en lui passant en paramètre la fonction d’entrée de la tâche ainsi que d’autres arguments détaillés dans le code.
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
const UBaseType_t taskPriority = 1;
void vTask1(void *pvParameters)
{
for (;;)
{
// Display the core on which the task is running
printf("Task1 is running on core %d\n", xPortGetCoreID());
// Wait 3 seconds
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
void vTask2(void *pvParameters)
{
for (;;)
{
// Display the core on which the task is running
printf("Task2 is running on core %d\n", xPortGetCoreID());
// Wait 2 seconds
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
}
void app_main()
{
xTaskCreate(vTask1, // Entry function of the task
"Task1", // Name of the task
10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)
NULL, // No parameter passed to the task
taskPriority, // Priority of the task
NULL); // No handle
xTaskCreate(vTask2, // Entry function of the task
"Task2", // Name of the task
10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)
NULL, // No parameter passed to the task
taskPriority, // Priority of the task
NULL); // No handle
for (;;)
{
// Display the core on which the main function is running
printf("app_main() is running on core %d\n", xPortGetCoreID());
// Wait 1 seconds
vTaskDelay(1000 / portTICK_PERIOD_MS);
};
}
Dans notre exemple, chaque tâche affiche le numéro du cœur du microcontrôleur sur lequel elle s’exécute. Car nous allons le voir juste après, l’ESP32 Wrover est un dual-core. Il sera donc possible de faire en sorte que 2 tâches puissent s’exécuter sur 2 cœurs différents.
Nous avons positionné des délais d’attente différents dans chaque tâche. Aussi le moniteur série va afficher successivement les traces de chaque tâche (toutes les secondes pour le programme principale, toutes les 4 secondes pour Task1 et toutes les 2 secondes pour la Task2) :
app_main() is running on core 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 0
app_main() is running on core 0
app_main() is running on core 0
Task2 is running on core 0
app_main() is running on core 0
Nous voyons que toutes les tâches s’exécutent sur le cœur 0 et que le cœur 1 n’est pas utilisé. Pour faire en sorte qu’une tâche s’exécute sur un cœur spécifique, la fonction du framework ESP-IDF xTaskCreatePinnedToCore peut être utilisée.
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
const UBaseType_t taskPriority = 1;
void vTask1(void *pvParameters)
{
for (;;)
{
// Display the core on which the task is running
printf("Task1 is running on core %d\n", xPortGetCoreID());
// Wait 2 seconds
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
}
void vTask2(void *pvParameters)
{
for (;;)
{
// Display the core on which the task is running
printf("Task2 is running on core %d\n", xPortGetCoreID());
// Wait 2 seconds
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
}
void app_main()
{
TaskHandle_t xHandle1 = NULL;
TaskHandle_t xHandle2 = NULL;
xTaskCreatePinnedToCore(vTask1, // Entry function of the task
"Task1", // Name of the task
10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)
NULL, // No parameter passed to the task
taskPriority, // Priority of the task
&xHandle1, // Handle to the created task
0); // Task must be executed on core 0
xTaskCreatePinnedToCore(vTask2, // Entry function of the task
"Task2", // Name of the task
10000, // The number of words to allocate for use as the task's stack (arbitrary size enough for this task)
NULL, // No parameter passed to the task
taskPriority, // Priority of the task
&xHandle2, // Handle to the created task
1); // Task must be executed on core 1
for (;;)
{
// Display the core on which the main function is running
printf("app_main() is running on core %d\n", xPortGetCoreID());
// Wait 1 seconds
vTaskDelay(1000 / portTICK_PERIOD_MS);
};
}
On voit bien dans le moniteur série que la tâche principale (fonction app_main) et Task1 s’exécutent sur le cœur 0 tandis que Task2 s’exécute sur le cœur 1 :
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 1
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 1
app_main() is running on core 0
app_main() is running on core 0
Task1 is running on core 0
Task2 is running on core 1
Attention, la fonction xTaskCreatePinnedToCore est spécifique au framework ESP-IDF. Il est préférable de laisser le noyau choisir sur quel cœur tourne chaque tâche. Il est en effet par exemple inutile de faire fonctionner les 2 cœurs alors qu’un seul cœur est suffisant pour faire tourner votre application. Je vous recommande la lecture du chapitre Symmetric Multiprocessing (SMP) with FreeRTOS
Les timers
Une autre fonctionnalité intéressante des OS temps réel est l’utilisation des timers. Il est en effet possible de faire exécuter une fonction au bout d’un temps défini. La fonction appelée à l’échéance du timer est nomée callback.
La fonction xTimerCreate permet de créer un timer en donnant en paramètre la période, la callback et en indiquant si le timer est de type “one-shot” ou répétitif :
- Un timer “one-shot” n’exécutera sa callback qu’une seule fois. Il peut être redémarré manuellement, mais il ne redémarrera pas automatiquement.
- Une fois démarré un timer “auto-reload” redémarrera automatiquement après chaque exécution de sa callback, entraînant son exécution périodique.
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
void autoReloadTimerCallback(TimerHandle_t xTimer)
{
printf("Timer callback\n");
}
void app_main()
{
TimerHandle_t xAutoReloadTimer;
xAutoReloadTimer = xTimerCreate("AutoReloadTimer", // Name of the timer
pdMS_TO_TICKS(500), // The period of the timer specified in ticks (500)
pdTRUE, // The timer will auto-reload when it expires
0, // Identifier of the timer
autoReloadTimerCallback); // Callback function
xTimerStart(xAutoReloadTimer, 0);
for (;;)
{
// Display the core on which the main function is running
printf("app_main() is running on core %d\n", xPortGetCoreID());
// Wait 1 seconds
vTaskDelay(1000 / portTICK_PERIOD_MS);
};
}
Nous observons bien dans le moniteur série que la callback est appelée à l’expiration du timer toutes les 500ms :
app_main() is running on core 0
Timer callback
Timer callback
app_main() is running on core 0
Timer callback
Timer callback
app_main() is running on core 0
Timer callback
Timer callback
Les interruptions
Une interruption est une suspension temporaire de l’exécution d’un programme par le microcontrôleur afin d’exécuter un programme prioritaire (appelé service d’interruption).
Dans l’exemple suivant nous allons configurer la broche GPIO32 de notre ESP32 en tant qu’ entrée et compter combien de fois cette entrée est passée de l’état bas à l’état haut lors de la dernière seconde. Nous allons configurer la broche GPIO32 afin qu’une routine d’interruption soit appelée lorsqu’elle passe de l’état bas à l’état haut (front montant = GPIO_INTR_POSEDGE).
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#define INPUT_PIN GPIO_NUM_32
uint32_t cpt = 0;
static void IRAM_ATTR gpio_interrupt_handler(void *args)
{
// Interrupt is raised, increase the counter
cpt++;
}
void app_main()
{
// Configure INPUT_PIN as input
gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
// Enable the pull-down for INPUT_PIN
gpio_pulldown_en(INPUT_PIN);
// Disable the pull-up for INPUT_PIN
gpio_pullup_dis(INPUT_PIN);
// An interrupt will be triggered when the the state of the INPUT_PIN changes from LOW to HIGH
gpio_set_intr_type(INPUT_PIN, GPIO_INTR_POSEDGE);
// Install the GPIO ISR service, which allows per-pin GPIO interrupt handlers
gpio_install_isr_service(0);
// Configure gpio_interrupt_handler function has ISR handler for INPUT_PIN
gpio_isr_handler_add(INPUT_PIN, gpio_interrupt_handler, (void *)INPUT_PIN);
for (;;)
{
// Display the number of time the interrupt was called during last second
printf("Counter = %lu\n", cpt);
// Reset the counter
cpt = 0;
// Wait 1 seconds
vTaskDelay(1000 / portTICK_PERIOD_MS);
};
}
Lorsque nous relions la broche 32 à la broche 3.3V pour la faire passer à son état haut, nous observons dans le moniteur série que le compteur indique qu’il y a eu de nombreuses interruption. C’est normal car nous relions le fil à la main, ce qui crée de nombreux passages de l’état bas à l’état haut.
Counter = 0
Counter = 0
Counter = 0
Counter = 7
Counter = 40
Counter = 0
L’objectif de ce tutoriel n’est pas d’approfondir le concept de système d’exploitation temps réel, mais uniquement de vous donner envie d’aller plus loins avec ESP-IDF et FreeRTOS avec un ESP32. Bonne découverte !
Bonjour, le code de base ne fonctionne pas sur mon heltec wifi kit 32, le terminal ne m’affiche que des caractères illisibles, pourtant je suis sur d’avoir sélectionné la bonne carte et d’avoir mis le monitor speed.