Aller au contenu
Académie JediSith · Programmation

Le temps : delay(), millis() et les événements

En une phrase : delay() met le programme en pause, alors que millis() regarde l'horloge sans s'arrêter : c'est ce qui permet de faire plusieurs choses en même temps.

Compter le temps en millisecondes

L'ESP32 compte le temps en millisecondes (ms) : 1 seconde = 1 000 ms.

Durée En millisecondes
un clignement d'œil environ 300 ms
un tour de loop() avec delay(10) un peu plus de 10 ms
une seconde 1 000 ms
une minute 60 000 ms

Fréquence et période

Pour un phénomène qui se répète, on parle de fréquence (combien de fois par seconde, en hertz, Hz) ou de période (combien de temps dure un cycle). Les deux sont liées :

période (ms) = 1 000 ÷ fréquence (Hz)

Une LED qui clignote 2 fois par seconde (2 Hz) a une période de 1 000 ÷ 2 = 500 ms : 250 ms allumée, 250 ms éteinte.

tempspériode : 500 ms250 ms250 ms2 clignotements par seconde = 2 Hz1 seconde = 2 périodes
Fréquence (combien de fois par seconde) et période (combien de temps dure un cycle) : deux façons de dire la même chose.

delay() : la sieste

digitalWrite(LED, HIGH);
delay(500);                    // le programme dort 500 millisecondes
digitalWrite(LED, LOW);

Simple et efficace… tant qu'il n'y a qu'une seule chose à faire. Pendant la sieste, l'ESP32 n'écoute plus rien : ni la manette, ni les boutons.

Le problème, en chiffres

Le programme de la mission 0 passe 1 000 ms par tour de loop() dans deux delay(500). S'il devait aussi lire la manette, il ne la regarderait qu'une fois par seconde : appuie sur ✕ pendant un quart de seconde au mauvais moment, et l'appui est tout simplement raté.

millis() : l'horloge

millis() donne le nombre de millisecondes écoulées depuis le démarrage de la carte. Au lieu de dormir, on compare l'heure :

unsigned long dernierChangement = 0;
bool allumee = false;

void loop() {
  if (millis() - dernierChangement >= 500) {   // 500 ms sont passées ?
    dernierChangement = millis();
    allumee = !allumee;                        // on inverse
    digitalWrite(LED, allumee ? HIGH : LOW);
  }
  // ici, le programme continue : il peut lire la manette en même temps !
}

C'est comme vérifier sa montre plutôt que de faire une sieste : « est-ce que c'est l'heure ? Non ? Alors je fais autre chose et je regarde de nouveau dans un instant. »

delay() millis()
Ce qu'il fait met en pause donne l'heure
Pendant l'attente le programme dort le programme continue
Idéal pour les premiers essais, les petits programmes tout ce qui réagit à la manette
delay()millis()dort 500 ms 😴dort 500 ms 😴✕ appuyé ici : raté !un petit tour toutes les quelques ms : « c'est l'heure ? non… oui ! »✕ appuyé ici : vu !change la LEDvérifie
En haut, delay() : pendant les zones grises, le programme dort et rate tout. En bas, millis() : le programme vérifie l'heure en permanence et reste attentif.

La notation condition ? A : B

allumee ? HIGH : LOW se lit « si allumee est vrai, alors HIGH, sinon LOW ». C'est un if miniature, pratique dans une seule ligne.

Plusieurs chronos en même temps

Le grand avantage de millis() : chaque tâche peut avoir son propre chrono. Voir la mise en pratique plus bas, avec deux LEDs à des rythmes différents.

Le temps qui avance plus ou moins vite

Dans le banc d'essai de la mission 1, les scénarios ne lisent pas millis() directement : ils lisent une horloge maison, qui avance plus ou moins vite selon la vitesse choisie avec les flèches ↑ ↓. C'est comme ça qu'on accélère un clignotement sans toucher à son code.

Une horloge qui fait le tour

millis() est rangé dans un unsigned long, qui ne dépasse pas 4 294 967 295. Ça fait 4 294 967 295 ms ÷ 1 000 ÷ 60 ÷ 60 ÷ 24 ≈ 49,7 jours. Au-delà, l'horloge repasse à 0. Écrire millis() - debut >= duree (et pas millis() >= debut + duree) évite tout souci ce jour-là : la soustraction reste juste même quand l'horloge a fait le tour.

Les événements : réagir au bon moment

