Tutoriel · Débutant · 30 min de lecture
Pourquoi delay() casse les robots : timing avec millis()
Une boucle bloquée ne voit rien. Ce que delay() coûte en millimètres, le motif millis() qui le remplace, et le bogue de bouclage du correctif évident.
Introduction
Presque chaque robot de ce site fait tourner une boucle qui doit faire plusieurs choses à la fois : lire un capteur, actualiser un contrôleur, piloter les moteurs, peut-être répondre à une radio. delay() est le premier outil de temporisation appris, et c’est celui qui rend tout cela impossible.
Le problème n’est pas que delay() est lent. C’est que pendant un delay(), le robot ne fait rien du tout — ne lit pas ses capteurs, ne braque pas, ne remarque pas qu’il va heurter un mur. C’est un programme qui s’est volontairement arrêté.
Cette page traite de ce que cela coûte vraiment, du motif qui le remplace, et de l’unique bogue que le remplacement évident a encore.
Ce que delay() fait vraiment
delay(50) tourne en boucle serrée en regardant un timer jusqu’à ce que 50 ms aient passé. Votre code n’avance pas. Les seules choses qui arrivent encore sont les interruptions — le timer qui pilote millis(), l’interruption de réception série, tout ce que vous avez attaché avec attachInterrupt().
Cette exception compte et c’est aussi un piège : un encodeur compté en interruption continue de compter pendant un delay(), donc le nombre reste juste pendant que le comportement du robot s’arrête. Ce qui rend la panne plus dure à voir, pas plus facile.
Ce que cela coûte, en millimètres
Une boucle de commande ne peut réagir qu’au rythme où elle échantillonne. Quelque chose qui apparaît juste après une lecture n’est remarqué qu’à la suivante.
Vingt-quatre millimètres sont l’essentiel de la barrette de capteurs d’un suiveur de ligne. C’est aussi largement assez pour quitter une case de labyrinthe, dépasser une intersection, ou faire basculer un robot équilibriste sans retour — celui-là exige une boucle interne à 200 Hz, soit un budget de 5 ms pour tout, et un seul delay(50) dépense dix boucles de cela. La latence est toujours une distance — la distance parcourue par le robot avant d’agir est ce qui décide s’il s’arrête à temps ou roule hors du bord.
Le motif qui le remplace
Au lieu de s’arrêter jusqu’à l’heure, demandez à chaque passage si c’est l’heure :
unsigned long lastRead = 0;
const unsigned long READ_INTERVAL = 50;
void loop() {
unsigned long now = millis();
if (now - lastRead >= READ_INTERVAL) {
lastRead = now;
readSensor();
}
// everything else keeps running, every pass
updateMotors();
checkBumper();
}
loop() tourne désormais des milliers de fois par seconde et la lecture du capteur arrive à l’heure dedans. Rien n’est bloqué, et ajouter une seconde tâche temporisée fait quatre lignes de plus plutôt qu’une réécriture.
Le bogue de bouclage
millis() renvoie un unsigned long — 32 bits sur AVR — comptant les millisecondes depuis le boot. Il s’épuise à 2³² ms et reboucle à zéro, soit 49,7 jours.
Deux façons d’écrire la même comparaison, et une seule survit à cela :
if (now - lastRead >= READ_INTERVAL) // correct
if (now >= lastRead + READ_INTERVAL) // wrong
Prenez lastRead = 4294967290 avec un intervalle de 100 ms, juste avant le bouclage. lastRead + 100 déborde à 94, et now >= 94 est aussitôt vrai — donc le timer cesse d’attendre et part à chaque passage jusqu’à ce que le compteur rattrape. La forme avec soustraction n’a pas ce problème, parce que la soustraction non signée boucle exactement de façon à ce que la différence sorte juste.
Trois règles suivent :
- Toujours soustraire, jamais additionner.
now - last >= interval. - Gardez chaque horodatage
unsigned long. Stockermillis()dans unintou unlongsigné réintroduit aussitôt le bogue, etintsur AVR boucle après 32 secondes. - Ne testez jamais l’égalité.
now == targetest un pile ou face ; un passage occupé l’enjambe et l’événement ne part jamais.
micros() mérite son avertissement propre. Il boucle à 2³² microsecondes, soit 71,6 minutes — une durée de fonctionnement tout à fait ordinaire pour un robot. Mêmes règles, bien plus tôt.
Plusieurs choses, à des rythmes différents
Une fois passé deux ou trois tâches temporisées, donnez-leur un tableau au lieu d’un tas de variables :
struct Task {
unsigned long last;
unsigned long interval;
void (*run)();
};
Task tasks[] = {
{ 0, 5, readSensors },
{ 0, 10, updateControl },
{ 0, 200, reportStatus },
};
void loop() {
unsigned long now = millis();
for (Task &t : tasks) {
if (now - t.last >= t.interval) {
t.last = now;
t.run();
}
}
}
C’est un ordonnanceur coopératif, et c’est tout ce dont la plupart des robots ont besoin. Deux notes dessus :
t.last = now redémarre l’horloge quand la tâche a vraiment tourné, donc un passage en retard rend le suivant en retard aussi et la cadence dérive lentement. t.last += t.interval tient la cadence à la place — mieux pour une boucle de commande, pire si une tâche dépasse jamais son intervalle, parce qu’alors elle essaie de rattraper et tourne dos à dos. Utilisez += pour la boucle dont la temporisation compte et = now pour tout le reste.
Et le tout dépend d’aucune tâche bloquante. Un delay() dans reportStatus() et chaque autre tâche est en retard.
Quand bloquer reste la bonne réponse
Toute attente n’est pas une erreur. Le test est si elle est plus courte qu’un passage de boucle.
delayMicroseconds(10)pour faire l’impulsion de déclenchement d’un HC-SR04 va bien. Dix microsecondes ne sont rien, et il n’y a pas de façon non bloquante de la produire.pulseIn()ne va pas bien. Il bloque jusqu’au retour de l’écho ou l’expiration du délai. Un HC-SR04 à sa limite de 4 m fait un aller-retour de 8 m, soit 23 ms à 343 m/s — et un écho manqué vous coûte tout le délai réglé. C’est pourquoi ce tutoriel insiste pour en régler un.Serial.print()cache un delay. Il revient aussitôt tant que le tampon de 64 octets a de la place, et bloque une fois plein. À 9 600 bauds le tampon prend 67 ms à se vider, donc afficher une ligne de 40 caractères à chaque passage à 100 Hz calera absolument la boucle. Utilisez 115 200, affichez moins, et affichez sur un timer lent.
Votre temps de boucle est un vrai nombre
Cessez de le deviner :
unsigned long worst = 0;
void loop() {
unsigned long t0 = micros();
// ... the whole loop body ...
unsigned long dt = micros() - t0;
if (dt > worst) worst = dt; // report this on a slow timer, not here
}
Suivez le passage pire, pas la moyenne. La moyenne dit que la boucle est confortable ; le pire dit ce que la boucle de commande doit vraiment survivre, et c’est d’ordinaire un Serial.print, un délai pulseIn, ou une lecture I²C qui a dû attendre.
Cela compte le plus pour un contrôleur PID. Le terme dérivé divise un changement d’erreur par le temps entre échantillons, donc la gigue de cet intervalle devient directement du bruit sur D — et des gains réglés à une fréquence de boucle cessent d’être justes à une autre. Soit faites tourner la boucle à intervalle fixe et traitez dt comme constant, soit mesurez dt à chaque passage et utilisez la vraie valeur. Ne faire ni l’un ni l’autre est pourquoi un contrôleur sage sur l’établi se comporte mal une fois le code radio ajouté.
Quoi utiliser à la place
| Au lieu de | Utilisez |
|---|---|
delay() pour rythmer une lecture de capteur |
Contrôle d’intervalle millis() |
delay() pour clignoter une LED |
Un timer dans la même boucle |
delay() pour attendre qu’un servo arrive |
Estimez le temps de trajet et vérifiez millis() |
delay() pour anti-rebondir un bouton |
Horodatez le front, ignorez les changements 20 ms |
delay() entre paliers de vitesse moteur |
Un profil de mouvement piloté au temps écoulé |
delay() pour ralentir toute la boucle |
Rien — tournez vite et temporisez les tâches une par une |
pulseIn() sans délai |
pulseIn(pin, HIGH, 30000) et une branche « pas d’écho » |
Quand ça tourne mal
| Symptôme | Généralement |
|---|---|
| Le robot ignore son pare-chocs en tournant | Un delay() dans le virage |
| Les lectures de capteurs arrivent par rafales | Tout partage un timer au lieu d’intervalles séparés |
| Bien sur l’établi, mou avec les logs activés | Serial.print qui bloque une fois le tampon plein |
| Le timer part en continu après ~50 jours | La forme now >= last + interval |
| Le timer déraille après environ une heure | Bouclage de micros() à 71,6 minutes |
| Le timer ne part jamais du tout | Horodatage stocké en int, ou test d’égalité |
| PID bien réglé, puis se comporte différemment | La fréquence de boucle a changé ; dt n’est plus ce que les gains supposaient |
| L’encodeur compte juste mais le robot réagit tard | Les interruptions tournent encore pendant delay() — les comptes ne sont pas le problème |
L’habitude à bâtir est de traiter loop() comme quelque chose qui doit toujours revenir vite. Une fois vrai, ajouter une fonction est ajouter une tâche, et le robot garde ses réflexes. Une fois faux, chaque nouvelle fonction rend le robot légèrement plus aveugle, et la cause n’est jamais là où est le symptôme.
Explorer le graphe
Fait partie de ces réalisations
Les projets et parcours d'apprentissage qui incluent ce tutoriel.
- ProjetConstruire un mini robot sumo : adhérence et coin
- ProjetConstruire un robot de couverture qui balaie une pièce
- ProjetConstruire un robot de pick-and-place qui sait quand il rate
- ProjetConstruire un robot Mecanum qui roule de côté
- ProjetConstruire un traceur à stylo : deux axes pas à pas qui arrivent
- Parcours d'apprentissageFondements d'électronique robotique : un parcours d'apprentissage
Pour aller plus loin