fbpixel
Programme Multitâche avec ESP32 et FreeRTOS

Programme Multitâche avec ESP32 et FreeRTOS

Le NodeMCU ESP32 est basé sur le microprocesseur dual-core Xtensa 32-bit LX6 qui embarque l’OS FreeRTOS. Lorsqu’on utilise l’IDE Arduino, le programme s’exécute par défaut sur le cœur 1. Pour faire du multitasking, il est intéressant d’utiliser toutes les ressources du microprocesseur. Nous allons voir dans ce tutoriel comment exécuter des tâches sur les deux cœurs

Il existe une librairie FreeRTOS compatible pour les microcontrôleurs Arduino avec une architecture AVR (Uno, Nano, Mega, etc.).

Matériel

  • NodeMCU ESP32
  • Câble USB A Mâle/ Micro B Mâle

Description de l’OS FreeRTOS

FreeRTOS est un système d’exploitation temps-réel open-source et léger. Il est donc parfaitement adapté à des problématiques de gestion de tâches en fonction du temps ou d’occurrence d’événements. Espressif a intégré l’OS sur les dernières versions du SDK. Il est donc possible d’utiliser les fonctionnalité de cet OS afin d’exploiter le potentiel des deux coeurs du NodeMCU ESP32 avec l’IDE Arduino.

Code

Dans ce code, nous allons créer plusieurs tâches auxquels nous allons affecter une fonction qui s’exécutera à l’appelle de la tâche.

Création de la tâche affectée à un cœur

  xTaskCreatePinnedToCore(
                    task1Func,   /* Task function. */
                    "Task1",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    10,           /* priority of the task */
                    &Task1,      /* Task handle to keep track of created task */
                    0);          /* pin task to core 0 */ 

Définition de la boucle infinie exécuter à l’appel de la tâche

void task1Func( void * pvParameters ){
  for(;;){
    // place code here
  }
}

Il est bon de noter que la fonction delay() (ou vsTaskDelay) permet de bloquer la tâche et de passer la main à la prochaine tâche qui a la priorité la plus élevée.

TaskHandle_t Task1;
TaskHandle_t Task2;
TaskHandle_t Task3;

unsigned long previousTime,previousTime1,previousTime2,previousTime3;

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

  //create a task on core 0 that will be execute task1Func() with priority 10
  xTaskCreatePinnedToCore(
                    task1Func,   /* Task function. */
                    "Task1",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    10,           /* priority of the task */
                    &Task1,      /* Task handle to keep track of created task */
                    0);          /* pin task to core 0 */                             
  delay(500); 

  //create a task on core 1 that will be execute task2Func() with priority 9
  xTaskCreatePinnedToCore(
                    task2Func,   /* Task function. */
                    "Task2",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    9,           /* priority of the task */
                    &Task2,      /* Task handle to keep track of created task */
                    1);          /* pin task to core 1 */
    delay(500); 

    xTaskCreatePinnedToCore(task3Func, "Task3", 10000, NULL, 8, &Task3, 0);
}

void task1Func( void * pvParameters ){
  for(;;){
    Serial.printf("%s running on core %d (priorite %d) - %dms\n", "Task1", xPortGetCoreID(), uxTaskPriorityGet( NULL ),millis()-previousTime1);
    previousTime1=millis();
    delay(200);//vTaskDelay( pdMS_TO_TICKS( 200 ) );
  }
}

void task2Func( void * pvParameters ){
  for(;;){
    Serial.printf("%s running on core %d (priorite %d) - %dms\n", "Task2", xPortGetCoreID(), uxTaskPriorityGet( NULL ),millis()-previousTime2);
    previousTime2=millis();
    delay(600);
    //vTaskDelay( pdMS_TO_TICKS( 600 ) );
  }
}

void task3Func( void *pvParameters )
{
  for( ;; )
  {
    Serial.printf("%s running on core %d (priorite %d) - %dms\n", "Task3", xPortGetCoreID(), uxTaskPriorityGet( NULL ),millis()-previousTime3);
    previousTime3=millis();
    delay(100);
    //vTaskDelay( pdMS_TO_TICKS( 100 ) );
  }
}
void loop() {
  Serial.printf("%s running on core %d (priorite %d) - %dms\n", "loop()", xPortGetCoreID(), uxTaskPriorityGet( NULL ),millis()-previousTime);
  previousTime=millis();
  delay(500);
}

