ESP32 + FreeRTOS

Wikipedia : FreeRTOS est un système d’exploitation temps réel (RTOS) open source de faible empreinte et portable. Il fonctionne en mode préemptif. Il a été porté sur dizaines d’architectures différentes. Créé en 2003 par Richard Barry et la FreeRTOS Team, il est aujourd’hui parmi les plus utilisés dans le marché des systèmes d’exploitation temps réel.

Notions de base

Dans PlatformIO, il est possible d’utiliser FreeRTOS soit avec le framework Arduino soit avec le framework officiel Espressif ESP-IDF (Espressif IoT Development Framework) :

  • Avec le framework Arduino, on crée un projet avec le fichier platformio.ini suivant :
[env:lolin32]
platform = espressif32
board = lolin32
framework = arduino
lib_deps =

Il faudra juste inclure le fichier d’en-tête suivant :

#include <Arduino.h>
  • Avec le framework Espressif (ESP-IDF), on crée un projet avec le fichier platformio.ini suivant :
[env:lolin32]
platform = espressif32
board = lolin32
framework = espidf
lib_deps =

Il faudra juste inclure les fichiers d’en-tête suivants :

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"

Notions de tâche

L’unité d’exécution contrôlée par FreeRTOS est une tâche (task).

Les tâches sont de simples fonctions qui s’exécutent généralement en boucle infinie et qui suivent la structure générique suivante :

void vATaskFunction( void *pvParameters ) // <- une tâche
{
    for( ;; ) // <- boucle infinie
    {
        /* Ajouter le code de votre tâche ici */
    }
}

Remarque : Le nombre de tâches exécutées simultanément et leur priorité ne sont limités que par le matériel.

On crée une tâche avec un appel à la fonction xTaskCreate().

Cette fonction accepte les arguments suivants :

  • pvTaskCode : pointeur sur la fonction qui implémentera la tâche (ici vATaskFunction)
  • pcName : nom de la tâche (chaîne de caractères)
  • usStackDepth : taille (en nombre de mots) de la pile
  • pvParameters : pointeur vers un paramètre passée à la fonction de tâche (void *)
  • uxPriority : priorité de la tâche (int)
  • pxCreatedTask : descripteur de la tâche

Cette fonction retourne pdPASS en cas de succès ou un code d’erreur .

xTaskCreate(
    vATaskFunction, /* Task function. */
    "vATaskFunction", /* name of task. */
    10000, /* Stack size of task */
    NULL, /* parameter of the task */
    1, /* priority of the task */
    NULL); /* Task handle to keep track of created task */

Dans FreeRTOS, chaque tâche est décrite par un TCB (Task Control Block) qui contient toutes les informations nécessaires afin de spécifier et de représenter une tâche :

Exemple n°1 (framework Arduino) :

void vATaskFunction( void *pvParameters ) // <- une tâche
{
    int variable1; // <- variable allouée dans la pile (*stack*) de la tâche et unique pour chaque instance de tâche
    static int variable2; // <- variable allouée en dehors de la pile de la tâche et partagée pour chaque instance de tâche
    
    for( ;; ) // <- boucle infinie
    {
      Serial.printf("vATaskFunction %d\n", xPortGetCoreID());
      delay(1000);
    }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xTaskCreate(
    vATaskFunction, /* Task function. */
    "vATaskFunction", /* name of task. */
    1000, /* Stack size of task */
    NULL, /* parameter of the task */
    1, /* priority of the task */
    NULL); /* Task handle to keep track of created task */
}

void loop()
{
  Serial.printf("Task loop() %d\n", xPortGetCoreID());
  delay(1000);  
}

On obtient :

Start
vATaskFunction 0
Task loop() 1
vATaskFunction 0
Task loop() 1

Remarque : 2 tâches s’exécutent (vATaskFunction() et loop()) sur les 2 coeurs de l’ESP32. Il est possible d’enpâcher le lancement de loop() en plaçant une boucle infinie à la fin de setup().

FreeRTOS fournit une fonction vTaskDelay() qui place la tâche appelante dans l’état Bloqué (Blocked) pour un nombre fixe d’interruptions de tick. A la fin de ce délai, la tâche repasse à l’état Prêt (Ready). La macro pdMS_TO_TICKS() peut être utilisée pour convertir un temps spécifié en millisecondes en un nombre de ticks.

Exemple n°2 (framework Arduino) :

void vTask1( void *pvParameters )
{
  for( ;; )
  {
    Serial.printf("vTask1 %d\n", xPortGetCoreID());
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
}

void vTask2( void *pvParameters )
{
  for( ;; )
  {
    Serial.printf("vTask2 %d\n", xPortGetCoreID());
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xTaskCreate(vTask1, "vTask1", 10000, NULL, 1, NULL);
  xTaskCreate(vTask2, "vTask2", 10000, NULL, 1, NULL);
}

void loop()
{
  Serial.printf("Task loop() %d\n", xPortGetCoreID());
  delay(1000);  
}

On obtient :

Start
Task loop() 1
vTask2 0
vTask1 0
vTask2 0
vTask1 0
Task loop() 1
vTask1 0
vTask2 0
vTask1 0
vTask2 0
Task loop() 1

Exemple n°2 (framework Espressif) :

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"

void vTask1( void *pvParameters )
{
  const char *pcTaskName = "Task 1 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
}

void vTask2( void *pvParameters )
{
  const char *pcTaskName = "Task 2 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 1000 ) );
  }
}

void app_main()
{
    xTaskCreate(vTask1, "vTask1", 10000, NULL, 1, NULL);
    xTaskCreate(vTask2, "vTask2", 10000, NULL, 1, NULL);

    for( ;; );
}

Tâche périodique

Pour réaliser des tâches périodiques, on utilisera la fonction vTaskDelayUntil().

Exemple n°3 (framework Arduino) :

void vTaskPeriodic( void *pvParameters )
{
  const char *pcTaskName = "Task periodique";
  TickType_t xLastWakeTime;
  xLastWakeTime = xTaskGetTickCount();
  for( ;; )
  {
    Serial.printf("%s %d\n", pcTaskName, xPortGetCoreID());
    vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) ); // toutes les 250 ms
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xTaskCreate(vTaskPeriodic, "vTaskPeriodic", 10000, NULL, 2, NULL);
}

