"delay(); pas forcément si bloquant que cela

Un certain nombre de personnes crient "haro sur le delay" car il est bloquant. Je vais montrer ici que l'on peut parfaitement utiliser un delay qui ne va pas bloquer l'exécution d'un autre code, par exemple un deuxième delay, lui même interrompu par exemple pour faire tourner un pas à pas.
Je suis sous Arduino Uno, mais les propos que je tiens ici peuvent sans doute être utilisés sur d'autres plate-formes.

 

Analyse du code de delay()

Jetons un œil tout d'abord au code du delay qui va être compilé pour ma Uno:

void delay(unsigned long ms)
{
	uint32_t start = micros();

	while (ms > 0) {
		yield();
		while ( ms > 0 && (micros() - start) >= 1000) {
			ms--;
			start += 1000;
		}
	}
}

Les commentaires n'aidant pas tellement, je donne des grandes lignes. On utilise un compteur ms qui va contenir le nombre de millisecondes qui est régulièrement décrémenté (du moins si on ne fait rien pendant le delay()), et lorsqu'il tombe à zéro termine la fonction.

On remarquera la présence de yield() qui indique déjà que l'on peut aller voir ailleurs pour faire une autre tâche pendant ce delay. Je n'en sais pas beaucoup plus, cela n'est pas utilisé sur la Uno. Mais cela indique tout de même que l'on peut faire autre chose pendant un delay().

Si on analyse un peu ce code, et si on laisse tout le temps libre pour delay(), toutes les 1000µs la valeur retournée par micros() s'est incrémenté de 1000, et on va d'une part décrémenter le compteur ms (on doit maintenant faire une milliseconde de moins), d'autre part incrémenter start qui va indiquer le nouveau départ.

Si pendant le delay() on s'occupe ailleurs et que le while ne tourne plus pendant un certain temps, micro() qui a continué d'augmenter retournera une valeur bien plus grande que start+1000. Quand on redonne la main à la boucle while, non plus à chaque milliseconde mais à chaque itération, on décompte une milliseconde et on va donc rattraper le retard. Si bien que l'on peut suspendre delay pendant presque le temps que l'on veut, et que tout redevient normal après la fin de cette suspension. Il va de soi que si on a programmé delay(100); et que l'on suspend le delay() pendant une seconde, on dépassera les 100ms.

En réalité on ne peut pas suspendre delay() trop longtemps car il ne faudrait pas que micros() fasse une boucle complète. Si le delay() maximum est de l'ordre de 50 jours, on ne peut suspendre le while de delay() que pendant 70 minutes. Au delà , delay() sera prolongé par paquets de 70mn.

 

Interrompre un delay()

C'est déjà fait "naturellement" par la remise à l'heure des compteur de temps (pour millis(), micros()) par le timer 0. On peut donc nous aussi écrire une fonction d'interruption qui peut suspendre un delay() de la boucle loop(). Une exigence tout de même, il faut réactiver les interruptions sinon le comptage du temps s'arrête. Il peut aussi y avoir des problèmes si notre fonction d'interruption est ré-entrante et qu'elle s'interrompt elle même à l'infini.

Utilisons par exemple le programme "blink with delay" bien connu:

#define milli_secondes // Commentaire, car remplacé par une chaîne vide
void setup()
{
  digitalWrite(LED_BUILTIN, LOW);  // Led en sortie, éteinte
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  // Programme classique du "blink with delay"
  Serial.println(F("J'allume"));
  digitalWrite(LED_BUILTIN, HIGH);
  delay(5000 milli_secondes);
  Serial.println(F("J'éteins"));
  digitalWrite(LED_BUILTIN, LOW);
  delay(5000 milli_secondes);
}

Voici ce que l'on peut obtenir sur la console:

23:39:15.942 -> J'allume
23:39:20.914 -> J'éteins
23:39:25.931 -> J'allume
23:39:30.903 -> J'éteins
23:39:35.918 -> J'allume
23:39:40.935 -> J'éteins
23:39:45.905 -> J'allume

On a bien un changement toutes les 5s.

Il y a dedans deux fonctions delay(); et je vais me faire une joie de faire autre chose pendant une partie de ces 5000 ms