Ils existent plusieurs fonctions pour créer des fonctionnalités plus avancées comme la synchronisation et la communication entre tâches. Veuillez vous reporter à la documentation.

Quelques fonctions à retenir:

  • xTaskCreate() pour créer une tâche
  • xTaskCreatePinnedToCore() pour créer une tâche sur un cœur particulier
  • vTaskDelayUntil() permet d’activer un fonction périodiquement
  • vTaskDelay() bloque une tâche pendant un certain nombre de tick d’horloge (utilise pdMS_TO_TICKS pour convertir une durée en nombre de ticks)
  • vTaskPrioritySet() modifie la priorité d’une tâche
  • vTaskDelete() pour supprimer une tâche

Résultat

Une fois le programme lancé, on observe les tâches s’exécuter de manière ordonnée.

Sources

Retrouvez nos tutoriels et d’autres exemples dans notre générateur automatique de code
La Programmerie

Programme multitâche avec Arduino Due et Scheduler

Programme multitâche avec Arduino Due et Scheduler

La carte Arduino Due permet de faire du multitasking à l’aide de la librairie Scheduler. Elle est basée sur un microcontrôleur ARM Cortex-M3 en 32 Bits avec 84MHz. Elle est donc bien plus puissante qu’un Arduino UNO. Ces caractéristiques lui permettent de créer des algorithmes multitâches plus performants. Nous allons voir ici comment utiliser la librairie Scheduler qui permet d’utiliser plusieurs fonctions loop().

Matériel

  • Arduino Due
  • Câble USB A Mâle/ Micro B Mâle

Description

La librairie Scheduler permet à un microcontrôleurs basé sur une Architecture SAM, comme Arduino Due ou l’Arduino Zero, d’effectuer plusieurs actions sans s’interrompre. La libraire permet d’agencer les tâches afin que le microprocesseur passe de l’une à l’autre sans avoir à créer de timer dédié. Tout comme les autres microcontrôleurs Arduino, l’Arduino Due ne permet pas de faire du vrai multithreading mais, grâce à sa fréquence d’horloge, elle donne l’impression d’exécuter les tâches en parallèle.

Code

Dans ce code, nous allons créer plusieurs fonction loop qui vont pouvoir s’exécuter en parallèle. Pour initialiser chacune des fonctions loop nous utilisons la commande startLoop:

  Scheduler.startLoop(loop2);
  Scheduler.startLoop(loop3);

Il est bon de noter que la fonction delay() permet à une autre tâche de s’activer. Lorsqu’il n’y a pas de fonction delay dans une des fonctions loop, il faut alors utiliser la fonction yield() afin de passer la main à la tâche suivante.

#include <Scheduler.h>

int counter=0;

void setup() {
  Serial.begin(9600);

  Scheduler.startLoop(loop2);
  Scheduler.startLoop(loop3);
}

void loop() {
  Serial.println(F(" Action 1 LED : HIGH"));
  digitalWrite(13, HIGH);
  delay(500);
  Serial.println(F(" Action 1 LED : LOW"));
  digitalWrite(13, LOW);
  delay(500);
}

void loop2() {
  if(counter<25) {
    counter++;
    Serial.print(F(" Action 2 counter : "));Serial.println(counter);
  }
  
  delay(100);
}

void loop3() {
  if(counter>=25){
    
    for(counter;counter>=0;counter--){
      Serial.print(F(" Action 3 Reset counter : "));Serial.println(counter);
    }
  }
  yield();
}

L’Arduino Due et la libraire nous permettent de créer simplement des fonctions qui vont tourner en parallèle et qui ne nécessite pas de gestion supplémentaire de l’ordonnancement temps-réel des tâches comme la création de timers.

Résultat

Une fois le programme lancé, on observe les tâches s’exécuter de manière ordonnée.