void loop()
{
  Serial.printf("Task loop() %d\n", xPortGetCoreID());
  delay(1000);  
}

Voir aussi : les temporisateurs logiciels

Notions de priorité

Les tâches en FreeRTOS se voient assigner à leur création, un niveau de priorité représenté par un nombre entier. Le niveau le plus bas vaut zéro et il doit être strictement réservé pour la tâche Idle (cf. la constante tskIDLE_PRIORITY du fichier FreeRTOSConfig.h).

Le nombre maximum de niveaux de priorités est défini par la constante : configMAX_PRIORITIES. Plusieurs tâches peuvent appartenir à un même niveau de priorité.

L’ordonnancement fonctionne selon le modèle Round-Robin avec gestion des priorités. Il s’exécute à une fréquence égale à configTICK_RATE_HZ (normalement fixé à 1 kHz soit toutes les millisecondes).

L’ordonnancement des tâches a pour but principal de décider parmi les tâches qui sont dans l’état « prêt », laquelle exécuter. Pour faire ce choix, l’ordonnanceur de FreeRTOS se base uniquement sur la priorité des tâches.

Les différents états d’une tâche :

Exemple n°4 (framework Arduino) :

#include <Arduino.h>

void infos()
{
  esp_chip_info_t out_info;
  esp_chip_info(&out_info);
  Serial.print("CPU freq : "); Serial.println(String(ESP.getCpuFreqMHz()) + " MHz");
  Serial.print("CPU cores : ");  Serial.println(String(out_info.cores));
  Serial.print("Flash size : "); Serial.println(String(ESP.getFlashChipSize() / 1000000) + " MB");
  Serial.print("Free RAM : "); Serial.println(String((long)ESP.getFreeHeap()) + " bytes");
  //Serial.print("Min. free seen : "); Serial.println(String((long)esp_get_minimum_free_heap_size()) + " bytes");
  Serial.print("tskIDLE_PRIORITY : "); Serial.println(String((long)tskIDLE_PRIORITY));
  Serial.print("configMAX_PRIORITIES : "); Serial.println(String((long)configMAX_PRIORITIES));
  Serial.print("configTICK_RATE_HZ : "); Serial.println(String(configTICK_RATE_HZ) + " Hz");
  Serial.println();
}

void vTask1( void *pvParameters )
{
  const char *pcTaskName = "Task 1 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    //vTaskDelay( pdMS_TO_TICKS( 1000 ) );
  }
}

void vTask2( void *pvParameters )
{
  const char *pcTaskName = "Task 2 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    //vTaskDelay( pdMS_TO_TICKS( 1000 ) );
  }
}

void vTask3( void *pvParameters )
{
  const char *pcTaskName = "Task 3 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    //vTaskDelay( pdMS_TO_TICKS( 1000 ) );
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);

  Serial.println("Start");
  infos();

  xTaskCreate(vTask1, "vTask1", 10000, NULL, 10, NULL);
  xTaskCreate(vTask2, "vTask2", 10000, NULL, 9, NULL);
  xTaskCreate(vTask3, "vTask3", 10000, NULL, 8, NULL);
}

void loop()
{
  const char *pcTaskName = "Main loop is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    //vTaskDelay( pdMS_TO_TICKS( 1000 ) );
  }
}

On obtient :

Start
CPU freq : 240 MHz
CPU cores : 2
Flash size : 4 MB
Free RAM : 362532 bytes
tskIDLE_PRIORITY : 0
configMAX_PRIORITIES : 25
configTICK_RATE_HZ : 1000 Hz

Task 2 is running - core = 1 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 1 (priorite 10)
Task 2 is running - core = 1 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 1 (priorite 10)
Task 2 is running - core = 1 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 1 (priorite 10)
Task 2 is running - core = 1 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 1 (priorite 10)
Task 2 is running - core = 1 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 1 (priorite 10)
Task 2 is running - core = 1 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 1 (priorite 10)
Task 2 is running - core = 1 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
...

Remarque : l’exemple ci-dessus montre que l’ordonnancement des tâches ne se fait que sur celles qui sont à l’état « prêt ». Les tâches vTask3() et loop() ont une trop faible priorité et ne seront jamais exécutées car les tâches vTask1() et vTask2() ont une priorité plus forte et ne passent jamais à un état « bloqué ».

En faisant passer les différentes tâches à un état « bloqué » (avec vTaskDelay() par exemple), on obtient alors :