Un bouton appuyé une demi-seconde est vu 50 fois par un loop() qui tourne toutes les 10 ms. Pour ne réagir qu'une fois, on compare avec ce qu'on avait vu au tour précédent :

relâchéappuyéchaque petit trait = un tour de loop() : 10 tours voient le bouton appuyéavant : non / maintenant : oui→ l'événement « vient d'appuyer »avant : oui / maintenant : non→ « vient de relâcher »
Le bouton reste enfoncé pendant plusieurs tours, mais il n'y a qu'un instant où il change : c'est là qu'on réagit.
bool avant = false;

void loop() {
  bool maintenant = manette->a();
  if (maintenant && !avant) {    // appuyé maintenant, pas appuyé avant :
    Serial.println("✕ !");       // il VIENT d'être enfoncé
  }
  avant = maintenant;
}
avant maintenant Ce qui vient de se passer
non non rien, le bouton est au repos
non oui le bouton vient d'être enfoncé ← l'événement
oui oui il est toujours tenu
oui non il vient d'être relâché (un autre événement !)

C'est exactement ce que fait la fonction vientDAppuyer du banc d'essai de la mission 1.

Et les vrais boutons ?

Un bouton-poussoir branché sur une broche « rebondit » : au moment de l'appui, le contact s'ouvre et se ferme plusieurs fois en quelques millisecondes. Il faut alors ignorer les changements trop rapprochés (moins de 20 ms environ) : c'est l'anti-rebond. La manette, elle, fait ce travail toute seule.

Les étapes : une petite machine à états

Pour un enchaînement (« boum, attendre, boum, attendre… ») sans delay(), on note à quelle étape on en est, et l'heure de départ. À chaque tour de loop(), on regarde si c'est l'heure de passer à l'étape suivante. C'est la technique du battement de cœur de la mission 1 : le programme garde une variable etapeCoeur et un tableau des moments où vibrer.

Mise en pratique : deux LEDs, deux rythmes

Sur le montage de la piste d'atterrissage, fais clignoter D18 toutes les 300 ms et D19 toutes les 700 ms, en même temps. Impossible avec delay(), facile avec deux chronos :

const int LED_A = 18, LED_B = 19;
unsigned long chronoA = 0, chronoB = 0;
bool etatA = false, etatB = false;

void setup() {
  pinMode(LED_A, OUTPUT);
  pinMode(LED_B, OUTPUT);
}

void loop() {
  unsigned long maintenant = millis();

  if (maintenant - chronoA >= 300) {      // la LED A a son propre chrono
    chronoA = maintenant;
    etatA = !etatA;
    digitalWrite(LED_A, etatA);
  }
  if (maintenant - chronoB >= 700) {      // la LED B aussi
    chronoB = maintenant;
    etatB = !etatB;
    digitalWrite(LED_B, etatB);
  }
}

Regarde bien : de temps en temps, les deux LEDs changent en même temps. Tous les combien ? Le plus petit nombre divisible à la fois par 300 et par 700 : 2 100 ms.

D18D190600120018002400ms2 100 ms : les deux changent ensemble
Les deux chronos tournent chacun à leur rythme, sans se gêner. Les zones colorées : LED allumée.

Exercices

1. Une LED doit clignoter 5 fois par seconde. Combien de temps allumée, combien éteinte ?

Période : 1 000 ÷ 5 = 200 ms, donc 100 ms allumée, 100 ms éteinte.

2. millis() vaut 12 500. Depuis combien de secondes la carte est-elle allumée ?

12 500 ÷ 1 000 = 12,5 secondes.

3. Un loop() contient delay(20). Combien de fois par seconde lit-il la manette, au maximum ?

1 000 ÷ 20 = 50 fois par seconde (un peu moins en vrai, car le reste du code prend aussi un peu de temps).

4. Modifie la mise en pratique pour ajouter une 3ᵉ LED sur D21, qui clignote toutes les 1 100 ms.

Il suffit d'ajouter une variable chronoC, une variable etatC, un pinMode(21, OUTPUT) et un troisième bloc if (maintenant - chronoC >= 1100) { … }. Avec plus de LEDs, un tableau de chronos et une boucle for seraient encore mieux : voir tableaux, boucles et fonctions.

À retenir

  • 1 s = 1 000 ms ; période = 1 000 ÷ fréquence.
  • delay() = pause : tout s'arrête. millis() = horloge : on vérifie l'heure et on continue.
  • Toujours écrire millis() - debut >= duree.
  • Un événement, c'est un changement : comparer avec le tour d'avant.

Où ça sert dans l'atelier

Pour aller plus loin