Une fois ce code fonctionnel, vous pouvez le modifier pour qu’il corresponde à votre besoin. La librairie Scheduler permet, par exemple, d’affecter une fonction loop à la gestion des capteurs, des moteurs ou encore à la communication série.

Sources

Retrouvez nos tutoriels et d’autres exemples dans notre générateur automatique de code
La Programmerie

Faire du multitâche avec Arduino

Faire du multitâche avec Arduino

Le multitâche ou multitasking est la capacité d’un microcontrôleur d’exécuter plusieurs tâches ou process sur le même horizon de temps. En pratique, un Arduino ne peut pas exécuter de tâches en parallèle mais il peut agencer et exécuter une partie des tâches les unes à la suite des autres dans un laps de temps très court. Ceci donne l’illusion du multitâche. Nous allons voir dans cette article quelques exemples de méthode dite de multitâche.

S’il est nécessaire pour votre projet d’exécuter des tâches en parallèle (multithreading), il faudra vous orienter vers d’autres microcontrôleurs ou micro-ordinateurs.

Utilisation des interruptions

Description

Les interruptions permettent au microcontrôleur d’exécuter une fonction lorsqu’un évènement survient sur une des broches d’interruption. Plutôt que de lire constamment la valeur d’un capteur, le programme ne va se déclencher que lorsque la valeur du capteur va changer. Cela permet de résoudre bon nombre de problème d’agencement de tâche.

Code

const byte interruptPin = 2;

void setup() {
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), onEvent, CHANGE);
}

void loop() {
}

void onEvent() {
  Serial.println(F("Action"));
}

Utilisation de la fonction millis()

Description

La fonction millis() retourne une variable de type unsigned long représentant le nombre de millisecondes écoulées depuis la mise sous tension du microcontrôleur. Elle permet notamment d’effectuer une action lorsque un intervalle de temps est écoulée sans bloquer le reste du programme.

Code

unsigned long currentTime=0;
unsigned long previousTime=0;
bool ledState=LOW;

void setup() {
  Serial.begin(9600);
}

void loop() {
  currentTime=millis();
  if((currentTime-previousTime)>200){
    previousTime=currentTime;
    Serial.println(F("Action "));
  }
}

Utilisation des Timers intégrés du microprocesseur

Les timers sont des registres du microcontrôleur qui s’incrémente à la réception d’une impulsion de l’horloge.

Utilisation des librairies Timer

Les librairies de gestions du temps en Arduino sont multiples . En règle générale, elle permettent de s’interfacer aux registres timer de l’Arduino. Pour citer les plus connues: TimerOne.h MsTimer2.h, TimerThree.h

Utilisation de librairie de gestions des tâches

Ces librairies permettent d’exécuter des tâches de manière ordonnées ce qui donne l’impression de faire du multithreading.

Quelques exemples : TaskManager, ProcessScheduler, ArduinoThread ou encore FreeRTOS. Retrouver ces librairies dans la liste référence d’Arduino.

Multitâche avec la carte Arduino Due

Pour les cartes basées sur une architecture SAM comme l’Arduino DUE, il existe une librairie qui permet de gérer plusieurs tâches dans des fonctions loop() différentes. Ceci fonctionne aussi pour les cartes Zero, MKRZero et MKR1000

Microcontrôleur multicœur: ESP32

Le microcontrôleur NodeMCU ESP32 est muni d’un microprocesseur avec deux cœurs et utilise l’OS FreeRTOS ce qui lui permet d’exécuter des tâches en parallèle.

Multithreading avec Raspberry Pi

Le Raspberry Pi est un micro-ordinateur, de ce fait il est capable d’exécuter plusieurs tâches en même temps. De plus, son microprocesseur est muni de quatre cœurs ce qui lui permet de faire du vrai multitâche. Vous pouvez, par exemple, lancer plusieurs scripts Python ou programmes C++ (ou autres) en même temps. Il est aussi possible d’utiliser les librairies de multithreading au sein d’un script Python

Utilisation des Timers de l’Arduino

Utilisation des Timers de l’Arduino