...
Task 1 is running - core = 0 (priorite 10)
Task 3 is running - core = 0 (priorite 8)
Main loop is running - core = 1 (priorite 1)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 3 is running - core = 0 (priorite 8)
Main loop is running - core = 1 (priorite 1)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
...

Dans FreeRTOS Il n’y a aucun mécanisme automatique de gestion des priorités. La priorité d’une tâche ne pourra être changée qu’à la demande explicite du développeur. Pour cela, on utilise la fonction vTaskPrioritySet().

Terminer une tâche

Pour terminer une tâche, il faut appeler la fonction vTaskDelete() en lui passant le handle de la tâche à terminer ou NULL pour la tâche courante.

Exemple n°5 (framework Arduino) :

#include <Arduino.h>

TaskHandle_t xTask2Handle = NULL;

void vTask1( void *pvParameters )
{
  const char *pcTaskName = "Task 1 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for(int i=0;i<10;i++)
  {
    if(i == 5)
      vTaskDelete( xTask2Handle ); // termine la tâche n°2
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
  vTaskDelete( NULL ); // termine la tâche n°1
}

void vTask2( void *pvParameters )
{
  const char *pcTaskName = "Task 2 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xTaskCreate(vTask1, "vTask1", 10000, NULL, 10, NULL);
  xTaskCreate(vTask2, "vTask2", 10000, NULL, 9, &xTask2Handle);
}

void loop()
{
  const char *pcTaskName = "Main loop is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 1000 ) );
  }
}

On obtient :

Main loop is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Main loop is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Task 1 is running - core = 0 (priorite 10)
Task 2 is running - core = 0 (priorite 9)
Main loop is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 10)
Task 1 is running - core = 0 (priorite 10)
Main loop is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 10)
Task 1 is running - core = 0 (priorite 10)
Main loop is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 10)
Main loop is running - core = 1 (priorite 1)
Main loop is running - core = 1 (priorite 1)
Main loop is running - core = 1 (priorite 1)
Main loop is running - core = 1 (priorite 1)
Main loop is running - core = 1 (priorite 1)
...

Affecter une tâche

Il est possible de créer et affecter une tâche à un coeur (core) avec la fonction xTaskCreatePinnedToCore() qui possède un argument supplémentaire xCoreID qui permet de sélectionner le numéro de coeur.

Exemple n°6 (framework Arduino) :

void vTask1( void *pvParameters )
{
  const char *pcTaskName = "Task 1 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
}

void vTask2( void *pvParameters )
{
  const char *pcTaskName = "Task 2 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xTaskCreatePinnedToCore(vTask1, "vTask1", 10000, NULL, 1, NULL, 0);
  xTaskCreatePinnedToCore(vTask2, "vTask2", 10000, NULL, 1, NULL, 1);  
}

void loop()
{
  const char *pcTaskName = "Main loop is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for( ;; )
  {
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    vTaskDelay( pdMS_TO_TICKS( 1000 ) );
  }
}

Résultat :

...
Main loop is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 1)
Task 2 is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 1)
Task 2 is running - core = 1 (priorite 1)
Main loop is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 1)
Task 2 is running - core = 1 (priorite 1)
Task 1 is running - core = 0 (priorite 1)
Task 2 is running - core = 1 (priorite 1)
...

Communication entre tâches

Les files (queue) permettent de faire communiquer et synchroniser les tâches entre elles.

Une queue (file) peut contenir un nombre fini d’éléments de données de taille fixe. La longueur (nombre maximal d’éléments) et la taille de chaque élément de données sont définies lors de la création de la file.

Les files sont normalement utilisées en tant que tampons FIFO (First In First Out) où les données sont écrites à la fin (tail) de la file et supprimées par l’avant (head) de la file.

Les files sont des objets auxquels peuvent accéder n’importe quelle tâche ou fonction d’interruption (ISR) qui connaît leur existence.

Lorsqu’une tâche tente de lire dans une file, elle peut éventuellement spécifier un timeout (temps pendant lequel la tâche sera maintenue à l’état Bloqué pour attendre que les données soient disponibles dans la file) si la file d’attente est déjà vide. Une tâche qui est dans l’état Bloqué, en attendant que les données soient disponibles dans une file d’attente, est automatiquement déplacée vers l’état Prêt lorsqu’une autre tâche ou interruption place des données dans la file d’attente. La tâche sera également déplacée automatiquement du état Bloqué à l’état Prêt si le timeout spécifié expire avant que les données ne soient disponibles.

Les files peuvent avoir plusieurs lecteurs, il est donc possible qu’une même file soit bloquée par plusieurs tâches en attente de données. Dans ce cas, une seule tâche sera débloquée lorsque les données seront disponibles. La tâche qui est débloquée sera toujours la plus haute priorité tâche qui attend des données. Si les tâches bloquées ont une priorité égale, la tâche qui attendait les données le plus longtemps sera débloquée.

Une tâche peut aussi spécifier un timeout (temps maximal pendant lequel la tâche doit être maintenue dans l’état Bloqué pour attendre que l’espace soit disponible dans la file) lors de l’écriture dans une file. Les files peuvent avoir plusieurs rédacteurs, il est donc possible pour une file complète d’avoir plus d’une tâche bloquée sur elle en attendant de terminer une opération d’envoi. Dans ce cas, une seule tâche sera débloquée lorsque de l’espace sur la file sera disponible. La tâche qui est débloquée sera toujours la tâche de priorité la plus élevée qui attend de l’espace. Si les tâches bloquées ont la même priorité, alors la tâche qui a attendu l’espace le plus longtemps sera débloquée.

