ESP32 FreeRTOS Mutex – Mise en route (IDE Arduino)

ESP32 FreeRTOS Mutex - Mise en route (IDE Arduino)

Dans ce guide, vous apprendrez à utiliser un FreeRTOS Mutex avec l’ESP32 programmé avec l’IDE Arduino. Un Mutex (Mutual Exclusion) est un type spécial de sémaphore binaire qui contrôle l’accès aux ressources partagées entre deux ou plusieurs tâches. Cela garantit qu’une seule tâche à la fois peut accéder à une ressource critique. Nous expliquerons les bases d’un Mutex et vous montrerons un exemple simple de comment les mettre en œuvre.

ESP32 avec FreeRTOS - Premiers pas avec Mutex Arduino IDE Core

Vous êtes nouveau sur FreeRTOS ? Commencez par ce didacticiel : ESP32 avec FreeRTOS (Arduino IDE) – Guide de démarrage : Création de tâches.

Table des matières

Dans ce tutoriel, nous aborderons les sujets suivants :

Présentation des mutex

Un mutex est un type spécial de sémaphore binaire (consultez notre tutoriel sur les sémaphores) utilisé pour contrôler l’accès à une ressource partagée entre deux ou plusieurs tâches. Il est créé avec xSemaphoreCreateMutex() et garantit qu’une seule tâche peut utiliser la ressource à la fois.

Le terme « mutex » signifie « MUTual EXclusion » et fonctionne comme un jeton qui permet à la tâche qui détient le jeton d’accéder en toute sécurité à la ressource partagée. Pour utiliser un mutex, une tâche doit d’abord le « prendre » avec xSemaphoreTake(mutex, timeout) pour devenir détenteur du token et entrer dans la section critique (le code qui accède à la ressource). Lorsque la tâche est terminée, elle « rend » le mutex avec xSemaphoreGive(mutex), le déverrouillant pour d’autres tâches. Ce n’est qu’alors qu’une autre tâche pourra prendre le mutex et accéder à la ressource sans conflits.

Le diagramme suivant montre comment cela fonctionne.

Diagramme montrant le fonctionnement de l'exclusion mutuelle avec les tâches FreeRTOS
Utiliser un mutex pour protéger l’accès à une ressource partagée (Source de l’image : freertos.org)

Scénarios ESP32

Par exemple, imaginez un scénario dans lequel deux tâches effectuent des requêtes HTTP pour enregistrer des données dans une base de données. Sans mutex, les deux tâches pourraient tenter d’accéder et d’écrire dans la base de données en même temps, risquant ainsi de corrompre les données car une tâche démarre une nouvelle requête HTTP avant que l’autre ne se termine. Avec un mutex, vous pouvez garantir qu’une seule tâche accède à la base de données à la fois, garantissant ainsi des opérations sûres et séquentielles.

Ce concept s’applique à de nombreux scénarios, tels que l’accès partagé au port série par plusieurs tâches, le bus I2C pour les capteurs et les écrans, plusieurs tâches de lecture à partir du même capteur, plusieurs tâches écrivant des données sur le même écran, etc.

Mutex et héritage prioritaire

Dans FreeRTOS, lorsqu’une tâche détient un mutex, elle garde le contrôle de la ressource partagée jusqu’à ce qu’elle libère le jeton. Si une tâche de faible priorité détient le mutex et qu’une tâche de haute priorité tente de le prendre, la tâche de haute priorité sera obligée d’attendre. Cette situation est appelée inversion de priorité, car une tâche de faible priorité bloque effectivement l’exécution d’une tâche de priorité plus élevée.

Pour gérer cela, FreeRTOS implémente l’héritage des priorités. Lorsqu’une tâche de priorité élevée attend un mutex détenu par une tâche de priorité inférieure, la tâche de priorité inférieure « hérite » temporairement de la priorité la plus élevée. Cela lui permet de terminer son travail et de libérer le mutex plus tôt, réduisant ainsi le délai pour la tâche hautement prioritaire. Une fois le mutex libéré, la priorité de la tâche revient à son niveau d’origine.

Donc en résumé :

  • L’inversion de priorité se produit lorsqu’une tâche de faible priorité détient un mutex et bloque une tâche de haute priorité qui en a besoin.
  • La tâche à priorité élevée ne peut pas s’exécuter tant que la tâche à faible priorité n’a pas libéré le mutex.
  • FreeRTOS utilise l’héritage de priorité pour résoudre ce problème.
  • La tâche de faible priorité hérite temporairement de la priorité élevée.
  • Cela lui permet de terminer son travail plus rapidement et de libérer le mutex plus tôt.
  • Une fois libérée, la priorité de la tâche revient à la normale.