L’utilisation des timers en Arduino est une méthode avancées permettant d’exécuter du code sans perturber le reste du programme. Ils permettent notamment d’activer des fonctions à des intervalles de temps précis. Les timers sont utilisées dans bon nombre de librairie de manière complètement transparente pour l’utilisateur (millis(), Servo.h, PWM, etc.)

Dans certains cas, il est possible d’utiliser des librairies qui configure les timers qui simplifierons l’utilisation.

Matériel

  • Arduino UNO
  • Câble USB A/ USB B

Description

Les timers sont des registres du microprocesseur qui s’incrémente en fonction des impulsions d’horloge ou d’impulsions extérieures. Il est possible de les configurer pour modifier leur comportement.

Le microprocesseur de l’Arduino UNO (ATmega328P) possède 3 timers:

  • timer0 (8 bits) compte de 0 à 256 et commande la PWM des broches 5 et 6. Il est aussi utilisé par les fonctions delay(), millis() et micros().
  • timer1 (16 bits) compte de 0 à 65535 et est utilisé pour la commande PWM des broches 9 et 10. Il est utilisé également par la libriaire Servo.h
  • timer2 (8 bits) qui est utilisé par la fonction Tone() et la génération de la PWM sur les broches 3 et 11.

Configuration des Timers

La partie la plus compliqué de l’utilisation du capteur est leur configuration. Un timer est configuré à l’aide de son registre de controle. Voici le rappel des registre.

Timer 0Timer 1Timer 2Rôle
TCNT0TCNT1LTCNT2Timer (bit 0 à 7)
TCNT1HTimer (bit 8 à 15)
TCCR0ATCCR1ATCCR2ARegistre de contrôle
TCCR0BTCCR1BTCCR2BRegistre de contrôle
TCCR1CRegistre de contrôle
OCR0AOCR1ALOCR2AOutput Compare (bit 0 à 7)
OCR1AHOutput Compare (bit 8 à 15)
OCR0BOCR1BLOCR2BOutput Compare (bit 0 à 7)
OCR1BHOutput Compare (bit 8 à 15)
ICR1LInput Capture (bit 0 à 7)
ICR1HInput Capture (bit 8 à 15)
TIMSK0TIMSK1TIMSK2Interrupt Mask
TIFR0TIFR1TIFR2Interrupt Flag

Le registre TCNTx contient la valeur du timer/counter, le registre TCCRxA et TCCRxB sont les registres de contrôles. OCRxA rt OCRxB contiennent les valeurs de registre à comparer. TIMSKx contient le masque d’interruption et TIFRx, quant à lui, contient les flags d’activation.

Code utilisant le timer2

Pour cette exemple, nous utilisons le timer2 qui est codé sur 8bits (256). Il nous faut donc introduire un compteur supplémentaire.

unsigned long elapsedTime, previousTime;

void onTimer() {
  static boolean state = HIGH;
  elapsedTime=millis()-previousTime;
  Serial.print(F("Set LED 13 : "));
  if(state){
    Serial.print(F("ON"));
  }else{
    Serial.print(F("OFF"));
  }
  Serial.print(F(" - "));Serial.print(elapsedTime);Serial.println(F("ms"));
  digitalWrite(13, state);
  state = !state;

  previousTime=millis();
}

void setup(){
  Serial.begin(9600);
  pinMode(13, OUTPUT);

  cli();           // disable all interrupts
  TCCR2A = (1<<WGM21)|(0<<WGM20); // Mode CTC
  TIMSK2 = (1<<OCIE2A); // Local interruption OCIE2A
  TCCR2B = (0<<WGM22)|(1<<CS22)|(1<<CS21); // Frequency 16Mhz/ 256 = 62500
  OCR2A = 250;       //250*125 = 31250 = 16Mhz/256/2
  sei();             // enable all interrupts

}
ISR(TIMER2_COMPA_vect){         // timer compare interrupt service routine
  static int counter=0;
  if (++counter >= 125) { // 125 * 4 ms = 500 ms
    counter = 0;
    onTimer();
  }
}

void loop(){
}

Code utilisant le timer1

Pour cette exemple, nous allons utiliser le timer1

unsigned long elapsedTime, previousTime;