Une file d’attente doit être créée explicitement avec xQueueCreate() avant de pouvoir être utilisée. La fonction xQueueCreate() crée une file et retourne un descripteur sur celle-ci de type QueueHandle_t.

  • xQueueSendToBack() ou xQueueSend() est utilisé pour envoyer (écrire) des données à l’arrière (tail) et xQueueSendToFront() pour envoyer des données à l’avant (head) d’une file d’attente.

  • xQueueReceive() est utilisé pour recevoir (lire) un élément d’une file. L’élément reçu (lu) est supprimé de la file.

  • uxQueueMessagesWaiting() est utilisé pour interroger le nombre d’éléments qui sont actuellement dans une file.

Exemple n°7 (framework Arduino) :

#include <Arduino.h>

#define TAILLE_MAX    5

QueueHandle_t xQueue;

void vSenderTask( void *pvParameters )
{
  const char *pcTaskName = "vSenderTask";
  int valueToSend;
  BaseType_t status;
  UBaseType_t uxPriority;

  valueToSend = ( int ) pvParameters;
  uxPriority = uxTaskPriorityGet( NULL );    

  for( ;; )
  {
    status = xQueueSendToBack( xQueue, &valueToSend, 0 );
    if( status == pdPASS )
    {
      Serial.printf("%s - envoyé = %d (priorite %d)\n", pcTaskName, valueToSend, uxPriority);
    }
    else
    {
      Serial.printf("%s - envoyé = aucun (priorite %d)\n", pcTaskName, uxPriority);
    }
  }
}

void vReceiverTask( void *pvParameters )
{
  const char *pcTaskName = "vReceiverTask";
  int receivedValue;
  BaseType_t status;
  UBaseType_t uxPriority;

  uxPriority = uxTaskPriorityGet( NULL );
  
  for( ;; )
  {
    Serial.printf("%s - nb elements = %d (priorite %d)\n", pcTaskName, uxQueueMessagesWaiting( xQueue ), uxPriority);
    status = xQueueReceive( xQueue, &receivedValue, pdMS_TO_TICKS( 100 ) );
    if( status == pdPASS )
    {
      Serial.printf("%s - lu = %d (priorite %d)\n", pcTaskName, receivedValue, uxPriority);
    }
    else
    {
      Serial.printf("%s - lu = aucun (priorite %d)\n", pcTaskName, uxPriority);
    }
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xQueue = xQueueCreate( TAILLE_MAX, sizeof( int ) );

  xTaskCreate( vSenderTask, "Sender1", 10000, (void *)100, 1, NULL );
  xTaskCreate( vSenderTask, "Sender2", 10000, (void *)200, 1, NULL );
  xTaskCreate( vReceiverTask, "Receiver", 10000, NULL, 2, NULL );

  for(;;); // pas de loop()
}

void loop()
{
}

Résultat :

...
vSenderTask - envoyé = 100 (priorite 1)
vReceiverTask - lu = 100 (priorite 2)
vReceiverTask - nb elements = 0 (priorite 2)
vSenderTask - envoyé = 100 (priorite 1)
vSenderTask - envoyé = 200 (priorite 1)
vReceiverTask - lu = 100 (priorite 2)
vReceiverTask - nb elements = 1 (priorite 2)
vReceiverTask - lu = 200 (priorite 2)
vReceiverTask - nb elements = 0 (priorite 2)
vSenderTask - envoyé = 200 (priorite 1)
vReceiverTask - lu = 200 (priorite 2)
vReceiverTask - nb elements = 0 (priorite 2)
vSenderTask - envoyé = 200 (priorite 1)
vReceiverTask - lu = 200 (priorite 2)
vReceiverTask - nb elements = 0 (priorite 2)
vSenderTask - envoyé = 200 (priorite 1)
vSenderTask - envoyé = 100 (priorite 1)
vReceiverTask - lu = 200 (priorite 2)
vReceiverTask - nb elements = 1 (priorite 2)
vReceiverTask - lu = 100 (priorite 2)
vReceiverTask - nb elements = 0 (priorite 2)
vSenderTask - envoyé = 100 (priorite 1)
vReceiverTask - lu = 100 (priorite 2)
vReceiverTask - nb elements = 0 (priorite 2)
...

Synchronisation entre tâches

FreeRTOS synchronise les tâches en utilisant principalement deux mécanismes : les Sémaphores et les Mutex.

Remarque : ces deux ressources sont implémentées sous forme de file (queue) dans FreeRTOS.

FreeRTOS fait une différence entre les sémaphores à N éléments (sémaphore de comptage) et les sémaphore binaires (un seul élement 0 ou 1). Un Sémaphore binaire ne pourra donc être pris qu’une seule fois avant qu’il ne devienne indisponible contrairement au Sémaphore à N éléments qui pourra être pris à plusieurs reprises ce qui permet par exemple de définir un nombre de ressources disponibles.

L’implémentation des Mutex dans FreeRTOS est similaire à celle des Sémaphores binaires sauf que la tâche qui prend le Mutex doit obligatoirement le rendre. Cela peut être vu comme l’association d’un jeton à une ressource : une tâche prend le jeton et utilise la ressource puis rend le jeton à la fin, au même moment aucun autre jeton supplémentaire ne pourra être associé à la tâche.

Une autre différence majeure entre les Mutex et les Sémaphores binaires dans FreeRTOS est le système d’héritage de priorité. Quand plusieurs tâches demandent à prendre un Mutex, la priorité du détenteur du Mutex est fixée momentanément à la valeur de la plus haute priorité parmi les tâches qui attendent sa libération. Cette technique a pour effet de prévenir les phénomènes à risques d’inversion de priorité même si cela ne garantit pas une sécurité infaillible face à ces phénomènes.

Les sémaphores

Les sémaphores FreeRTOS sont stockés dans une variable de type SemaphoreHandle_t.

Avant qu’un sémaphore puisse être utilisé, il doit être créé :

  • avec la fonction xSemaphoreCreateBinary() pour un sémaphore binaire.
  • avec la fonction xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) pour un sépahore de comptage où uxMaxCount représente la valeur maximale de comptage et uxInitialCount sa valeur initiale.

