>>>> Télécharger le code source de la librairie sur GitHub <<<<
RunLoop est une librairie C++ open source développée dans le cadre de mon projet de nouvelle électronique à base de Arduino pour ma monture Takahashi EM10 USD.
-Arduino Only-
Librairie RunLoop: a small step for Arduino...

L'une des limitations ennuyeuse des Arduinos c'est qu'ils ne sont pas capables d'exécuter plusieurs tâches de manière réellement parallèle (ni multi-tâche, ni multi-thread). Il faut donc ruser avec la simple boucle d'exécution loop() et quelques subterfuges.
Dans cette idée, Run Loop est une librairie C++ Open Source me servant de couteau suisse pour paralléliser le traitement des informations sur Arduino. Son code constitue la colonne vertébrale de mon projet d'astronomie et j'ai donc décidé d'en faire une librairie à part entière pour le partage avec d'autres projets.
Dans cette idée, Run Loop est une librairie C++ Open Source me servant de couteau suisse pour paralléliser le traitement des informations sur Arduino. Son code constitue la colonne vertébrale de mon projet d'astronomie et j'ai donc décidé d'en faire une librairie à part entière pour le partage avec d'autres projets.

Voici, dans les grandes lignes, les possibilités offertes par la bibliothèque:
- Boucles d'exécution hiérarchiques avec gestion d'état.
- Timers logiciels.
- Timers matériels: Timer0, Timer1, Timer2, Timer3, Timer4, Timer5 (3,4,5 dispos uniquement sur Arduino Mega).
- Timers logiciels et matériels parfaitement interchangeables.
- Interruptions matérielles (se référer à la documentation de votre Arduino pour connaitre les entrées exploitables).
- Approche 100% C++ avec paradigme de délégation pour les notifications asynchrones.
- Compatibilité C avec notifications par fonctions C améliorées.
RunLoopObject: la pierre angulaire qui tourne rond.

Nous avons évoqué dans l'introduction cette fameuse fonction C nommée loop() que le Arduino passe sont temps à relancer sans arrêt. Le concept est toujours un peu le même: on vérifie la valeur de certaines entrées (capteur de température, données disponibles sur une liaison série, état d'un bouton poussoir, etc), on exécute des actions en conséquence et la boucle recommence. Sur un projet simple pas de problème mais si les choses se compliquent il peut devenir intéressant de disposer d'une gestion plus structurée en C++.
Et cela tombe bien: la classe RunLoopObject est faite pour ça. Chaque instance RunLoopObject dispose de sa propre méthode de boucle loop(). On peut lui adjoindre d'autres RunLoopObject "enfants" avec la méthode addToRunLoop(RunLoopObject *object). Il est ainsi facile de créer une hiérarchie d'exécution structurée complexe.
Et cela tombe bien: la classe RunLoopObject est faite pour ça. Chaque instance RunLoopObject dispose de sa propre méthode de boucle loop(). On peut lui adjoindre d'autres RunLoopObject "enfants" avec la méthode addToRunLoop(RunLoopObject *object). Il est ainsi facile de créer une hiérarchie d'exécution structurée complexe.

A noter que si une instance de RunLoopObject est suspendue (Idle), aucune boucle loop() de ses enfants ne sera exécutée. En d'autres termes, l'état de l'objet qui a la plus haute hiérarchie est prioritaire.
Voici la liste des méthodes publiques intéressantes à connaître pour manipuler RunLoopObject:

La compréhension de ces méthodes est relativement simple. Mais revenons juste un instant sur smartDelay(…) dont la fonction est, me semble-t-il, intéressante. Pour obtenir un fonctionnement presque "multi-thread" avec un Arduino, il y a une règle très importante: proscrire au maximum l'usage d'attentes bloquantes telles que la fonction C delay(…). Ok, sur le principe, on est tous d'accord mais dans les faits on a parfois besoin d'attendre à un moment dans son code. Alors on fait quoi? Et bien la méthode smartDelay(…) offre un début de réponse. Voici un exemple de code tiré du projet star_wars disponible avec la librairie...