void onTimer() {
  static boolean state = HIGH;
  elapsedTime=millis()-previousTime;
  Serial.print(F("Set LED 13 : "));
  if(state){
    Serial.print(F("ON"));
  }else{
    Serial.print(F("OFF"));
  }
  Serial.print(F(" - "));Serial.print(elapsedTime);Serial.println(F("ms"));
  digitalWrite(13, state);
  state = !state;

  previousTime=millis();
}

void setup(){
  Serial.begin(9600);
  pinMode(13, OUTPUT);

  cli();           // disable all interrupts
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A = 31250;            // compare match register 16MHz/256/2 = 31250
  TCCR1B = (1 << WGM12);   // CTC mode
  TCCR1B = (1 << CS12);    // // Frequency 16Mhz/ 256 = 62500 
  TIMSK1 = (1 << OCIE1A);  // Local interruption OCIE1A
  sei();             // enable all interrupts
}

ISR(TIMER1_COMPA_vect){          // timer compare interrupt service routine
  onTimer();
}

void loop(){
}

N.B. : Nous avons rajouté une mesure du temps écoulé à titre d’illustration mais il faut faire attention lorsqu’on utilise des fonctions basées sur les interruptions dans les fonctions appelées par des interruptions. Il peut y avoir des interférences.

Résultat

Une fois le programme lancé, la LED embarquée s’allume et s’éteint toutes les 500ms

Sources

Retrouvez nos tutoriels et d’autres exemples dans notre générateur automatique de code
La Programmerie

Programme multitâche avec Arduino et la librairie FlexiTimer2

Programme multitâche avec Arduino et la librairie FlexiTimer2

La librairie Arduino FlexiTimer2 est une librairie qui permet d’activer des fonctions à des intervalles de temps réguliers. Elle permet, à ce titre, de faire des programmes multitâche (ou multitasking) avec des microcontrôleurs Arduino. Cette méthode est utile lorsque vous souhaitez actionner deux moteurs en parallèle de manière indépendante.

Matériel

  • Arduino UNO
  • Câble USB A/ USB B

Description

La librairie FlexiTimer2 est basée sur la librairie MsTimer2. Elle contient un ensemble de fonctions qui permettent de s’interfacer avec le registre timer2 du microprocesseur ATmega de l’Arduino. Cette librairie, d’un usage similaire à une structure avec la fonction millis(), appelle une fonction lorsque l’intervalle de temps spécifié est écoulé.

L’intérêt de cette librairie par rapport à MsTimer2 est qu’il est possible de modifier la résolution de comptage, c’est à dire, la fréquence à laquelle le compteur s’incrémente.

Code

Dans ce code, nous allons créer une fonction définie de type void onTimer, qui va nous permettre de changer l’état de la LED embarquée de l’Arduino. Pour initialiser le timer, nous utilisons la ligne:

FlexiTimer2::set(500, onTimer);

Et pour l’activer, la commande:

FlexiTimer2::start();
#include <FlexiTimer2.h>
unsigned long elapsedTime, previousTime;

void onTimer() {
  static boolean state = HIGH;
  elapsedTime=millis()-previousTime;
  Serial.print(F("Set LED 13 : "));
  if(state){
    Serial.print(F("ON"));
  }else{
    Serial.print(F("OFF"));
  }
  Serial.print(F(" - "));Serial.print(elapsedTime);Serial.println(F("ms"));
  digitalWrite(13, state);
  state = !state;

  previousTime=millis();
}

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);

  FlexiTimer2::set(500, onTimer); // equivalent to FlexiTimer2::set(500, 1/1000, onTimer);
  FlexiTimer2::start();
}

void loop() {
}

N.B. : Nous avons rajouté une mesure du temps écouler à titre d’illustration mais il faut faire attention lorsqu’on utilise des fonctions basées sur les interruptions dans les fonctions appelées par des interruptions. Il peut y avoir des interférences.

Résultat

Une fois le programme lancé, la LED embarquée s’allume et s’éteint toutes les 500ms

Sources

Retrouvez nos tutoriels et d’autres exemples dans notre générateur automatique de code
La Programmerie