« Prendre » un sémaphore signifie « obtenir » ou « recevoir » le sémaphore. Le sémaphore ne peut être pris que s’il est disponible. Tous les différents types de sémaphore FreeRTOS peuvent être « pris » en utilisant la fonction xSemaphoreTake() (un timeout peut être passé en second argument) ou xSemaphoreTakeFromISR().

Les sémaphores binaires et de comptage peuvent être « donnés » en utilisant la fonction xSemaphoreGive() ou xSemaphoreGiveFromISR().

// Création d'un sémaphore de comptage
SemaphoreHandle_t semaphore = NULL;
semaphore = xSemaphoreCreateCounting( 10, 0 );

// Opération P()
xSemaphoreTake( semaphore, portMAX_DELAY );

// Opération V()
xSemaphoreGive( semaphore );

Les mutex

Les sémaphores FreeRTOS sont stockés dans une variable de type SemaphoreHandle_t.

Avant qu’un mutex puisse être utilisé, il doit être créé avec la fonction xSemaphoreCreateMutex()

// Création d'un mutex
SemaphoreHandle_t mutex = NULL;
mutex = xSemaphoreCreateMutex;

// Exemple d'accès protégé à une ressource (ici Serial)
void printString( const char *pcString )
{
    // Verrouillage du mutex
    xSemaphoreTake( mutex, portMAX_DELAY );

    Serial.printf( "%s", pcString );
    Serial.flush();

    // Déverrouillage du mutex
    xSemaphoreGive( mutex );
}

Les interruptions

Les routines d’interruptions (ISR) sont des fonctions exécutées par l’ESP32 lui-même et qui ne peuvent être gérées par FreeRTOS, ce qui peut poser certains problèmes.

Pour ces raisons, les routines d’interruptions ne peuvent pas utiliser les fonctions habituelles de l’API FreeRTOS que toute autre tâche basique peut utiliser. FreeRTOS définit un groupe de fonctions spécialement conçues pour les ISR. Ces fonctions destinées à être utilisées à partir des ISR ont «FromISR» ajouté à leur nom. Par exemple, une routine d’interruption ISR utilisera la fonction xQueueReceiveFromISR() à la place de xQueueReceive().

Remarque : les fonctions de base de l’API FreeRTOS effectuent des actions qui ne sont pas valides à l’intérieur d’une routine d’interruption ISR. Par exemple, si une fonction FreeRTOS (qui doit placer la tâche à l’état Bloqué) est appelée à partir d’un ISR, elle n’est pas donc appelée à partir d’une tâche et aucune tâche d’appel ne pourra être placée à l’état Bloqué.

Les interruptions différées sont une stratégie pour réduire le temps d’exécution en utilisant les Sémaphores binaires de FreeRTOS. Un Sémaphore binaire peut être utilisé afin de débloquer une tâche à chaque fois qu’une interruption particulière a lieu. Ainsi la partie de code exécutée dans l’ISR pourra être très largement réduite et la gestion de l’interruption reviendra en grande partie à la tâche débloquée. On aura ainsi différé le processus d’interruption vers une simple tâche. Si l’interruption s’avère être critique, alors la priorité de la tâche de gestion de l’interruption pourra être définie de manière à toujours préempter les autres tâches du système.

Exemple n°8 (framework Arduino) :

#include <Arduino.h>

// Brochages
#define GPIO_LED_ROUGE      5
#define GPIO_SW1            12

SemaphoreHandle_t xBinarySemaphore = NULL;

void IRAM_ATTR interruptSW1()
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;

  Serial.println("interruptSW1()");
  xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
  if( xHigherPriorityTaskWoken != pdFALSE )
    portYIELD_FROM_ISR();
}

void clignoterLedRouge(void *pvParameters)
{
  const char *pcTaskName = "Task clignoterLedRouge";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  for(;;)
  {
    xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
    Serial.printf("%s - core = %d (priorite %d)\n", pcTaskName, xPortGetCoreID(), uxPriority);
    // provoque un clignotement de la Led
    digitalWrite(GPIO_LED_ROUGE, HIGH);
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
    digitalWrite(GPIO_LED_ROUGE, LOW);
    vTaskDelay( pdMS_TO_TICKS( 500 ) );
  }
}