Dans ce projet, j'ai créé un objet AppController héritant de RunLoopObject et j'ai ajouté en autre à son "run loop" un buzzer pour jouer de la musique. J'ai ensuite surchargé la méthode loop() -cf capture ci-dessus- afin d'écrire un texte très spirituel pour l'exemple. L'usage de smartDelay() me permet d'attendre une seconde avant de reboucler. La subtilité c'est que cette méthode va passer son temps à exécuter les boucles de tous ses fils tant que le délai n'est pas achevé plutôt que de ne rien faire. Dans le cas présent, le projet joue les notes d'une mélodie bien connue… Vous avez compris l'idée? Alors qu'un delay() classique aurait bloqué la mélodie, ici l'application va continuer à jouer la symphonie de manière parfaitement asynchrone.
RunLoopTimer: tic-tac… DRRRIIINNNGG!!!

Lancer des actions de manière périodique est souvent un besoin récurrent. La classe RunLoopTimer est là pour ça dès lors que nous n'avons pas de grosses contraintes temps réel. Cette classe hérite de RunLoopObject à ceci près que son loop() a été surchargé pour exécuter, après un délai défini, l'une des actions suivantes:
- l'appel à la méthode surchargée fire() d'une classe héritant de RunLoopTimer.
- l'appel à une fonction de callback C attachée via la méthode attachInterrupt(void (*fire)(RunLoopTimer*)) d'une instance de RunLoopTimer.
- l'appel à la méthode fire(RunLoopTimer *sender) d'une classe déléguée qu'on aura attribué à une instance de RunLoopTimer.
Choix 1: la surcharge.
Nous sommes ici dans un cas courant bien connu des développeurs C++. Il nous suffit de créer une sous-classe de RunLoopTimer puis de surcharger la méthode virtuelle void fire() pour y insérer le code à exécuter de manière périodique.
Choix 2: le callback C.
Le fameux callback C. C'est ce que font toutes les librairies Arduino ayant besoin de lancer un évènement de manière asynchrone. Il y a néanmoins une subtilité de taille avec la librairie RunLoop: je peux enfin passer un paramètre à la fonction de callback.

La fonction callback prend un argument unique de type RunLoopTimer. Il s'agit en fait du "sender": le timer émettant le callback.
Choix 3: la délégation.
La délégation a sans nulle doute ma préférence pour son élégance et son côté pratique. Mais kezako?!? Le principe est simple: on fait faire le travaille à quelqu'un d'autre. C'est un paradigme très répandu dans certains langages comme l'Objective-C par exemple. L'idée est de créer un protocole que doit respecter la classe à qui sera délégué le travail. C'est une sorte de "cahier des charges" pour l'exécutant.
Le C++ n'intègre pas à proprement parler la notion de protocole comme l'Objective-C. Nous utilisons en lieu et place une classe abstraite qui fait parfaitement l'affaire. Cette classe abstraite se contente de déclarer les méthodes à implémenter. Pour rester concret voici ce que cela donne avec le protocole de délégation RunLoopTimerDelegate qui n'a en fait besoin que d'une méthode:
Le C++ n'intègre pas à proprement parler la notion de protocole comme l'Objective-C. Nous utilisons en lieu et place une classe abstraite qui fait parfaitement l'affaire. Cette classe abstraite se contente de déclarer les méthodes à implémenter. Pour rester concret voici ce que cela donne avec le protocole de délégation RunLoopTimerDelegate qui n'a en fait besoin que d'une méthode:

Côté classe RunLoopTimer, nous disposons de deux méthodes getter/setter:

A l'usage, c'est on ne peut plus simple: la classe qui souhaite "piloter" le timer doit simplement hériter de RunLoopTimerDelegate et, du coup, elle doit impérativement implémenter la méthode virtuelle. Voici ce que cela peut donner (projet software_timer_callback):

Lors de la création du timer, on peut alors appeler timer->setTimerDelegate(this) et c'est bouclé. Lorsque le timer se déclenche, il appelle comme un grand la méthode fire(RunLoopTimer *sender) de notre AppController. Vous noterez au passage que comme pour la méthode de callback, nous disposons de l'émetteur (sender). Une même classe contrôleur peut ainsi très facilement piloter plusieurs timers.
Le gros avantage de la délégation, c'est que c'est très rapide et pratique à implémenter. On a pas besoin de créer une nouvelle classe par héritage et la classe contrôleur a tout loisir d'utiliser d'autres objets (ex: le timer doit mettre à jour un lcd lui même gérer par le contrôleur). Si vous ne connaissiez pas ce paradigme, je vous invite vivement à prendre le temps d'appréhender ce concept en regardant les projets d'exemple de la librairie.
Le gros avantage de la délégation, c'est que c'est très rapide et pratique à implémenter. On a pas besoin de créer une nouvelle classe par héritage et la classe contrôleur a tout loisir d'utiliser d'autres objets (ex: le timer doit mettre à jour un lcd lui même gérer par le contrôleur). Si vous ne connaissiez pas ce paradigme, je vous invite vivement à prendre le temps d'appréhender ce concept en regardant les projets d'exemple de la librairie.
RunLoopHardwareTimer: les timers des boss. ;)

