Programmation temps réel
Comment écrire du code qui gère plusieurs tâches simultanément sans se bloquer — timing précis, boucles rapides, séquenceurs et interruptions.
Le problème du code bloquant
Avec time.sleep(), ton programme est bloqué — il ne peut rien faire d'autre pendant la pause. C'est acceptable pour des projets simples, mais impossible dès qu'on veut réagir à plusieurs événements.
Mesure du temps avec ticks_ms()
La solution : mémoriser l'heure du dernier événement et vérifier si le délai est écoulé sans attendre.
💡 Pourquoi ticks_diff() ?
Le compteur ticks_ms() déborde après environ 12 jours. ticks_diff() gère correctement ce débordement — ne jamais faire t2 - t1 directement.
Boucle rapide non-bloquante
La technique fondamentale : une boucle qui tourne très vite (1-10 ms), et qui vérifie si c'est le bon moment pour faire chaque action.
Séquenceur — machine à états
Pour des séquences plus complexes, on utilise une machine à états. Le programme mémorise dans quel "état" il se trouve et quand passer au suivant.
Interruptions
Les interruptions permettent de réagir immédiatement à un événement (front montant ou descendant d'un pin), sans attendre le prochain tour de boucle.
✅ À faire dans une IRQ
- Positionner un flag booléen
- Incrémenter un compteur
- Mémoriser
ticks_ms()
❌ À éviter dans une IRQ
print()ou I/O lents- Allocation mémoire
- Longues boucles
Résumé des techniques
| Technique | Usage | Complexité |
|---|---|---|
| time.sleep() | Code simple, une seule tâche | Simple |
| ticks_ms() + vérification | 2-5 tâches indépendantes | Moyen |
| Machine à états | Séquences complexes | Avancé |
| Interruptions (IRQ) | Réaction immédiate à un signal | Avancé |
Exemples commentés
Chaque exemple est précédé de son contexte. Le code est affiché directement — bouton copier + lien GitHub disponibles.
⏱️ Mesure du temps
ticks_ms(), mesure de durées, détection de fronts.
1 Quatre approches pour mesurer un temps d'appui
Mesurer le temps pendant lequel on appuie sur un bouton peut se faire de 4 façons différentes, selon deux axes :
- Par comptage de boucles : on compte le nombre d'itérations de 20 ms pendant l'appui → résolution fixe de 20 ms
- Par ticks_ms() : on lit l'horloge interne → meilleure précision
Et pour chacune des deux méthodes, une version :
- Bloquante : des boucles
whileattendent l'appui et le relâchement - Non-bloquante : détection de fronts dans la boucle principale
2 Méthode 1 — comptage de boucles (bloquant et non-bloquant)
Version bloquante : on attend l'appui (while pas appuyé), puis on incrémente toutes les 20 ms tant qu'appuyé, puis on calcule : durée = compteur × 0,02 s.
Version non-bloquante : la boucle principale tourne à 50 Hz. On incrémente tant que etat == 1, et au front descendant (relâchement) on affiche la durée accumulée.
if etat:
compt_temps += 1 # incrémenter si appuyé
if ancien_etat and not etat: # front descendant
print(compt_temps / 50, "s") # afficher la durée
compt_temps = 0 # remettre à 0 lecture-bp-temps-bloquant.py — Durée d'appui — version bloquante .python cours-exemples/timing/lecture-bp-temps-bloquant.py 16 lignes
GitHub
from machine import Pin
from time import sleep
bp = Pin(25, Pin.IN)
compt_temps = 0
for t in range(5): # on fait juste 5 cycles appui/relachement
while not bp.value() : # on attend l'appui sur le bp
sleep(.02) # on attend 20 ms, 50 boucles/s suffisant
compt_temps = 0
while bp.value() : # on attend l'appui sur le bp
compt_temps += 1 # on incremente toutes les 20 ms
sleep(.02) # delai entre deux boucles
print("Compteur : {0:2d}, soit : {1:2f} s".\
format(compt_temps, compt_temps/50))
lecture-bp-temps-ok.py — Durée d'appui — version non-bloquante .python cours-exemples/timing/lecture-bp-temps-ok.py 18 lignes
GitHub
from machine import Pin
from time import sleep
bp = Pin(25, Pin.IN)
compt_temps = 0
ancien_etat = bp.value() # initialisation ancien_etat
for t in range(500): # on fait 500 cycles * 0.02 s = 10 s
etat = bp.value() # lecture etat
if etat : # tant qu'on appuie...
compt_temps += 1 # ... on incremente
if ancien_etat and not etat : # et si front descendant ! ...
print("Compteur : {0:2d}, soit : {1} s".\
format(compt_temps, compt_temps /50))
compt_temps = 0 # on remet a 0
ancien_etat = etat # memorisation etat boucle avant
sleep(0.02) # delai de boucle
3 Méthode 2 — ticks_ms() : l'horloge interne
ticks_ms() retourne le temps écoulé en millisecondes depuis la mise sous tension (ou le dernier reset). C'est une horloge absolue, indépendante de la durée des autres instructions.
from time import ticks_ms
print(ticks_ms()) # ex : 12345 ms depuis le démarrage Pour mesurer un temps d'appui : on mémorise start = ticks_ms() au front montant, stop = ticks_ms() au front descendant, et delta = stop - start donne le temps en ms. La précision est bien meilleure que le comptage de boucles, car il n'y a pas de sleep() dans les boucles d'attente.
methode-ticks.py — Découverte de ticks_ms() .python cours-exemples/timing/methode-ticks.py 13 lignes
GitHub
from machine import Pin
from time import sleep, sleep_ms, sleep_us, ticks_ms, ticks_us
bp = Pin(25, Pin.IN)
for t in range(100): # duree = 100 * 0.1 = 10 s
if bp.value() :
print (ticks_ms())
sleep(0.1)
lecture-bp-temps-ticks-bloquant.py — Durée appui avec ticks — bloquant .python cours-exemples/timing/lecture-bp-temps-ticks-bloquant.py 16 lignes
GitHub
from machine import Pin
from time import sleep, sleep_ms, sleep_us, ticks_ms, ticks_us
cont = True
bp = Pin(25, Pin.IN)
for t in range(5): # on fait juste 5 cycles appui/relachement
while not bp.value() : # on attend l'appui sur le bp
pass # pas besoin de tempo
start = ticks_ms() # memorisation de start lors du front montant
while bp.value() : # on attend l'appui sur le bp
pass # pas besoin de tempo
stop = ticks_ms() # stop sur front montant 2
delta = stop - start
print("Temps d'appui : {0} ms".format(delta))
lecture-bp-temps-ticks-ok.py — Durée appui avec ticks — non-bloquant .python cours-exemples/timing/lecture-bp-temps-ticks-ok.py 19 lignes
GitHub
from machine import Pin
from time import sleep, sleep_ms, sleep_us, ticks_ms, ticks_us
led = Pin(2, Pin.OUT)
bp = Pin(25, Pin.IN)
ancien_etat = bp.value() # initialisation ancien_etat
for t in range(10000): # duree = 10000 * 0.001 = 10 s
etat = bp.value() # lecture etat
if etat and not ancien_etat : # condition vrai : front montant
start = ticks_ms() # memorisation de start
if not etat and ancien_etat : # condition vrai : front descendant
stop = ticks_ms() # stop sur front montant 2
delta = stop - start # calul du temps d'appui
print("Temps d'appui : {0} ms".format(delta))
ancien_etat = etat # memorisation ancien-etat
sleep_ms(1)
4 Application — mesure de temps entre deux événements
Application réelle : mesurer la vitesse d'une bille tombant entre deux capteurs distants de 50 mm. On simule les deux capteurs par deux boutons (bpA et bpB).
- V1 : comptage de boucles entre les deux fronts — simple mais résolution limitée à 1 ms
- V2 :
start = ticks_ms()sur bpA,stop = ticks_ms()sur bpB — bien plus précis - V3 : version améliorée de V2 avec une précaution : on vérifie que les deux capteurs sont à l'état bas avant de démarrer la mesure (cas où bpA serait déjà pressé au lancement)
mesure-temps-deux-fronts-1.py — Entre deux fronts — comptage boucles .python cours-exemples/timing/mesure-temps-deux-fronts-1.py 26 lignes
GitHub
############## mesure de temps entre 2 fronts ################
from machine import Pin
from time import sleep, sleep_ms, sleep_us
cont = True
led = Pin(2, Pin.OUT)
bpA = Pin(25, Pin.IN)
bpB = Pin(34, Pin.IN)
compt = 0
print("Attente du front montant de bpA")
while not bpA.value(): # attente a l'etat bas du front montant
pass
led.value(1)
print("Attente du front montant de bpB")
while not bpB.value(): # attente a l'etat bas du front montant
compt = compt + 1
sleep_ms(1)
led.value(0)
print("Temps entre deux fronts : {0} ms \n".format(compt))
mesure-temps-deux-fronts-2.py — Entre deux fronts — ticks_ms() .python cours-exemples/timing/mesure-temps-deux-fronts-2.py 27 lignes
GitHub
############## mesure de temps entre 2 fronts ################
from machine import Pin
from time import sleep, sleep_ms, sleep_us, ticks_ms, ticks_us
cont = True
led = Pin(2, Pin.OUT)
bpA = Pin(25, Pin.IN)
bpB = Pin(34, Pin.IN)
print("Attente du front montant de bpA")
while not bpA.value(): # attente a l'etat bas
pass
start = ticks_ms() # start sur front montant 1
led.value(1)
print("Attente du front montant de bpB")
while not bpB.value(): # attente a l'etat bas
pass
stop = ticks_ms() # stop sur front montant 2
led.value(0)
delta = stop - start
print("Temps entre deux fronts : {0} ms \n".format(delta))
mesure-temps-deux-fronts-3.py — Version avec précaution initiale .python cours-exemples/timing/mesure-temps-deux-fronts-3.py 35 lignes
GitHub
############## mesure de temps entre 2 fronts ################
from machine import Pin
from time import sleep, sleep_ms, sleep_us, ticks_ms, ticks_us
cont = True
led = Pin(2, Pin.OUT)
bpA = Pin(25, Pin.IN)
bpB = Pin(34, Pin.IN)
print("Attente de l'etat BAS de A")
while bpA.value(): # attente a l'etat haut
pass
print("Attente du front montant de bpA")
while not bpA.value(): # attente a l'etat bas
pass
start = ticks_ms() # start sur front montant 1
led.value(1)
print("Attente de l'etat BAS de B")
while bpB.value(): # attente a l'etat haut
pass
print("Attente du front montant de bpB")
while not bpB.value(): # attente a l'etat bas
pass
stop = ticks_ms() # stop sur front montant 2
led.value(0)
delta = stop - start
print("Temps entre deux fronts : {0} ms \n".format(delta))
🔄 Séquenceur
Boucles rapides non-bloquantes et machines à états.
1 Le problème du séquenceur bloquant
Le programme suivant fait clignoter deux LEDs avec un décalage de phase — mais de façon bloquante : pendant chaque sleep(0.25), le microcontrôleur ne peut rien faire d'autre.
while True:
led_bleue.on()
sleep(0.25)
led_verte.on()
sleep(0.25)
led_bleue.off()
sleep(0.25)
led_verte.off()
sleep(0.25) Impossible d'ajouter une 3ème LED à fréquence différente, ou de détecter un appui bouton pendant les 0,25 s d'attente.
sequenceur.py — Séquenceur bloquant — illustre la limite .python cours-exemples/sequenceur/sequenceur.py 12 lignes
GitHub
from machine import Pin
from time import sleep
led_bleue = Pin(2, Pin.OUT)
led_verte = Pin(18, Pin.OUT)
while True:
led_bleue.on() ; sleep(0.25)
led_verte.on() ; sleep(0.25)
led_bleue.off() ; sleep(0.25)
led_verte.off() ; sleep(0.25)
2 La solution — boucle rapide et opérateur modulo
La solution non-bloquante : la boucle tourne très rapidement (10 ms) et incrémente un compteur. Chaque tâche est déclenchée grâce à l'opérateur modulo (%) :
compt = 0
while True:
compt += 1
if compt % 50 == 0: # toutes les 500 ms (50 × 10 ms)
led_bleue.value(not led_bleue.value())
if (compt + 25) % 50 == 0: # idem, avec 250 ms de décalage
led_verte.value(not led_verte.value())
# ici on peut ajouter autant de tâches que l'on veut
sleep_ms(10) # temps de cycle : 10 ms Pour des fréquences plus élevées (jusqu'à 500 Hz), on passe le temps de cycle à 1 ms (sleep_ms(1)) et on ajuste les modulos en conséquence.
boucle-rapide-simple.py — Boucle 10 ms non-bloquante (2 LEDs) .python cours-exemples/sequenceur/boucle-rapide-simple.py 17 lignes
GitHub
from machine import Pin
from time import sleep, sleep_ms
led_bleue = Pin(2, Pin.OUT)
led_verte = Pin(18, Pin.OUT)
compt = 0
while compt < 400 : # on boucle durant 4 s (400 * 10 ms)
compt +=1 # on incremente chaque 10 ms
if compt % 50 == 0 : # compt % 50 est vrai chaque 0.5 s
led_bleue.value(not led_bleue.value()) # on bascule la LED bleue.
if (compt + 25) % 50 == 0 : # vrai chaque 0.5 s, mais 0.25 s plus tot
led_verte.value(not led_verte.value()) # on bascule la LED verte
sleep_ms(10) # temps de cycle 10 ms
boucle-rapide-plus.py — Boucle 1 ms — LED à 500 Hz .python cours-exemples/sequenceur/boucle-rapide-plus.py 15 lignes
GitHub
from machine import Pin
from time import sleep, sleep_ms, sleep_us
led_bleue = Pin(2, Pin.OUT)
led_verte = Pin(18, Pin.OUT)
compt = 0
while compt < 4000 : # on arrete la While apres 4 secondes
compt +=1 # on incremente chaque 1000 us
if compt % 1 == 0 : # compt % 1 est vrai a chaque boucle
led_bleue.value(not led_bleue.value()) # on bascule la LED bleue
sleep_us(1000) # temps de cycle 1 ms
⚡ Interruptions
IRQ sur front montant/descendant — réaction immédiate.
1 Principe des interruptions matérielles
Une interruption (IRQ — Interrupt Request) est un mécanisme hardware : lorsqu'un événement survient sur une broche (front montant ou descendant), l'ESP32 interrompt immédiatement ce qu'il fait, exécute une fonction spéciale (handler), puis reprend exactement là où il était.
Contrairement à la scrutation (polling), aucun événement n'est raté — même si la boucle principale est en train de dormir ou de calculer.
def handler(pin): # la fonction appelée automatiquement
led.value(not led.value())
bpA = Pin(25, Pin.IN)
bpA.irq(trigger=Pin.IRQ_RISING, handler=handler)
# maintenant la boucle principale fait ce qu'elle veut
while True:
compt += 1
sleep(0.25) # l'IRQ fonctionne même pendant le sleep ! interruption1.py — IRQ front montant — principe de base .python cours-exemples/interruptions/interruption1.py 26 lignes
GitHub
from machine import Pin
from time import sleep
# le parametre pin est fourni par la methode irq lors de l'appel
# ce parametre est la pin qui cause l'interruption
def gestion_interrupt(pin): # fonction d'interruption
led_bleue.value(not led_bleue.value()) # bascule l'etat de LED bleue
print('Interruption causee par :', pin) # affiche la pin en cause
bpA = Pin(25, Pin.IN)
led_bleue = Pin(2, Pin.OUT)
# configure l'interruption sur le front montant de bpA,
# et renvoie vers la fonction gestion_interrupt pour son traitement
bpA.irq(trigger=Pin.IRQ_RISING, handler = gestion_interrupt)
compt = 0
while True: # boucle principale qui compte les 0.25 s
compt = compt + 1
print(compt)
sleep(0.25)
2 La règle d'or : handler court + flag global
Règle critique : la fonction handler doit être la plus courte possible. Pas de print(), pas de sleep(), pas de calcul lourd. Pourquoi ? Parce que pendant l'exécution du handler, le programme principal est suspendu.
La bonne pratique : dans le handler, on lève uniquement un flag global (inter = True). La boucle principale détecte ce flag, effectue le traitement et remet le flag à False.
inter = False
def handler(pin):
global inter
led.value(not led.value()) # action immédiate : OK (rapide)
inter = True # flag levé : boucle principale traitera le reste
while True:
if inter:
print('Interruption !') # print ici, pas dans le handler
inter = False
sleep(0.25) interruption3.py va plus loin : l'interruption initialise un compteur à 12 ; la boucle principale le décrémente toutes les 0,25 s et allume la LED si ce compteur est positif → LED allumée 3 s après chaque appui.
interruption2.py — IRQ avec flag global — bonne pratique .python cours-exemples/interruptions/interruption2.py 30 lignes
GitHub
from machine import Pin
from time import sleep
# le parametre pin est fourni par la methode irq lors de l'appel
# ce parametre est la pin qui cause l'interruption
def gestion_interrupt(pin): # fonction d'interruption
led_bleue.value(not led_bleue.value()) # bascule l'etat de LED bleue
global inter # definition de la variable globale
inter = True # la variable globale est mise a True
global interrupt_pin # definition de la variable globale
interrupt_pin = pin # correspond a la pin d'interruption
bpA = Pin(25, Pin.IN)
led_bleue = Pin(2, Pin.OUT)
# configure l'interruption sur le front montant de bpA,
# et renvoie vers la fonction gestion_interrupt pour son traitement
bpA.irq(trigger=Pin.IRQ_RISING, handler = gestion_interrupt)
compt = 0
inter = False
while True: # boucle principale qui compte les 0.25 s
compt = compt + 1
print(compt)
if inter :
print('Interruption causee par :', interrupt_pin)
inter = False
sleep(.25)
interruption3.py — LED active 3 s sur interruption .python cours-exemples/interruptions/interruption3.py 33 lignes
GitHub
from machine import Pin
from time import sleep
# le parametre pin est fourni par la methode irq lors de l'appel
# ce parametre est la pin qui cause l'interruption
def gestion_interrupt(pin): # fonction d'interruption
global inter # definition de la variable globale
inter = True # la variable globale est mise a True
global interrupt_pin # definition de la variable globale
interrupt_pin = pin # correspond a la pin d'interruption
bpA = Pin(25, Pin.IN)
led_bleue = Pin(2, Pin.OUT)
# configure l'interruption sur le front montant de bpA,
# et renvoie vers la fonction gestion_interrupt pour son traitement
bpA.irq(trigger=Pin.IRQ_RISING, handler = gestion_interrupt)
compt = 0
inter = False
temps = 0
while True: # boucle principale qui compte les 0.25 s
compt = compt + 1
print(compt)
temps = temps - 1
led_bleue.value(temps > 0) # allume LED si temps > 0
if inter :
print('Interruption causee par :', interrupt_pin)
temps = 12 # temps = 12 pour duree 3 s d'allumage LED
inter = False
sleep(.25)
Atelier 01 — Serveur web ESP32
Mets en pratique ces concepts avec un projet WiFi complet.