void vTaskPeriodic( void *pvParameters )
{
  const char *pcTaskName = "Task periodique";
  TickType_t xLastWakeTime;
  xLastWakeTime = xTaskGetTickCount();
  for( ;; )
  {
    Serial.printf("%s %d\n", pcTaskName, xPortGetCoreID());
    vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 500 ) );
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");
  infos();

  // Gestion matérielle
  pinMode(GPIO_LED_ROUGE, OUTPUT);
  pinMode(GPIO_SW1, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(GPIO_SW2), interruptSW1, FALLING);
  digitalWrite(GPIO_LED_ROUGE, LOW);

  xBinarySemaphore = xSemaphoreCreateBinary();

  xTaskCreate( vTaskPeriodic, "vTaskPeriodic", 10000, NULL, 1, NULL );
  xTaskCreate( clignoterLedRouge, "clignoterLedRouge", 10000, NULL, 1, NULL );

  for(;;); // pas de loop()
}

void loop()
{
}

void infos()
{
  esp_chip_info_t out_info;
  esp_chip_info(&out_info);
  Serial.print("CPU freq : "); Serial.println(String(ESP.getCpuFreqMHz()) + " MHz");
  Serial.print("CPU cores : ");  Serial.println(String(out_info.cores));
  Serial.print("Flash size : "); Serial.println(String(ESP.getFlashChipSize() / 1000000) + " MB");
  Serial.print("Free RAM : "); Serial.println(String((long)ESP.getFreeHeap()) + " bytes");
  //Serial.print("Min. free seen : "); Serial.println(String((long)esp_get_minimum_free_heap_size()) + " bytes");
  Serial.print("tskIDLE_PRIORITY : "); Serial.println(String((long)tskIDLE_PRIORITY));
  Serial.print("configMAX_PRIORITIES : "); Serial.println(String((long)configMAX_PRIORITIES));
  Serial.print("configTICK_RATE_HZ : "); Serial.println(String(configTICK_RATE_HZ) + " Hz");
  Serial.print("configKERNEL_INTERRUPT_PRIORITY : "); Serial.println(String((long)configKERNEL_INTERRUPT_PRIORITY));
  Serial.print("configMAX_SYSCALL_INTERRUPT_PRIORITY : "); Serial.println(String((long)configMAX_SYSCALL_INTERRUPT_PRIORITY));
  Serial.println();
}

Résultat :

Start
CPU freq : 240 MHz
CPU cores : 2
Flash size : 4 MB
Free RAM : 362324 bytes
tskIDLE_PRIORITY : 0
configMAX_PRIORITIES : 25
configTICK_RATE_HZ : 1000 Hz
configKERNEL_INTERRUPT_PRIORITY : 1
configMAX_SYSCALL_INTERRUPT_PRIORITY : 3

Task periodique 0
Task periodique 0
interruptSW1()
Task clignoterLedRouge - core = 1 (priorite 1)
Task periodique 0
Task periodique 0
Task periodique 0
interruptSW1()
Task clignoterLedRouge - core = 1 (priorite 1)
Task periodique 0
Task periodique 0
...

Il est possible aussi de suspendre les interruptions en utilisant les sections critiques de FreeRTOS (taskENTER_CRITICAL() et taskEXIT_CRITICAL()). Les sections critiques permettent de bloquer tout changement de contexte, d’opération de l’ordonnanceur ou même d’une levée d’interruption dans une portion de code appartenant à une tâche.

Temporisateurs logiciels (timers)

Les temporisateurs logiciels (timers) sont utilisés pour planifier l’exécution d’une fonction à un temps défini ou périodiquement avec une fréquence fixe. La fonction exécutée est appelée fonction de rappel (callback) du temporisateur logiciel.

Remarque : les temporisateurs logiciels sont implémentés par FreeRTOS et sont sous son contrôle. Ils ne nécessitent pas de prise en charge matérielle et ne sont pas liés aux temporisateurs ou compteurs matériels.

Les temporisateurs sont référencés par des variables de type TimerHandle_t et créés par la fonction xTimerCreate().

xTimerStart() ou xTimerStartFromISR() sont utilisés pour démarrer un temporisateur qui est à l’état Dormant, ou le redémarrer. xTimerStop() est utilisé pour arrêter un temporisateur qui est en cours d’exécution.

La fonction xTimerDelete() supprime un temporisateur. Un temporisateur peut être supprimée à tout moment.

Exemple n°9 (framework Arduino) :

#include <Arduino.h>

TimerHandle_t xAutoReloadTimer, xOneShotTimer;

void oneShotTimerCallback( TimerHandle_t xTimer )
{
  const char *name = "oneShotTimerCallback";
  TickType_t xTimeNow = xTaskGetTickCount();
  Serial.printf("%s - %d\n", name, xTimeNow);
}

void autoReloadTimerCallback( TimerHandle_t xTimer )
{
  const char *name = "autoReloadTimerCallback";
  TickType_t xTimeNow = xTaskGetTickCount();
  Serial.printf("%s - %d\n", name, xTimeNow);
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xOneShotTimer = xTimerCreate(
                    "OneShot", /* text name for the software timer - not used by FreeRTOS. */
                    pdMS_TO_TICKS( 3000 ), /* the software timer's period in ticks. */
                    pdFALSE, /* uxAutoRealod to pdFALSE creates a one-shot software timer. */
                    NULL, /* the timer id (NULL = not use). */
                    oneShotTimerCallback /* the callback function to be used by the software timer being created. */
                  );

  xAutoReloadTimer = xTimerCreate( "AutoReload", pdMS_TO_TICKS( 500 ), pdTRUE, 0, autoReloadTimerCallback );

  xTimerStart( xOneShotTimer, 0 );
  xTimerStart( xAutoReloadTimer, 0 );

  for( ;; );
}