Enfin, une autre chose importante à propos des mutex est qu’ils ne doivent pas être utilisés à partir d’une interruption car (citant la documentation FreeRTOS) :

  • Ils incluent un mécanisme d’héritage de priorité qui n’a de sens que si le mutex est donné et extrait d’une tâche, et non d’une interruption.
  • Une interruption ne peut pas bloquer l’attente qu’une ressource protégée par un mutex devienne disponible.

Exemples d’application d’utilisation de Mutex sur des projets ESP32

L’utilisation de mutex FreeRTOS peut être utile pour vos projets IoT ESP32. Par exemple:

Carte ESP32 CYD affichant les données reçues via ESP-NOW sur une table
  • Données de capteur partagées : par exemple, plusieurs tâches doivent lire ou mettre à jour les mêmes valeurs de capteur.
  • Sortie d’affichage : plusieurs tâches doivent être imprimées sur le moniteur série ou sur un écran. Nous devons nous assurer qu’une seule tâche génère des données à la fois afin que ses résultats ne se chevauchent pas. Nous examinerons cet exemple dans ce tutoriel.
  • Opérations Wi-Fi : si différentes tâches effectuent des requêtes HTTP, une publication MQTT ou une communication WebSocket, un mutex empêche l’accès simultané à la même ressource réseau, évitant ainsi les erreurs de connexion ou la corruption de la base de données.
  • Enregistrement des données sur une carte SD ou LittleFS : l’écriture sur la mémoire flash ou sur une carte SD doit être effectuée avec soin pour éviter toute corruption. Avec un mutex, nous garantissons qu’une tâche termine l’opération avant que l’autre puisse démarrer.

Fonctions de base du Mutex

Voici quelques fonctions de base pour créer et gérer un mutex à l’aide de l’ESP32 avec l’IDE Arduino. Nous explorerons ensuite ces fonctions dans un exemple pratique.

Créer un mutex

Pour créer un mutex, utilisez xSemaphoreCreateMutex(). Il renvoie un handle SemaphoreHandle_t ou NULL si la création échoue.

Prendre un Mutex

Pour prendre un mutex (et accéder à la ressource partagée), utilisez xSemaphoreTake(mutex, timeout). Le délai d’attente est la durée maximale pendant laquelle la tâche bloquera en attendant que le mutex soit disponible. Utilisez portMAX_DELAY pour le délai d’attente si vous souhaitez que la tâche attende indéfiniment.

Donner un Mutex

Pour donner le mutex (pour le libérer afin que d’autres tâches puissent accéder à la ressource partagée), utilisez xSemaphoreGive(mutex), débloquant une tâche en attente. Cela doit toujours être appelé après la section critique pour libérer la ressource.

Exemple de mutex ESP32 : sortie série partagée

Pour vous montrer comment utiliser un Mutex avec l’ESP32 et comment il fonctionne, nous allons jeter un œil à un exemple simple de sortie série partagée. Nous allons tester le code avec et sans Mutex afin que vous puissiez voir comment il fonctionne.

Voici l’exemple que nous allons exécuter :

  • Nous allons créer deux tâches différentes.
  • Chaque tâche imprimera deux lignes de texte sur le moniteur série, avec un petit délai entre les lignes.
  • Les tâches s’exécutent à des intervalles différents, de sorte que leurs résultats peuvent se chevaucher.
  • Nous ne voulons pas que des chevauchements se produisent.
  • En utilisant un mutex, nous nous assurons que chaque tâche termine l’impression de ses deux lignes avant que l’autre tâche ne commence à écrire.

Tout d’abord, nous allons exécuter l’exemple sans mutex. Ensuite, nous l’exécuterons à nouveau en utilisant un mutex pour montrer la différence.

Sortie série partagée (sans Mutex)

Téléchargez le code suivant sur votre carte ESP32.

