freertos-esp32

Découvrir FreeRTOS sur un ESP32 avec PlatformIO

4.6
(9)

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.

Installation de 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 :

Création du projet pour l’ESP32 en utilisant le framework ESP-IDF avec PlatformIO

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.

Fichier de configuration du projet PlatformIO pour ESP32 Wrover et framework ESP-IDF

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.

Une fois téléversé dans l’ESP32, le programme affiche les informations du microcontrôleur

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 !

Votre avis compte !

Note moyenne : 4.6 / 5. Nombre de votes : 9

Pas encore de vote pour ce tutoriel

Désolé si cet article ne vous a pas intéressé

Merci de commenter afin que je puisse l’améliorer.

Dites-moi comment améliorer cette page.