void loop()
{
}

Résultat :

Start
autoReloadTimerCallback - 535
autoReloadTimerCallback - 1035
autoReloadTimerCallback - 1535
autoReloadTimerCallback - 2035
autoReloadTimerCallback - 2535
oneShotTimerCallback - 3035
autoReloadTimerCallback - 3035
autoReloadTimerCallback - 3535
...

Les événements

Les événements dans FreeRTOS sont utiles pour synchroniser plusieurs tâches, diffuser des événements à plusieurs tâches, permettre à une tâche d’attendre à l’état Bloqué que n’importe lequel d’un ensemble d’événements se produise et permettre à une tâche d’attendre dans le État bloqué pour l’exécution de plusieurs actions.

Un « indicateur » ou « bit » d’événement est une valeur booléenne (1 ou 0) dans une variable de type EventBits_t utilisée pour indiquer si un événement s’est produit ou non. Un groupe d’événements est un ensemble de bits d’événements. Les groupes d’événements sont référencés à l’aide de variables de type EventGroupHandle_t.

Le nombre de bits d’événement dans un groupe d’événements dépend de la constante de configuration de temps de compilation configUSE_16_BIT_TICKS dans FreeRTOSConfig.h :

  • Si configUSE_16_BIT_TICKS vaut 1, alors chaque groupe d’événements contient 8 bits d’événement utilisables.
  • Si configUSE_16_BIT_TICKS vaut 0, alors chaque groupe d’événements contient 24 bits d’événement utilisables.

La fonction xEventGroupCreate() est utilisée pour créer un groupe d’événements.

La fonction xEventGroupSetBits() ou xEventGroupSetBitsFromISR() définit un ou plusieurs bits dans un groupe d’événements et est généralement utilisée pour notifier à une tâche que les événements représentés par le ou les bits en cours de définition se sont produits.

La fonction xEventGroupWaitBits() permet à une tâche de lire la valeur d’un groupe d’événements et d’attendre éventuellement à l’état Bloqué qu’un ou plusieurs bits d’événements du groupe d’événements deviennent définis, si les bits d’événements ne sont pas déjà définis.

La condition de déblocage est spécifiée par une combinaison des valeurs des paramètres uxBitsToWaitFor et xWaitForAllBits :

  • uxBitsToWaitFor spécifie les bits d’événement du groupe d’événements à tester
  • xWaitForAllBits spécifie s’il faut utiliser un OU (pdFALSE) ou un ET (pdTRUE) au niveau des bits d’événement

Exemple n°10 (framework Arduino) :

#include <Arduino.h>

#define EVENEMENT1 (1 << 0) // bit 0
#define EVENEMENT2 (1 << 1) // bit 1

EventGroupHandle_t xEventGroup;

void vTask1( void *pvParameters )
{
  const char *pcTaskName = "Task 1 is running";
  UBaseType_t uxPriority = uxTaskPriorityGet( NULL );

  for(;;)
  {
    Serial.printf("%s (priorite %d)\n", pcTaskName, uxPriority);
    xEventGroupSetBits( xEventGroup, EVENEMENT1 );
    vTaskDelay( pdMS_TO_TICKS( 500 ) );    
  }
}

void vTask2( void *pvParameters )
{
  const char *pcTaskName = "Task 2 is running";
  UBaseType_t uxPriority = uxTaskPriorityGet( NULL );
  
  for(;;)
  {
    Serial.printf("%s (priorite %d)\n", pcTaskName, uxPriority);
    xEventGroupSetBits( xEventGroup, EVENEMENT2 );
    vTaskDelay( pdMS_TO_TICKS( 1200 ) );    
  }
}

