• Nico! C’est quoi le programme du jour ?
  • Heu... T’es sûr de vouloir le savoir?
  • Bah oui, ça fait trop longtemps, j’en peux plus d’attendre!
  • Ok, alors si on parlait Arduino et temps réel? Je vais te montrer comment on programme des événements récurrents de façon non bloquante!
  • ...

Une petite contextualisation me semble nécessaire. En ce moment je travaille sur une interface MIDI, pour synchroniser un microcontrôleur (Teensy) avec des instruments de musique. Pour la faire courte, la Teensy va donner un tempo en BPM, et les instruments MIDI vont se caler dessus.

La Teensy est géniale dans ce contexte, parce qu’elle peut se faire passer pour un périphérique MIDI standard et son API dispose de toute une batterie de fonctions pour envoyer des instructions MIDI à qui veut bien les écouter.

Mais pour l’instant, pas besoin de rentrer dans le protocole d’horloge midi, on va se contenter d’afficher du texte dans le moniteur série, et après si vous êtes sages, on fera clignoter des LEDs. Houlala, mais quel programme trépidant!

1. Approche naïve pour gérer un événement récurrent

Si vous êtes en train de lire ceci, il y a de forte chances pour que vous ayez testé votre première Arduino avec l’exemple blink.

//pin de sortie
int led = 13;

// Initialisation:
void setup() {                
  pinMode(led, OUTPUT);     
}

void loop() {
  digitalWrite(led, HIGH);   // Allume la LED
  delay(1000);               // attend une seconde
  digitalWrite(led, LOW);    // éteint la LED
  delay(1000);               // attend une seconde
}

Rien de choquant, on est tous passé par là :).

Affichons plutôt des messages, ce sera plus pratique pour la suite:

// Initialisation:
void setup() {                
  Serial.begin(115200); 
}

void loop() {
  Serial.println("Paf");
  delay(1000);               // attend une seconde
}

Si j'ai maintenant envie d'afficher "Paf" toutes les 500ms et "Pif" toutes les 3 secondes... Comment faire? On pourrait utiliser un compteur, mettre un delay(500), à chaque tour afficher Paf, et tous les 6 tours afficher Pif.

Ça peut vite devenir très compliqué l'histoire. Et d'autant plus que pendant l'appel à delay(), il ne se passe rien, le programme est bloqué pendant 500ms. C'est la méga-cagade si on doit faire d'autres traitements en parallèle (lecture d'entrées, affichage...).

2. Approche non bloquante

Pour remédier à cela, on doit avoir une approche non bloquante, basée sur l'heure actuelle (ou plus précisément sur le nombre de millisecondes (ou microsecondes) écoulées depuis le démarrage du programme). On va utiliser les fonction millis() ou micros(), selon la précision désirée (respectivement à la milliseconde ou à la microseconde près).

L'exemple proposé dans l'IDE Arduino est BlinkWithoutDelay, et on peut l'adapter ainsi pour notre exemple:

unsigned long previousMillisPaf = 0;
unsigned long previousMillisPif = 0;

unsigned long intervalPaf = 500;
unsigned long intervalPif = 3000;

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

void loop()
{
  //lit l'heure actuelle
  unsigned long currentMillis = millis();

  if (currentMillis > previousMillisPaf + intervalPaf) {
    previousMillisPaf = currentMillis;
    Serial.println("Paf");
  }

  if (currentMillis > previousMillisPif + intervalPif) {
    previousMillisPif = currentMillis;
    Serial.println("Pif");
  }
}

Dans ce cas, à chaque tour de loop(), on va regarder si la durée nécessaire est passée, et si ce n'est pas le cas on laisse filer (et du coup on peut faire autre chose plutôt que d'attendre bêtement).

3. Approche plus encapsulée

On est déjà bien mieux que dans le tout premier exemple, mais il y a des variables globales qui se baladent, des grosses conditions dans les if(), ce n'est pas encore très élégant.

Pour mon projet, j'ai encapsulé ce fonctionnement dans une classe C++ histoire de faciliter l'utilisation:


#ifndef DMTIMER_H
#define DMTIMER_H

#include "Arduino.h"

class DMTimer {
private: //private members

public: //public members
  //DMTimer constructor
  DMTimer();
  DMTimer(unsigned long interval);

  bool isTimeReached(unsigned long currentTime, unsigned long interval);
  bool isTimeReached(unsigned long currentTime);
  bool isTimeReached() { return isTimeReached(micros()); }

  void reset() { setLastTime(micros()); }
  void setLastTime(unsigned long time) { this->_lastTime = time; }
  void setInterval(unsigned long interval) { this->_interval = interval; }

private:
  unsigned long _lastTime = 0;
  unsigned long _interval = 0;

};

#endif //DMTIMER_H

Pour reprendre notre exemple précédent, on aurait donc:

DMTimer paf(500000); //en microsecondes
DMTimer pif(3000000); 

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

void loop()
{
  //lit l'heure actuelle
  unsigned long m = micros();

  if (paf.isTimeReached(m)) {
    Serial.println("Paf");
  }

  if (pif.isTimeReached(m)) {
    Serial.println("Pif");
  }
}

C'est plus léger. Plus de variables globales pour stocker des états dont on se fout royalement dans le programme principal. Vous noterez que l'heure (ici en microsecondes) est lue une seule fois, puis passée en paramètre aux méthodes de test. Ça permet d'éviter des décalages de temps causés par l'exécution du code qui peut être "longue" (~100µs pour un analogRead() par exemple). En faisant comme ça, on travaille sur la même référence de temps pour toutes les opérations.

4. Application : Faire clignoter une LED de façon contrôlée et non bloquante

Je dis LED, mais le concept s'applique évidemment au contrôle de moteurs pas à pas, à l'envoi de signaux d'horloge MIDI, et que sais-je encore.

Voici l'interface de la classe :

#ifndef DMOSCILLATOR_H
#define DMOSCILLATOR_H

#include "Arduino.h"
#include 

enum OscillationMode {
  duration,  // basé sur une durée de clignotement
  count,     // basé sur un nombre d'alternances
  infinite   // aucune condition d'arrêt
};

class DMOscillator {

public: 

  DMOscillator(int pin, OscillationMode mode);
  void setMode(OscillationMode mode) { this->_mode = mode; }
  void update();
  void stop();
  void oscillate(unsigned long period, unsigned long durationOrCount);
  void oscillateHz(unsigned long frequency, unsigned long durationOrCount);

  //for infinite mode, no need to pass another argument
  void oscillate(unsigned long period);
  void oscillateHz(unsigned long frequency);

};

#endif //DMOSCILLATOR_H

Si je veux faire clignoter une LED à 3Hz, pendant 5 impulsions, il me suffit d'écrire:

//déclaration:
DMOscillator led(13, BlinkMode::count);

//dans le code, pour démarrer un clignotement:
led.oscillateHz(3, 5);

//et dans la boucle principale:
led.update();

Si je veux faire 3 clignotements à 5 Hz chaque seconde, il me suffit d'écrire le programme suivant:


#include 
#include "dmoscillator.h"

DMTimer *t = NULL;
DMOscillator *osc = NULL;

void setup(){
  t = new DMTimer(1000000); //déclenchement toutes les secondes
  osc = new DMOscillator(13, OscillationMode::count); //clignotement sur la pin 13
}

void loop(){

  if(t->isTimeReached())
    osc->oscillateHz(5, 3);

  osc->update();
}

Les deux projets sont dispo sur GitHub et dans les bibliothèques Arduino avec des exemples!

https://github.com/toxnico/DMTimer

https://github.com/toxnico/DMOscillator

Article précédent Article suivant