> Home > Français > Arduino > Librairies > jm_Scheduler
Cette librairie est le fruit d'un questionnement lors d'un concours de robotique: Comment gérer et contrôler des processus parallèles et indépendants avec Arduino?
Sans librairie additionnelle, la réponse est assez complexe et nécessite une programmation délicate
et coûteuse en effort de programmation: Chaque processus doit effectuer sa propre gestion cyclique
dans la fonction loop()
.
La réponse professionnelle serait sans doute l'usage de RTOS
avec le modèle multitâche préemptif
.
Cela induit une complexité de programmation avec les librairies non-réentrantes d'Arduino
ou avec la taille de stack très petite des Arduino Uno, Leonardo, etc....
Au final, le mode multitâche coopératif
est bien plus approprié,
à la manière de nombreux logiciels de programmation d'applications modernes
comme NodeJS
, Python
, Lua
...
En plus d'offrir une gestion multitâche coopératif
presque native,
ces logiciels offrent également une gestion d'événements
.
Générer des événements
, on peut le faire également avec la librairie jm_Scheduler
.
jm_Scheduler
gère des coroutines
activées et réactivées à la manière de la fonction JavaScript setInterval()
,
avec des fonctionnalités supplémentaires:
jm_Scheduler
démarre immédiatement la coroutine
et la répète périodiquement.coroutine
peut être changée dynamiquement.coroutine
peut être stoppée et puis redémarrée ultérieurement.coroutine
peut être réveillée par une routine d'interruption.coroutine
est associé un scheduler
.schedulers
n'est limité que par la mémoire à disposition.
La librairie officielle Scheduler
ne fonctionne que pour les processeurs SAM et SAMD (Arduino Due et Zero) et
planifie les tâches avec le modèle préemptif
.
jm_Scheduler
planifie les tâches avec le modèle coopératif
POUR TOUS LES MODÈLES D'ARDUINO, avec plus de fonctionnalités et
avec une gestion centralisée du temps des processus.
La fonction yield()
qui suspend une tâche est implémentée.
La fonction startLoop()
de la librairie officielle Arduino n'est pas implémentée
car elle n'est pas appropriée au principe du multitâche coopératif
.
jm_Scheduler
exécute les coroutines
séquentiellement sur le stack du processeur.
Les règles de yield
et resume
sont les suivantes:
yield
intervient immédiatement lorsque une coroutine
prend fin (return
final).coroutine
sera redémarrée cycliquement selon la valeur de répétition.
yield
peut être déclenché par l'appel de la fonction yield()
.coroutine
sera résumée dès que plus aucune coroutine
ne demande une exécution immédiate.
coroutines
doivent être implémentées globalement
ou à l'intérieur des coroutines
en accolant le mot-clé static
à la déclaration de la variable.
// This example schedules a coroutine every second
#include <jm_Scheduler.h>
jm_Scheduler scheduler;
void coroutine()
{
Serial.print('.');
}
void setup(void)
{
Serial.begin(9600);
scheduler.start(coroutine, TIMESTAMP_1SEC); // Start coroutine() immediately
// and repeat it every second
}
void loop(void)
{
yield();
}
Clock1.ino
.
Cet exemple démontre les avantages de démarrer immédiatement une coroutine
d'affichage du temps avec une répétition périodique.
Clock2.ino
et Clock3.ino
qui présentent d'autres idées de comptage du temps.
Clock4.ino
présente une autre technique utile de jm_Scheduler
:
Changer dynamiquement de fonction de coroutine
.
Beat1.ino
et Beat2.ino
présentent les interactions possibles entre 2 coroutines
.
Wakeup1.ino
démontre l'interaction possible entre une routine d'interruption
et une coroutine
, et en implémentant un timeout
dans une gestion d'événement.
timestamp
(référence de temps) est généré par la fonction Arduino micros()
.
micros()
d'un UNO ou Leonardo fonctionnant à 16MHz
retourne un timestamp
avec une granularité de [4us]
.
jm_Scheduler
ne requière aucun hardware spécifique, ni n'utilise aucune ressource dédiée.
La déclaration Arduino de la fonction micros()
est la suivante:
unsigned long micros();
Regardez dans les références Arduino micros()
pour les détails de la fonction.
timestamp
est un compteur 32-bit
en [us]
et redémarre à 0 environ toutes les 70 minutes (précisément 1h+11m+34s+967ms+296us).
typedef uint32_t timestamp_t;
#define timestamp_read() ((timestamp_t)micros())
#define TIMESTAMP_DEAD (0x01CA0000) // coroutine
dead time [30s + 15ms + 488us]
#define TIMESTAMP_TMAX (0xFE35FFFF) // [1h + 11m + 4s + 951ms + 808us - 1]
#define TIMESTAMP_1US (1UL) // [1us]
#define TIMESTAMP_1MS (1000*TIMESTAMP_1US) // [1ms]
#define TIMESTAMP_1SEC (1000*TIMESTAMP_1MS) // [1s]
#define TIMESTAMP_1MIN (60*TIMESTAMP_1SEC) // [1 minute]
#define TIMESTAMP_1HOUR (60*TIMESTAMP_1MIN) // [1 hour]
timestamp_t
est le type de toutes les valeurs timestamp
.
timestamp_read()
retourne la valeur instantanée timestamp
.
Cette fonction peut aussi être utilisée dans les routines d'interruption
pour dater leurs données.
TIMESTAMP_DEAD
est le temps d'exécution maximum d'une coroutine
pour garantir un fonctionnement correct de jm_Scheduler
.
Si la coroutine
ne prend pas fin avant cette valeur, alors jm_Scheduler
peut perdre la synchronisation des tâches (voir ci-dessous).
TIMESTAMP_TMAX
est le temps maximum de planification d'une coroutine
.
En pratique, n'utilisez pas de valeur de timestamp
plus grande qu'une heure.
// start coroutine
immediately
void start(voidfuncptr_t func);
// start coroutine
immediately and repeat it at fixed interval
void start(voidfuncptr_t func, timestamp_t ival);
// start coroutine
on time and repeat it at fixed interval
void start(voidfuncptr_t func, timestamp_t time, timestamp_t ival);
// stop coroutine, current or scheduled, remove it from chain
void stop();
// rearm current coroutine
and set or reset interval
void rearm(timestamp_t ival);
// rearm current coroutine, change coroutine
function and set or reset interval
void rearm(voidfuncptr_t func, timestamp_t ival);
// rearm coroutine asynchronously and set interval
void rearm_async(timestamp_t ival);
// rearm coroutine asynchronously, change coroutine function and set interval
void rearm_async(voidfuncptr_t func, timestamp_t ival);
// wakeup a scheduled coroutine (maybe repeated)
void wakeup();
// wakeup a scheduled coroutine (maybe repeated but only 1st wakeup_time is recorded)
void wakeup(uint32_t wakeup_time);
// read wakeup count, reset it and remove coroutine from wakeup chain
int wakeup_read();
start()
démarre une coroutine
,
immédiatement ou après une attente time,
avec ou sans répétition ival.
start()
est en principe invoqué une fois ou
lorsque la coroutine
est stoppée.
rearm()
permet de changer les données du scheduler lorsque la coroutine
est en exécution.
stop()
interrompt toutes nouvelles exécutions d'une coroutine
.
stop()
peut être invoqué par la coroutine
en exécution ou par d'autres processus.
Lorsque stop()
est invoqué par la coroutine
en exécution,
stop()
ne provoque pas l'arrêt immédiat de la coroutine
,
stop()
suspend simplement les exécutions suivantes.
Pour stopper immédiatement une coroutine
,
la coroutine
doit invoquer stop()
puis effectuer un return.
rearm()
change les consignes de la variable scheduler.
Les nouvelles valeurs pendront effet à la fin d'exécution de la coroutine
.
rearm()
permet de changer l'intervalle d'exécution
ou de changer la fonction coroutine
.
L'exécution de la prochaine instance de la coroutine
sera déclenchée
de manière synchrone en additionnant ival
au temps de déclenchement de l'instance précédente.
rearm_async()
change les consignes de la variable scheduler.
Les nouvelles valeurs pendront effet à la fin d'exécution de la coroutine
.
rearm_async()
permet de changer l'intervalle de déclenchement de la prochaine instance,
et en option de changer la fonction coroutine
.
L'exécution de la prochaine instance de la coroutine
sera déclenchée
de manière asynchrone en additionnant ival
au temps de fin d'exécution de l'instance précédente.
wakeup()
réveille une coroutine
en attente d'exécution.
Le nombre de réveils est comptabilisé.
wakeup_read()
lit le compteur puis le réinitialise.
wakeup()
peut être invoqué depuis une routine d'interruption.
wakeup_read()
ne doit être invoqué que depuis la coroutine
réveillée.
Le contrôle du scheduler implique au minimum la mise en place de la fonction yield()
dans la fonction Arduino loop()
.
Déclaration de la fonction yield()
:
static void yield();
yield()
est la pierre angulaire du scheduler. Elle doit être invoquée aussi souvent que possible.
Prenez note que yield()
est une méthode statique de la librairie jm_Scheduler
.
Elle remplace la fonction weak native d'Arduino.
Le bon endroit pour invoquer yield()
est dans la fonction Arduino loop()
.
Exemple:
void setup()
{
// ...
}
void loop()
{
yield();
// ...
}
yield()
peut aussi être invoqué dans la fonction Arduino setup()
.
Exemple:
void setup(void)
{
// here, some jm_Scheduler variables initialized...
Serial.begin(9600);
while (!Serial)
{
// wait for USB Serial ready...
yield(); // split loop while()...
}
yield(); // split long setup()...
// continue setup()...
}
Remarques:
yield()
peut aussi être invoqué depuis une coroutine
.
yield()
est automatiquement invoqué dans la fonction Arduino delay()
.
Pour garantir la bonne exécution de toutes les tâches coopératives,
le temps d'exécution des coroutines
doit être le plus court possible.
Cette pratique est identique à celle d'une routine d'interruption
.
Utilisez la fonction Arduino delay()
pour mettre en pause une coroutine
ou la fonction yield()
pour fractionner l'exécution d'une coroutine
.
Une meilleure pratique est de fractionner le code d'une coroutine
en plusieurs coroutines
.
Les méthodes jm_Scheduler
rearm()
et rearm_async()
permettent de sérialiser l'exécution de plusieurs coroutines
.
Ci-dessous sont listés quelques hacks qui peuvent être implémentés en modifiant le fichier jm_Scheduler.h
:
timestamp
peut être la fonction Arduino millis()
en remplacement de micros()
.
timestamp
à 16-bit
.
De cette manière, toutes les comparaisons de timestamp
seront accélérées avec un Arduino UNO ou Leonardo.
timestamp
de 64-bit
.