void vTask3( void *pvParameters )
{
  const char *pcTaskName = "Task 3 is running";
  UBaseType_t uxPriority = uxTaskPriorityGet( NULL );
  EventBits_t xEventGroupValue;
  const EventBits_t bitsEvenements = ( EVENEMENT1 | EVENEMENT2 );

  for( ;; )
  {
    xEventGroupValue = xEventGroupWaitBits( xEventGroup, bitsEvenements, pdTRUE, pdFALSE, portMAX_DELAY );
    if( ( xEventGroupValue & EVENEMENT1 ) != 0 )
    {
      Serial.printf("%s - EVENEMENT1 (priorite %d)\n", pcTaskName, uxPriority);
    }
    if( ( xEventGroupValue & EVENEMENT2 ) != 0 )
    {
      Serial.printf("%s - EVENEMENT2 (priorite %d)\n", pcTaskName, uxPriority);
    }    
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  xEventGroup = xEventGroupCreate();

  xTaskCreate( vTask1, "vTask1", 10000, NULL, 1, NULL );
  xTaskCreate( vTask2, "vTask2", 10000, NULL, 2, NULL );
  xTaskCreate( vTask3, "vTask3", 10000, NULL, 3, NULL );  

  for( ;; );
}

void loop()
{
}

Résultat :

Start
Task 1 is running (priorite 1)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 3 is running - EVENEMENT2 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT2 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT2 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT2 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 1 is running (priorite 1)
Task 3 is running - EVENEMENT1 (priorite 3)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT2 (priorite 3)
...

On peut modifier la condition de déblocage xWaitForAllBits avec un ET (pdTRUE) pour les deux événements :

void vTask3( void *pvParameters )
{
  const char *pcTaskName = "Task 3 is running";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  EventBits_t xEventGroupValue;
  const EventBits_t bitsEvenements = ( EVENEMENT1 | EVENEMENT2 );

  for( ;; )
  {
    xEventGroupValue = xEventGroupWaitBits( xEventGroup, bitsEvenements, pdTRUE, pdTRUE, portMAX_DELAY );
    Serial.printf("%s - EVENEMENT1 & EVENEMENT2 (priorite %d)\n", pcTaskName, uxPriority);
  }
}

On obtient alors :

Start
Task 1 is running (priorite 1)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT1 & EVENEMENT2 (priorite 3)
Task 1 is running (priorite 1)
Task 1 is running (priorite 1)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT1 & EVENEMENT2 (priorite 3)
Task 1 is running (priorite 1)
Task 1 is running (priorite 1)
Task 2 is running (priorite 2)
Task 3 is running - EVENEMENT1 & EVENEMENT2 (priorite 3)
...

Remarque : xEventGroupSync() est fourni pour permettre à deux tâches ou plus d’utiliser un groupe d’événements pour se synchroniser entre elles.

Notifications de tâches

Les notifications de tâches sont une fonctionnalité qui peut souvent être utilisée à la place d’un sémaphore binaire, d’un sémaphore de comptage, d’un groupe d’événements et parfois même d’une file d’attente.

Le principe est d’utiliser la fonction xTaskNotify() ou son alternative xTaskNotifyGive() pour envoyer une notification de tâche et la fonction xTaskNotifyWait() ou son alternative ulTaskNotifyTake() pour recevoir une notification de tâche.

Exemple n°11 (framework Arduino) :

#include <Arduino.h>

// Brochages
#define GPIO_LED_ROUGE      5
#define GPIO_SW1            12

TaskHandle_t taskHandle = NULL;

void IRAM_ATTR interruptSW1()
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  vTaskNotifyGiveFromISR( taskHandle, &xHigherPriorityTaskWoken );
  if( xHigherPriorityTaskWoken != pdFALSE )
    portYIELD_FROM_ISR();
}

void clignoterLedRouge(void *pvParameters)
{
  const char *pcTaskName = "Task clignoterLedRouge";
  UBaseType_t uxPriority;
  uxPriority = uxTaskPriorityGet( NULL );
  uint32_t ulEventsToProcess;

  for(;;)
  {
    ulEventsToProcess = ulTaskNotifyTake( pdTRUE, pdMS_TO_TICKS( 500 ) );
    if( ulEventsToProcess != 0 )
    {
      Serial.printf("%s - notification = %d (priorite %d)\n", pcTaskName, ulEventsToProcess, uxPriority);
      while( ulEventsToProcess > 0 )
      {
        // provoque un clignotement de la Led
        digitalWrite(GPIO_LED_ROUGE, HIGH);
        vTaskDelay( pdMS_TO_TICKS( 500 ) );
        digitalWrite(GPIO_LED_ROUGE, LOW);
        vTaskDelay( pdMS_TO_TICKS( 500 ) );
        
        ulEventsToProcess--;
      }      
    }
  }
}

void setup()
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Start");

  pinMode(GPIO_LED_ROUGE, OUTPUT);
  pinMode(GPIO_SW1, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(GPIO_SW2), interruptSW1, FALLING);
  digitalWrite(GPIO_LED_ROUGE, LOW);

  xTaskCreate( clignoterLedRouge, "clignoterLedRouge", 10000, NULL, 1, &taskHandle );

  for( ;; );
}

void loop()
{
}

Résultat :

Start
Task clignoterLedRouge - notification = 1 (priorite 1)
Task clignoterLedRouge - notification = 2 (priorite 1)
...

Glossaire

Tâche
Une tâche est une unité d’exécution. Il s’agit d’un terme global qui peut être précisé par une dénomination plus spécifique tel que le processus, le processus léger (thread), fil d’exécution, …
Multitâche
Le terme de « multitâche » fait principalement référence à l’exécution de plusieurs tâches en parallèle ou en même temps.
Ordonnanceur
L’ordonnanceur (scheduler) désigne le composant du noyau d’un système d’exploitation choisissant l’ordre d’exécution des tâches.
Round-Robin
Round-robin (RR) est un algorithme d’ordonnancement courant dans les systèmes d’exploitation. Il est adapté aux systèmes travaillant en temps partagés. Une unité de temps (time quantum ou time slice) est définie. La file d’attente est gérée comme une file circulaire. L’ordonnanceur parcourt cette file et alloue un temps processeur à chacune des tâches pour un intervalle de temps de l’ordre d’un quantum au maximum. Le tourniquet d’un jeu de parcs est l’idée intuitive derrière cet algorithme.
Priorité
L’ordonnanceur (scheduler) désigne le composant du noyau d’un système d’exploitation choisissant l’ordre d’exécution des tâches.
Sémaphore
Un sémaphore est une variable entière (accessible à travers d’opérations atomiques) qui permet de restreindre l’accès à des ressources partagées et synchroniser les tâches dans un environnement de programmation multitâche.
Mutex
Un mutex (Mutual exclusion) est un mécanisme de synchronisation utilisée en programmation multitâche pour éviter que des ressources partagées d’un système ne soient utilisées en même temps (exclusion mutuelle). Avec un mutex, il est possiblme de réaliser une section critique qui est une portion de code dans laquelle il doit être garanti qu’il n’y aura jamais plus d’une tâche simultanément.

Voir aussi