Je vais éviter de détailler le détail du code de l'interruption, ce n'est pas trivial, je vais utiliser une bibliothèque déjà faite. Mais rien ne vous empêche de regarder le source, et/ou de me demander plus de détails dessus. Voici les lignes qui vont permettre de faire autre chose pendant les 5000ms:

#include "MTbutton.h" // V1.0.0 Voir http://arduino.dansetrad.fr/MTobjects

const uint8_t PIN_BOUTON = A0;

void boutonAppuye(void)
{
  for (char lettre = 'A'; lettre <= 'Z'; lettre++)
  {
    Serial.print(lettre); // Écrit donc l'alphabet en 2,6s
    delay(100 milli_secondes); // Qui va donc suspendre le delay du blink
  }
  Serial.println(); // Pour être prêt pour un nouvel alphabet
}

MTbutton Bouton(PIN_BOUTON, boutonAppuye);

void setup()
{
  Serial.begin(115200); // Pour envoyer les messages
}

void loop()
{
}

Cette partie définit un bouton sur la broche A0 qui appelle la fonction void boutonAppuye(void) quand on vient juste d'appuyer dessus. A ce moment, on va envoyer les lettres de l'alphabet avec un délai de 100ms entre chaque lettre. Tous les petits delay(100 milli_secondes); vont donc avoir lieu principalement pendant les delay(5000 milli_secondes);.

Note: la fonction boutonAppuye() est appelée par une interruption non ré-entrante qui réactive les interruptions. En première approximation, c'est comme si on avait deux fonctions loop(), une qui tourne en permanence et une deuxième qui ne tourne qu'une fois quand on appuie sur le bouton.

 

Appui en début de delay(5000 milli_secondes);

Si on appuie sur le bouton juste après un changement, comme l'écriture de l'alphabet ne dure que 2,6s, le delay(5000 milli_secondes); va être interrompu une fois pour faire la boucle for et au sortir de la boucle, comme cela à duré moins de 70mn, il va finir correctement son comptage. Sur la console, on va obtenir par exemple:

23:39:40.935 -> J'éteins
23:39:45.905 -> J'allume
23:39:46.420 -> ABCDEFGHIJKLMNOPQRSTUVWXYZ
23:39:50.919 -> J'éteins
23:39:55.935 -> J'allume
23:40:00.907 -> J'éteins
23:40:01.329 -> ABCDEFGHIJKLMNOPQRSTUVWXYZ
23:40:05.922 -> J'allume
23:40:10.939 -> J'éteins

On voit que l'appui s'étant fait environ une seconde après un changement, cela ne modifie pas le temps des allumages/extinctions (secondes divisibles par 5).

 

Appui à la fin de delay(5000 milli_secondes);

Si on appuie sur le bouton beaucoup plus tard, , il faudra attendre la fin du for pour revenir au delay(5000 milli_secondes); et ce dernier va être prolongé. On peut alors avoir un affichage:

23:40:10.939 -> J'éteins
23:40:15.910 -> J'allume
23:40:19.566 -> ABCDEFGHIJKLMNOPQRSTUVWXYZ
23:40:22.147 -> J'éteins
23:40:27.167 -> J'allume
23:40:32.136 -> J'éteins
23:40:32.417 -> ABCDEFGHIJKLMNOPQRSTUVWXYZ
23:40:37.155 -> J'allume
23:40:42.160 -> J'éteins

Au début les extinctions se faisaient sur les dizaines entières des secondes. Le bouton ayant été appuyé la première fois à t=19.56s, le for va donc durer environ jusqu'à t=19.5s+2,6s soit 22,1s, et c'est donc à ce moment que s'éteint la led.

On peut donc interrompre tranquillement un delay si on a finit le boulot avant la fin prévue.

 

Que faire pendant le delay(100 milli_secondes);?

On a donc un delay(5000 milli_secondes); suspendu par des delay(100 milli_secondes);. Peut-on interrompre ce deuxième delay()? Oui, je l'ai déjà mentionné il est interrompu par l'horloge système, sinon les temps ne seraient pas corrects. Si on veut une approche plus visuelle, on peut combiner le tout avec le code suivant:

#include "MTulnStepper.h" // V1.0.0 Voir http://arduino.dansetrad.fr/MTobjects

MTulnStepper Stepper(pin_A1 2, pin_B1 3, pin_A2 4, pin_B2 5);

void setup()
{
  Stepper.move(CONTINUE); // Rotation infinie
}

void loop()
{
}

... qui va faire une rotation infinie à un moteur pas à pas. Si il tourne correctement, c'est bien que les pas sont envoyés régulièrement. un changement de pas est envoyé ici toutes les 4,9ms, ce qui correspond à la vitesse par défaut de la bibliothèque pour un 28BYJ48 à 0,1tr/s.

 

Programme complet

Le programme complet utilisé (on rajoute tous les bouts de code ensembles, ce qui est simple car un seul utilise loop() ):

// Version 1.0.1

//###########################################################################
//###########################################################################
//####                                                                   ####
//####                          Olivier Pécheux                          ####
//####                       Olivier(a)Pecheux.fr                        ####
//####                        (33) +6 69 77 82 58                        ####
//####               http://arduino.dansetrad.fr/MTobjects               ####
//####                                                                   ####
//###########################################################################
//###########################################################################

// Ce programme montre que l'on peut mettre des delays(), et même suspendre
// un delay() par un autre

// Ce programme comporte un "blink with delay" en toile de fond.
// Quand on appuie sur un bouton, 10 caractères sont envoyés sur la console 
// en utilisant un delay. Comme on a du temps libre pendant que les delay
// travaillent, on montre que l'on peut en profiter pour gérer la rotation
// d'un pas à pas. 
// Les deux delay() simultanés ne vont pas gêner la mémorisation de l'appui
// du bouton ni la rotation du moteur. La durée du blink peut être rallongée
// au maximum de 100ms si le changement d'état doit se faire pendant le delay
// de 100ms.

#include "MTbutton.h" // V1.0.0 Voir http://arduino.dansetrad.fr/MTobjects
#include "MTulnStepper.h" // V1.0.0 Voir http://arduino.dansetrad.fr/MTobjects

const uint8_t PIN_BOUTON = A0;

void boutonAppuye(void)
{
  for (char lettre = 'A'; lettre <= 'Z'; lettre++)
  {
    Serial.print(lettre); // Écrit donc l'alphabet en 2,6s
    delay(100 milli_secondes); // Qui va donc suspendre le delay du blink
  }
  Serial.println(); // Pour être prêt pour un nouvel alphabet
}

MTbutton Bouton(PIN_BOUTON, boutonAppuye);

MTulnStepper Stepper(pin_A1 2, pin_B1 3, pin_A2 4, pin_B2 5);

void setup()
{
  digitalWrite(LED_BUILTIN, LOW); // Led en sortie, éteinte 
  pinMode(LED_BUILTIN, OUTPUT);

  Serial.begin(115200); // Pour envoyer les messages
  
  Stepper.move(CONTINUE); // Rotation infinie
}

void loop()
{
  // Programme classique du "blink with delay"
  Serial.println(F("J'allume"));
  digitalWrite(LED_BUILTIN, HIGH);
  delay(5000 milli_secondes);
  Serial.println(F("J'éteins"));
  digitalWrite(LED_BUILTIN, LOW);
  delay(5000 milli_secondes);
}

 

Divers

Si j'appuie plein de fois sur le bouton?
Si on appuie une première fois sur le bouton, les lettres commencent à s'écrire. Si on appuie une seconde fois avant la fin de l'écriture, l'information "bouton vient juste d'être appuyé" repasse de faux à vrai et on va écrire deux fois l'alphabet. Si on appuie encore une fois avant la fin du premier alphabet, l'info "bouton vient juste d'être appuyé" étant déjà à vrai, il ne se passe pus rien; Un triple ou quadruple appui ne fera que deux alphabet. Bien sûr si on appuie pendant la deuxième écriture, on aura 3 alphabets.

Peut-on interrompre un delay par un deuxième, lui même interrompu par un troisième, lui même par un quatrième?
Oui, mais je n'ai pas de programme sous la main qui le fait.