Accueil Cours Programmation temps réel
⏱️ Avancé Intermédiaire

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

TechniqueUsageComplexité
time.sleep()Code simple, une seule tâcheSimple
ticks_ms() + vérification2-5 tâches indépendantesMoyen
Machine à étatsSéquences complexesAvancé
Interruptions (IRQ)Réaction immédiate à un signalAvancé

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 while attendent 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.

Démarrer l'atelier →