Piloter une carte ESP32 : du PC à MicroPython
Ce que ce chapitre apporte
- Distinguer Python sur PC et MicroPython sur microcontrôleur, et savoir ce que chacun sait faire.
- Simuler une entrée et une sortie par de simples variables tant qu'on n'a pas la carte.
- Écrire la boucle principale d'un programme embarqué.
- Relier durée d'un bit, cadence et débit en bauds.
- Écrire une machine à états simple avec des if et une variable etat.
- Construire une trame texte et la relire avec split().
- Expliquer pourquoi on écoute la ligne avant d'émettre.
La même langue, deux cibles
La carte employée ici est un ESP32, un microcontrôleur à 3,3 volts qui embarque MicroPython. Dans Thonny, on écrit du Python. Ce qui change, c'est qui l'exécute : l'ordinateur, ou l'ESP32 branché sur le port USB. On choisit la cible en bas à droite de la fenêtre.
MicroPython est une version allégée de Python conçue pour tenir dans la mémoire d'un microcontrôleur. La syntaxe est la même : variables, if, for, while, fonctions, listes, dictionnaires. Ce sont les bibliothèques disponibles qui changent.
Autrement dit : tout ce qu'on a appris dans les chapitres précédents fonctionne sur la carte. Une boucle for est une boucle for. Une fonction est une fonction. On n'apprend pas un nouveau langage, on change de destination.
Ce que la carte ajoute : les entrées/sorties
C'est la vraie différence, et elle est énorme. Le PC ne sait pas mettre 3,3 volts sur un fil. L'ESP32, si : sur chacune de ses broches numérotées io18, io19, etc. (io pour input/output, entrée/sortie). Le nombre est celui du GPIO, et c'est lui qu'on passe à Pin(), pas la position physique sur le connecteur.
Une sortie numérique est une broche que le programme force à l'état haut (environ 3,3 V, ce qu'on note 1) ou à l'état bas (0 V, noté 0). Une entrée numérique est une broche que le programme lit : elle vaut 1 si la tension est haute, 0 si elle est basse.
En MicroPython, cela tient en trois lignes :
# Ce code ne s'exécute que sur la carte, pas dans l'éditeur de cette page.
from machine import Pin
ligne = Pin(18, Pin.OUT) # la broche 18 devient une sortie
ligne.value(1) # état haut
ligne.value(0) # état bas
bouton = Pin(19, Pin.IN) # la broche 19 devient une entrée
print(bouton.value()) # 0 ou 1
Le troisième état : la haute impédance
Sur le projet, plusieurs stations partagent un seul et même câble. Si deux d'entre elles forcent la ligne en même temps, l'une à 0 et l'autre à 1, elles se battent : le courant passe de l'une à l'autre, la tension lue ne veut rien dire, et les composants chauffent. C'est un conflit électrique.
D'où un troisième état, indispensable sur un bus partagé :
La haute impédance est l'état d'une broche qui ne force rien du tout : elle est comme débranchée, elle laisse la ligne libre pour les autres. On l'obtient en repassant la broche en entrée, ou en désactivant le driver de bus (la puce MAX485 du montage).
Une station bien élevée passe donc son temps en haute impédance, et ne force la ligne que pendant qu'elle émet. Retenons les trois états : 0, 1, et rien du tout.
Ce que la carte retire
C'est le piège où tout le monde tombe une fois. Un ESP32 dispose d'environ 4 Mo de mémoire de programme et de 520 ko de mémoire vive, contre plusieurs gigaoctets sur le PC. Le rapport est de l'ordre de dix mille. Conséquences très concrètes :
- Pas de
numpy, pas dematplotlib, pas depandas, pas descipy. Ces bibliothèques n'existent pas en MicroPython. Unimport numpynous renverraImportError: no module named 'numpy'. Si on veut tracer une courbe, on envoie les valeurs au PC et on trace sur le PC. - Pas d'installation de paquets depuis Internet. La carte n'a pas de connexion directe ; les bibliothèques dont on a besoin (celle du capteur DHT11, celle de l'écran) se copient à la main depuis Thonny.
- La mémoire est comptée. Construire une liste de plusieurs milliers d'éléments peut faire redémarrer la carte. Sur un microcontrôleur, on traite les données au fil de l'eau plutôt que de tout stocker.
Simuler la ligne avec des variables
Tant qu'on n'a pas la carte sous la main (ou tant que le protocole n'est pas au point), remplace la broche par une variable. Le reste du programme ne voit pas la différence, et on peut tester la logique dans l'éditeur de cette page.
L'idée : une fonction emettre() qui, au lieu de piloter une broche, ajoute la valeur à une liste qui joue le rôle du fil.
Le jour où on branche la carte, une seule ligne change : ligne.append(bit) devient broche.value(bit). Toute la logique qu'on aura validée sur PC reste valable. C'est exactement pour ça qu'on isole l'accès au matériel dans une petite fonction.
Même principe pour la lecture : au lieu de lire une broche, on lit la case suivante d'une liste préparée à l'avance.
La boucle principale
Un programme sur PC a un début et une fin : il calcule, il affiche, il s'arrête. Un programme embarqué, lui, ne s'arrête jamais. Tant que la carte est alimentée, il tourne. Sa structure est toujours la même :
# Ce code ne s'exécute que sur la carte.
from machine import Pin
import time
led = Pin(2, Pin.OUT)
# 1. Préparation : une seule fois, au démarrage
etat = 0
# 2. Boucle principale : sans fin
while True:
etat = 1 - etat # bascule 0 -> 1 -> 0 -> 1…
led.value(etat)
time.sleep(0.5) # une demi-seconde
Trois choses à remarquer. D'abord while True : la condition est toujours vraie, donc la boucle ne se termine pas. Ensuite, tout ce qui doit être fait une seule fois (créer les broches, initialiser les variables) est avant la boucle. Enfin, time.sleep(0.5) met la carte en pause pendant une demi-seconde.
Ne jamais écrire while True sans pause à l'intérieur dans l'éditeur de cette page : le navigateur se figerait. Sur la carte, ce n'est pas grave. C'est même le fonctionnement normal.
Sur PC, pour tester la logique d'une boucle principale, on la remplace par un for avec un nombre de tours fini :
Cadence, durée d'un bit et bauds
Sur le fil, il n'y a que des tensions. Si on pose un 1 puis un autre 1, comment celui d'en face saura-t-il qu'on a envoyé deux bits, et pas un seul long ? Réponse : parce que les deux stations se sont mises d'accord à l'avance sur la durée d'un bit.
Le débit en bauds est le nombre de symboles envoyés par seconde. À 1 baud, un bit dure 1 seconde. À 10 bauds, un bit dure 0,1 seconde. Durée d'un bit = 1 ÷ débit.
C'est pour ça que le sujet propose de commencer à 1 baud : un bit par seconde, c'est assez lent pour suivre les changements à l'œil sur une LED, ou pour les relever au multimètre. On accélère seulement une fois que le protocole marche.
Le résultat est instructif : à 1 baud, envoyer <S01:OK> prend plus d'une minute. Et si on ajoute un codage Manchester, qui envoie deux symboles par bit, on double encore cette durée. Un codage n'est jamais gratuit : il coûte du temps de ligne, et c'est le prix de la synchronisation.
Voici ce que cela donne sur le fil. Le même octet, 0110 1001, posé à deux cadences différentes.
Les deux dessins sont superposables, et c'est le point : le débit ne change pas la forme du signal, il change sa durée. Les deux stations doivent seulement s'accorder sur cette durée, sans quoi celle qui écoute découpe au mauvais endroit et lit n'importe quoi.
Pourquoi le Manchester coûte le double, et ce qu'il achète
Le codage NRZ pose directement le bit sur la ligne. Sa faiblesse se voit à l'œil : une suite de bits identiques donne un palier immobile, et le récepteur n'a plus rien pour se recaler.
En Manchester, chaque bit devient deux demi-bits opposés : un 0 s'écrit 01, un 1 s'écrit 10. Il y a donc une transition au milieu de chaque bit, sans exception, et le récepteur y recale son horloge à chaque fois. Il ne peut plus dériver.
Le prix est visible sur le dessin : deux fois plus de symboles pour le même message, donc deux fois plus de temps de ligne. C'est l'arbitrage, et il n'a rien d'arbitraire : on paie en débit ce qu'on gagne en robustesse.
Sur la carte, la cadence se tient avec une pause :
# Ce code ne s'exécute que sur la carte.
import time
from machine import Pin
ligne = Pin(18, Pin.OUT)
DUREE_BIT = 1.0 # 1 baud
for bit in [1, 0, 1, 1, 0, 0, 1]:
ligne.value(bit)
time.sleep(DUREE_BIT)
Une machine à états
La station ne fait pas qu'une seule chose : tantôt elle attend, tantôt elle émet, tantôt elle écoute ce que raconte une autre station. Elle ne peut pas faire les trois en même temps. On dit qu'elle est dans un état, et qu'elle en change quand un événement se produit.
Une machine à états est une façon d'organiser un programme : une variable retient l'état courant, et à chaque tour de boucle le programme regarde cet état pour décider quoi faire, puis éventuellement en change.
Pas besoin d'outil compliqué : une variable etat et des if suffisent.
Lisons bien la structure : le premier niveau de if regarde où on est, le second regarde ce qui arrive. C'est le squelette de la station, et il nous servira tel quel : au repos la broche est en haute impédance, en émission elle pose les bits, en écoute elle les lit.
Construire et relire une trame
Le datagramme est du texte : <S03:T=20;V=80;MSG=OK>. Le construire, c'est de l'assemblage de chaînes ; le relire, c'est du découpage. Rien de plus.
Pour la relecture, split() découpe une chaîne à chaque séparateur et renvoie une liste. On découpe en cascade, en suivant la structure de la trame :
Le 1 dans split(":", 1) demande de ne découper qu'une seule fois, au premier : rencontré. Sans lui, un message contenant lui-même un : casserait tout. C'est le genre de détail qui doit figurer dans la documentation du protocole : quels caractères sont interdits dans un message, et pourquoi.
>, avec un champ vide. Avant de découper, vérifie que la trame commence bien par < et finit bien par >. Sinon, la jeter. Un programme embarqué qui plante sur une trame abîmée, c'est une station muette jusqu'au prochain redémarrage.
Écouter avant d'émettre
Dernière brique, et pas la moindre. Le câble est partagé par toutes les stations. Si deux d'entre elles émettent en même temps, il se passe deux choses désagréables : électriquement, deux sorties se battent sur le même fil ; logiquement, les deux messages se mélangent et personne ne reçoit rien d'exploitable. C'est une collision.
La règle est celle d'une conversation polie : on écoute avant de parler. Avant d'émettre, la station regarde la ligne. Si elle y voit de l'activité, elle attend. Sinon, elle prend la parole.
Les deux situations se lisent d'un coup sur le fil. Voici d'abord ce qui se passe quand la règle est respectée : chaque station reste en haute impédance tant qu'elle n'a rien à dire, et n'émet que sur une ligne libre.
- Le trait pointillé à mi-hauteur est la haute impédance : la broche est repassée en entrée, elle ne force rien, et le fil reste disponible pour les autres. Une station bien élevée y passe tout le temps où elle n'émet pas.
- Aucune collision : à chaque instant, une seule station au plus force la ligne, et c'est elle qu'on lit sur le fil.
Et voici ce qui se passe quand B n'écoute pas.
- Le trait pointillé à mi-hauteur est la haute impédance : la broche est repassée en entrée, elle ne force rien, et le fil reste disponible pour les autres. Une station bien élevée y passe tout le temps où elle n'émet pas.
- Conflit électrique sur les bits 3, 4 : A et B forcent des niveaux opposés en même temps. La tension lue ne veut plus rien dire, du courant passe d'une sortie à l'autre, et les composants chauffent. C'est ce que l'écoute avant émission cherche à éviter, sans toujours y parvenir : un signal met du temps à parcourir le fil, et la ligne paraît libre à qui se trouve plus loin.
Aux bits 3 et 4, A pose un 1 et B pose un 0. Deux sorties se battent sur le même conducteur : du courant passe de l'une à l'autre, la tension prend une valeur intermédiaire qui n'appartient à personne, et les composants chauffent. Le dessin marque ces deux instants en rouge, et le message des deux stations est perdu, pas seulement retardé.
C'est très exactement ce que la haute impédance sert à éviter, et pourquoi une station bien élevée y passe l'essentiel de son temps.
Deux remarques de terrain. D'abord, si toutes les stations attendent puis émettent au même instant dès que la ligne se libère, elles entrent de nouveau en collision : on ajoute donc en général une petite attente de durée différente d'une station à l'autre. Ensuite, il faut décider quoi faire quand la ligne ne se libère jamais : réessayer indéfiniment, ou abandonner au bout de N tentatives. Ces deux règles font partie du protocole, et doivent être écrites.
Vérification
1.Qu'est-ce que MicroPython retire par rapport au Python d'un ordinateur ?
2.Pourquoi une carte a-t-elle une boucle principale sans fin, là où un script se termine ?
3.Une broche configurée en entrée et laissée non connectée lit quoi ?
4.La durée d'un bit vaut 1 milliseconde. Quel est le débit ?
Synthèse
- MicroPython, c'est le même langage sur une autre cible : ce sont les bibliothèques et les ressources qui changent.
- La carte ajoute les entrées/sorties (
Pin), avec trois états utiles sur un bus partagé : 0, 1, haute impédance. - La carte retire numpy, matplotlib, l'installation de paquets, et beaucoup de mémoire. Mettre la logique au point sur PC.
- Tant qu'il n'y a pas de matériel, simule la ligne par une liste et isole l'accès au matériel dans une petite fonction.
- Un programme embarqué : une préparation, puis une boucle principale
while Truesans fin. - Durée d'un bit = 1 ÷ débit. À 1 baud, un bit dure une seconde ; le Manchester double le temps de ligne.
- Une variable
etatet desifsuffisent à écrire une machine à états (repos / émission / écoute). - Une trame se construit par assemblage et se relit avec
split(), en vérifiant d'abord qu'elle est complète. - Sur un média partagé, on écoute avant d'émettre, sinon c'est la collision.