Les timers hardware fonctionnent exactement sur le même modèle (du moins côté utilisateur des classes) que les timers software. Et pour cause puisqu'ils héritent de RunLoopTimer. En d'autres termes, changez simplement un:
RunLoopTimer *myTimer = new RunLoopTimer();
en
RunLoopTimer *myTimer = new RunLoopHardwareTimer2();
Et le tour est joué! Vous utilisez maintenant le Timer2 matériel fonctionnant par interruption.
Les timers matériels ont pour principal intérêt leur régularité du fait d'un mécanisme de comptage des cycles indépendant aboutissant à une interruption exécutant le code cible. Cependant leur nombre est très limité (Timer0, Timer1, Timer2 pour les Arduinos "classic" et à cela s'ajoute les Timer3, Timer4 et Timer5 pour le Arduino Mega). Il est donc primordial de les utiliser de manière bien ciblée dans un projet.
Concernant le Timer0, l'usage de celui-ci est un peu plus limité. Il est en fait déjà utilisé par le Arduino pour les mécanismes de comptage du temps (millis(), etc) et son horloge est calée sur une milliseconde. Sa précision dans le cadre de RunLoopHardwareTimer0 ne descend donc pas sous la milliseconde contrairement aux autres timers matériels qui utilisent la microseconde comme unité.
Pour les autres timers matériels pas de problème particulier. On dispose des classes:
Note particulière aux timers hardware: contrairement au timers logiciels, il n'est pas obligatoire de les ajouter à un RunLoopObject ou de lancer périodiquement leur méthode loop() puisqu'ils n'utilisent pas le même mécanisme. On peut donc les utiliser de manière totalement autonome.
RunLoopTimer *myTimer = new RunLoopTimer();
en
RunLoopTimer *myTimer = new RunLoopHardwareTimer2();
Et le tour est joué! Vous utilisez maintenant le Timer2 matériel fonctionnant par interruption.
Les timers matériels ont pour principal intérêt leur régularité du fait d'un mécanisme de comptage des cycles indépendant aboutissant à une interruption exécutant le code cible. Cependant leur nombre est très limité (Timer0, Timer1, Timer2 pour les Arduinos "classic" et à cela s'ajoute les Timer3, Timer4 et Timer5 pour le Arduino Mega). Il est donc primordial de les utiliser de manière bien ciblée dans un projet.
Concernant le Timer0, l'usage de celui-ci est un peu plus limité. Il est en fait déjà utilisé par le Arduino pour les mécanismes de comptage du temps (millis(), etc) et son horloge est calée sur une milliseconde. Sa précision dans le cadre de RunLoopHardwareTimer0 ne descend donc pas sous la milliseconde contrairement aux autres timers matériels qui utilisent la microseconde comme unité.
Pour les autres timers matériels pas de problème particulier. On dispose des classes:
- RunLoopHardwareTimer1 qui compte sur 16 bits.
- RunLoopHardwareTimer2 qui compte sur 8 bits (un peu moins précis).
- RunLoopHardwareTimer3 qui compte sur 16 bits (Arduino Mega uniquement).
- RunLoopHardwareTimer4 qui compte sur 16 bits (Arduino Mega uniquement).
- RunLoopHardwareTimer5 qui compte sur 16 bits (Arduino Mega uniquement).
Note particulière aux timers hardware: contrairement au timers logiciels, il n'est pas obligatoire de les ajouter à un RunLoopObject ou de lancer périodiquement leur méthode loop() puisqu'ils n'utilisent pas le même mécanisme. On peut donc les utiliser de manière totalement autonome.
Interruptions:

