Je viens de voir qu'une version de FreeRTOS existe pour les processeurs AVR qui équipent nos Arduinos. FreeRTOS est un "operating system" temps réel pour microcontrôleurs. Il est donc parfaitement adapté à des problématiques de gestion de tâches en fonction du temps ou d’occurrence d’événements.
Par contre, il ne faut pas attendre une précision à la milliseconde...
Pour l'utiliser sur Arduino, il faut d'abord installer la bibliothèque. Ça peut se faire via le gestionnaire de bibliothèques sous l'IDE. Le principe de fonctionnement est de créer des tâches indépendantes qui ont chacune une fonction. Le RTOS va allouer les ressources à la tâche qu'il juge prioritaire à chaque instant.
L'intérêt c'est qu'on peut ainsi découper son code en tâches décorrélées, qui gèrent chacune un capteur, un servo ou un moteur, un afficheur, etc et c'est FreeRTOS qui orchestrera l'ensemble. Chaque fonction est donc simplifiée car elle n'a pas à prendre en compte une contrainte ou une interaction avec une autre fonction (ou un autre périphérique).
Directement, voici un exemple de code, avec 2 tâches qui font clignoter des leds à des fréquences différentes. Le code est organisé en 4 parties :
- La déclaration de la bibliothèque,
- Le setup qui contient la déclaration des tâches,
- La loop, qui peut rester vide, car le RTOS va gérer l'exécution des tâches tout seul,
- Les tâches.
Voici le code :
#include <Arduino_FreeRTOS.h>
#ifndef LED_BUILTIN
#define LED_BUILTIN 13
#endif
// define two tasks for Blink & AnalogRead
void TaskBlink1 ( void *pvParameters );
void TaskBlink2( void *pvParameters );
unsigned long chrono;
void setup() {
Serial.begin(115200);
while (!Serial) ;
// Now set up two tasks to run independently.
xTaskCreate(
TaskBlink1
, (const portCHAR *)"Blink1" // A name just for humans
, 128 // This stack size can be checked & adjusted by reading the Stack Highwater
, NULL
, 2 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
, NULL );
xTaskCreate(
TaskBlink2
, (const portCHAR *) "Blink2"
, 128 // Stack size
, NULL
, 1 // Priority
, NULL );
// Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started.
chrono = millis();
}
void loop()
{
// Empty. Things are done in Tasks.
}
/*--------------------------------------------------*/
/*---------------------- Tasks ---------------------*/
/*--------------------------------------------------*/
void TaskBlink1(void *pvParameters) // This is a task.
{
(void) pvParameters;
// initialize digital LED_BUILTIN on pin 13 as an output.
pinMode(LED_BUILTIN, OUTPUT);
for (;;) // A Task shall never return or exit.
{
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
Serial.println("Blink1 ON");
vTaskDelay( 1000 / portTICK_PERIOD_MS ); // wait for one second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
Serial.println("Blink1 OFF");
vTaskDelay( 1000 / portTICK_PERIOD_MS ); // wait for one second
}
}
void TaskBlink2(void *pvParameters) // This is a task.
{
(void) pvParameters;
// second LED attached to pin 10
pinMode(10, OUTPUT);
for (;;) // A Task shall never return or exit.
{
digitalWrite(10, HIGH); // turn the LED on (HIGH is the voltage level)
Serial.println("\tBlink2 ON");
vTaskDelay( 2500 / portTICK_PERIOD_MS ); // wait for one second
digitalWrite(10, LOW); // turn the LED off by making the voltage LOW
Serial.println("\tBlink2 OFF");
vTaskDelay( 2500 / portTICK_PERIOD_MS ); // wait for one second
}
}
La déclaration des tâches, dans le setup, se fait avec cette instruction (et ses paramètres) :
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, NULL, tskIDLE_PRIORITY, &xHandle );
Pour nous :
xTaskCreate(
TaskBlink1
, (const portCHAR *)"Blink1" // A name just for humans
, 128 // This stack size can be checked & adjusted by reading the Stack Highwater
, NULL
, 2 // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
, NULL );
C'est un cas parmi d'autres, mais c'est le plus simple : il existe aussi les tâches statiques.
La fonction qui crée la tâches est [url=https://www.freertos.org/a00125.html]xTaskCreate[/url]
, ses arguments sont :
- Le nom de la fonction associée
- Un intitulé pour des besoins de facilitation du débug (juste une chaîne de caractères pour nous autres, pauvre humains)
- La taille de la pile associée à la tâche : c'est le nombre de variables que la tâche pourra créer (pas la taille mémoire, mais bien le nombre de variables : la taille s'obtient en multipliant ce nombre par la taille de la variable 'standard', en général un int soit 2 octets)
- Un pointeur vers les paramètres que l'on veut passer à la tâche : ici NULL, on ne passe rien
- La priorité : de 0 à 3, 0 la plus basse
- Un 'handle', c'est à dire un petit nom pour que le code puisse manipuler la tâche, ici NULL : on ne fera rien avec cette tâche
Le code de la tâche est des plus simples :
void TaskBlink1(void *pvParameters) // This is a task.
{
(void) pvParameters;
// initialize digital LED_BUILTIN on pin 13 as an output.
pinMode(LED_BUILTIN, OUTPUT);
for (;;) // A Task shall never return or exit.
{
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
Serial.println("Blink1 ON");
vTaskDelay( 1000 / portTICK_PERIOD_MS ); // wait for one second
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
Serial.println("Blink1 OFF");
vTaskDelay( 1000 / portTICK_PERIOD_MS ); // wait for one second
}
}
Il commence toujours par ces lignes, et contient une boucle infinie dans laquelle est exécuté son code. Le RTOS s'occupe de gérer cette boucle et les appels à ces instructions.
On voit que cette tâche est "autoporteuse" car elle définit le pinMode de la pin qu'elle utilise. C'est cohérent avec ce que je disais plus haut : les tâches sont indépendantes.
Notez qu'on ne met pas de delay
dans une tâche (au sens Arduino du terme) mais qu'on peut utiliser vTaskDelay
qui fera une attente de manière non bloquante pour le RTOS. La constante portTICK_PERIOD_MS
est utilisée pour convertir des millisecondes en nombre de ticks d'horloge (horloge du RTOS, pas celle du µC). Je crois qu'une tick vaut 15 ms (à vérifier).
Il est aussi possible de créer une tâche dans une fonction :
void vOtherFunction( void )
{
static uint8_t ucParameterToPass;
TaskHandle_t xHandle = NULL;
// Create the task, storing the handle. Note that the passed parameter ucParameterToPass
// must exist for the lifetime of the task, so in this case is declared static.
xTaskCreate( vTaskCode, "NAME", STACK_SIZE, &ucParameterToPass, tskIDLE_PRIORITY, &xHandle );
configASSERT( xHandle );
// Use the handle to delete the task.
if( xHandle != NULL )
{
vTaskDelete( xHandle );
}
}
Notez les différences avec le cas précédent:
- On crée un handle pour agir sur la tâche
-
configASSERT
: La sémantique de la macro configASSERT
est la même que celle de la macro standard C assert
. Une assertion est déclenchée si le paramètre passé dans configASSERT() est zéro.
-
vTaskDelete
: on tue la tâche une fois qu'on n'en a plus besoin. Notez qu'elle est créée à chaque lancement de la fonction, donc il faut la tuer à la fin sinon elle sera créée plusieurs fois et on ira dans le décor...
En plus des tâches que vous créez, il existe une autre tâche, de priorité basse : la tâche IDLE
. C'est une tâche que le RTOS exécute lorsque les autres tâches ne sont pas actives. Et, tenez-vous bien : la tâche IDLE correspond à la loop
!
L'ESP32 utilise FreeRTOS en standard, il n'est pas nécessaire d'utiliser cette bibliothèque. Les codes sont similaires, la seule différence est lié au fait que l'ESP32 a deux cœurs de processeur : on ajoute une dernier paramètre à xTaskCreate
qui est le numéro du cœur auquel on veut attribuer la tâche (0 ou 1).
Voila ce que je sais de FreeRTOS pour l'instant.