/*
  Rui Santos & Sara Santos - Raspberryme.com
  Complete project details at https://Raspberryme.com/esp32-freertos-mutex-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
void Task1(void *parameter) {
  for (;;) {
    Serial.println("Task1: Logging from Task 1");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    Serial.println("Task1: End of log from Task 1");
    vTaskDelay(5000 / portTICK_PERIOD_MS);
  }
}

void Task2(void *parameter) {
  for (;;) {
    Serial.println("Task2: Logging from Task 2");
    vTaskDelay(800 / portTICK_PERIOD_MS);
    Serial.println("Task2: End of log from Task 2");
    vTaskDelay(3000 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Starting FreeRTOS");

  xTaskCreatePinnedToCore(
    Task1,                  // Task function
    "Task1",                // Task name
    3000,                   // Stack size
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );

  xTaskCreatePinnedToCore(
    Task2,                  // Task function
    "Task2",                // Task name
    3000,                   // Stack size
    NULL,                   // Task parameters
    2,                      // Higher priority
    NULL,                   // Task handle
    1                       // Core ID
  );
}

void loop() {

}

Afficher le code brut

Ce code crée deux tâches. Les deux écrivent deux lignes de texte sur le moniteur série.

Voici une tâche :

void Task1(void *parameter) {
  for (;;) {
    Serial.println("Task1: Logging from Task 1");
    vTaskDelay(1000 / portTICK_PERIOD_MS);
    Serial.println("Task1: End of log from Task 1");
    vTaskDelay(5000 / portTICK_PERIOD_MS);
  }
}

Et voici l’autre :

void Task2(void *parameter) {
  for (;;) {
    Serial.println("Task2: Logging from Task 2");
    vTaskDelay(800 / portTICK_PERIOD_MS);
    Serial.println("Task2: End of log from Task 2");
    vTaskDelay(3000 / portTICK_PERIOD_MS);
  }
}

Maintenant, après avoir téléchargé le code, appuyez sur le bouton ESP32 RST et ouvrez le moniteur série.

Sortie série partagée ESP32 sans mutex. Une tâche s'exécute avant la fin de l'autre

Vous verrez qu’une tâche peut imprimer sa sortie pendant que l’autre est toujours en cours d’exécution, ce qui entraîne un chevauchement de ses sorties. Cet exemple montre ce qui peut se produire dans des situations similaires, telles que l’impression de données sur un écran ou l’écriture dans une base de données. Pour éviter cela, nous pouvons utiliser un mutex (voir l’exemple suivant).

Sortie série partagée (avec Mutex)

Cet exemple fonctionne de la même manière que le précédent, mais nous utilisons maintenant un mutex afin qu’une tâche ne puisse pas s’exécuter tant que l’autre ne la libère pas.

Voici le code à télécharger sur l’ESP32 :

/*
  Rui Santos & Sara Santos - Raspberryme.com
  Complete project details at https://Raspberryme.com/esp32-freertos-mutex-arduino/
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define MUTEX_TIMEOUT 5000  // 5s timeout

SemaphoreHandle_t serialMutex = NULL;

void Task1(void *parameter) {
  for (;;) {
    if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
      Serial.println("Task1: Logging from Task 1");
      vTaskDelay(1000 / portTICK_PERIOD_MS);
      Serial.println("Task1: End of log from Task 1");
      xSemaphoreGive(serialMutex);
    }
    vTaskDelay(5000 / portTICK_PERIOD_MS);
  }
}

void Task2(void *parameter) {
  for (;;) {
    if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
      Serial.println("Task2: Logging from Task 2");
      vTaskDelay(800 / portTICK_PERIOD_MS);
      Serial.println("Task2: End of log from Task 2");
      xSemaphoreGive(serialMutex);
    }
    vTaskDelay(3000 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Starting FreeRTOS");

  serialMutex = xSemaphoreCreateMutex();
  if (serialMutex == NULL) {
    Serial.println("Failed to create mutex!");
    while (1);
  }

  xTaskCreatePinnedToCore(
    Task1,                  // Task function
    "Task1",                // Task name
    3000,                   // Stack size
    NULL,                   // Task parameters
    1,                      // Priority
    NULL,                   // Task handle
    1                       // Core ID
  );

  xTaskCreatePinnedToCore(
    Task2,                  // Task function
    "Task2",                // Task name
    3000,                   // Stack size
    NULL,                   // Task parameters
    2,                      // Higher priority
    NULL,                   // Task handle
    1                       // Core ID
  );
}

void loop() {
  
}

Afficher le code brut

Comment fonctionne le code ?

Nous commençons par définir le délai d’attente du mutex. Il s’agit de la durée maximale pendant laquelle une tâche attendra le mutex jusqu’à ce qu’elle renvoie pdFALSE.

#define MUTEX_TIMEOUT 5000  // 5s timeout

Créez un handle pour le mutex. C’est le même type de handle utilisé avec un sémaphore ordinaire. Nous l’appelons SerialMutex.

SemaphoreHandle_t serialMutex = NULL;

Dans setup(), nous créons le mutex en utilisant la fonction xSemaphoreCreateMutex() comme nous l’avons vu dans la section d’introduction.

serialMutex = xSemaphoreCreateMutex();
if (serialMutex == NULL) {
  Serial.println("Failed to create mutex!");
  while (1);
}

Toujours dans le setup(), nous créons nos tâches.

xTaskCreatePinnedToCore(
  Task1,                  // Task function
  "Task1",                // Task name
  3000,                   // Stack size
  NULL,                   // Task parameters
  1,                      // Priority
  NULL,                   // Task handle
  1                       // Core ID
);

xTaskCreatePinnedToCore(
  Task2,                  // Task function
  "Task2",                // Task name
  3000,                   // Stack size
  NULL,                   // Task parameters
  2,                      // Higher priority
  NULL,                   // Task handle
  1                       // Core ID
);

Jetons maintenant un œil aux fonctions de rappel des tâches définies avant le setup(). La ligne suivante montre la tâche 1, et ici nous pouvons voir comment nous utilisons le mutex. Il utilise la même API que les sémaphores.

void Task1(void *parameter) {
  for (;;) {
    if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
      Serial.println("Task1: Logging from Task 1");
      vTaskDelay(1000 / portTICK_PERIOD_MS);
      Serial.println("Task1: End of log from Task 1");
      xSemaphoreGive(serialMutex);
    }
    vTaskDelay(5000 / portTICK_PERIOD_MS);
  }
}

Tout d’abord, nous vérifions si nous pouvons obtenir le mutex avec xSemaphoreTake(). Cela durera jusqu’à une durée maximale de MUTEX_TIMEOUT.

if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {

Si nous le pouvons, nous imprimons les lignes sur le Serial Monitor, ce qui signifie que nous entrons dans la tâche critique (la ressource partagée).

Serial.println("Task1: Logging from Task 1");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println("Task1: End of log from Task 1");

Juste après l’impression, puisque nous avons déjà terminé la section critique, qui dans ce cas accède au Serial Monitor, nous pouvons maintenant libérer le mutex avec xSemaphoreGive(serialMutex).

xSemaphoreGive(serialMutex);

Si le MUTEX_TIMEOUT est dépassé lors de l’attente du mutex, il passera à la ligne suivante avant de vérifier à nouveau le mutex.

vTaskDelay(5000 / portTICK_PERIOD_MS);

Le rappel pour Task2 fonctionne de la même manière, mais avec des timings différents.

void Task2(void *parameter) {
  for (;;) {
    if (xSemaphoreTake(serialMutex, MUTEX_TIMEOUT)) {
      Serial.println("Task2: Logging from Task 2");
      vTaskDelay(800 / portTICK_PERIOD_MS);
      Serial.println("Task2: End of log from Task 2");
      xSemaphoreGive(serialMutex);
    }
    vTaskDelay(3000 / portTICK_PERIOD_MS);
  }
}

Démonstration

Téléchargez le code sur l’ESP32. Ensuite, ouvrez le moniteur série et appuyez sur le bouton ESP32 RST pour qu’il commence à exécuter le code.

Sortie série partagée ESP32 avec Mutex

Notez que cette fois, la deuxième tâche ne démarre qu’une fois que la première a fini d’imprimer sa sortie. Le mutex empêche les tâches de se chevaucher. Les deux tâches veulent accéder au moniteur série, mais seule la tâche contenant le mutex peut y écrire.

Conclusion

En résumé, l’utilisation de mutex est un moyen simple mais puissant de contrôler l’accès aux ressources partagées dans vos projets ESP32. Que vous imprimiez sur le moniteur série, mettiez à jour un affichage, écriviez dans un fichier ou envoyiez des données via Wi-Fi, mettiez à jour une base de données, etc., les mutex aident à éviter les conflits entre les tâches de votre code.

Pour plus d’informations sur les mutex FreeRTOS, vous pouvez consulter la documentation officielle.

Nous espérons que vous avez trouvé ce tutoriel utile. Si vous souhaitez en savoir plus sur l’utilisation de la programmation FreeRTOS avec l’ESP32 sur l’IDE Arduino, assurez-vous de consulter nos autres didacticiels de cette série :

Pour en savoir plus sur l’ESP32, assurez-vous de consulter nos ressources :

Cette vidéo vous emmène dans l’histoire de Raspberry Pi :

Youtube video

  • ELEGOO Carte Starter Kit de Démarrage ESP-32 Compatible avec Arduino IDE
  • Carte de développement ESP32, ESP32 WROOM 32U pour Arduino, ESP-32-DevKitC V4 Carte avec Antenne 3 dBi Câble U.FL vers SMA Femelle Compatible Prototypage IoT Bluetooth BLE WiFi pour Raspberry Pi STM32