Dernier morceau pour boucler la boucle: les interruptions. L'idée est qu'une entrée du Arduino puisse stopper le programme lors d'un changement d'état. Une portion de code est alors exécutée puis le programme reprend son cours. Cette fonctionnalité peut s'avérer particulièrement intéressante si l'on ne souhaite pas louper un changement d'état comme avec un encodeur par exemple.
A ce stade, il est important de noter que les interruptions ne sont pas disponibles sur toutes les entrées/sorties. Cela dépend du Arduino utilisé.
A ce stade, il est important de noter que les interruptions ne sont pas disponibles sur toutes les entrées/sorties. Cela dépend du Arduino utilisé.
Liste de interruptions disponibles en fonction du modèle Arduino:
Board
Uno, Nano, Mini, other 328-based
Micro, Leonardo, other 32u4-based
Zero
Due
101
Uno, Nano, Mini, other 328-based
Mega, Mega2560, MegaADK |
Zero
MKR1000 Rev.1 |
101
Digital Pins Usable For Interrupts
2, 3
2, 3, 18, 19, 20, 21
0, 1, 2, 3, 7
all digital pins, except 4
0, 1, 4, 5, 6, 7, 8, 9, A1, A2
all digital pins
all digital pins
2, 3
2, 3, 18, 19, 20, 21
0, 1, 2, 3, 7
all digital pins, except 4
0, 1, 4, 5, 6, 7, 8, 9, A1, A2
all digital pins
all digital pins
L'instantiation d'une interruption prend deux paramètres:
- le mode de l'entrée: INPUT ou INPUT_PULLUP.
- le mode de l'interruption: LOW, CHANGE, RISING,FALLING, HIGH (Arduino Due, Zero, MKR1000 uniquement).

Les interruptions bénéficient des mêmes mécanismes d'héritage, de délégation et de callback que les timers pour les notifications asynchrones. Seul le protocole du delegate change pour RunLoopInterruptDelegate:

Je ne reviens pas plus dessus et vous invite à vous référer au paragraphe RunLoopTimer pour les trois différentes approches possibles.
Afin de couvrir tous les cas de figure, la librairie dispose de 20 classes dédiées. Une pour chaque entrée potentiellement exploitable soit:
La classe RunLoopInterrupt est une classe abstraite qui n'a pour unique raison d'être que de factoriser le code commun. Elle n'est donc pas instanciable.
Note particulière aux interruptions: il n'est pas obligatoire de les ajouter à un RunLoopObject ou de lancer périodiquement leur méthode loop() puisqu'elles n'utilisent pas le même mécanisme.
- RunLoopInterruptPin0
- RunLoopInterruptPin1
- RunLoopInterruptPin2
- RunLoopInterruptPin3
- RunLoopInterruptPin4
- RunLoopInterruptPin5
- RunLoopInterruptPin6
- RunLoopInterruptPin7
- RunLoopInterruptPin8
- RunLoopInterruptPin9
- RunLoopInterruptPin10
- RunLoopInterruptPin11
- RunLoopInterruptPin12
- RunLoopInterruptPin13
- RunLoopInterruptPinA0
- RunLoopInterruptPinA1
- RunLoopInterruptPinA2
- RunLoopInterruptPinA3
- RunLoopInterruptPinA4
- RunLoopInterruptPinA5
La classe RunLoopInterrupt est une classe abstraite qui n'a pour unique raison d'être que de factoriser le code commun. Elle n'est donc pas instanciable.
Note particulière aux interruptions: il n'est pas obligatoire de les ajouter à un RunLoopObject ou de lancer périodiquement leur méthode loop() puisqu'elles n'utilisent pas le même mécanisme.
Pour conclure: Veni, vidi, vici.

L'air de rien RunLoop répond à des besoins récurrents de nombreux projets Arduino. Perso, ce que j'aime le plus c'est la facilité d'interchanger les timers (très utile par exemple en cas de conflit de timers hardware lorsqu'on inclut certaines librairies) et la facilité de structurer hiérarchiquement un projet avec RunLoopObject.
En voici une illustration concrète dans mon projet: buzzer, led, télécommande infra rouge, écran LCD et GPS fonctionnant de concert...
J'espère que cet article détaillé vous aura donné envie d'aller plus loin et de jouer avec ce couteau Suisse pour vos propres projets. N'hésitez pas à consulter les exemples disponibles avec la librairie.
En voici une illustration concrète dans mon projet: buzzer, led, télécommande infra rouge, écran LCD et GPS fonctionnant de concert...
J'espère que cet article détaillé vous aura donné envie d'aller plus loin et de jouer avec ce couteau Suisse pour vos propres projets. N'hésitez pas à consulter les exemples disponibles avec la librairie.

- Et c'est déjà pas mal... -