Aller au contenu principal

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.
Jusqu'ici, le code s'exécutait sur l'ordinateur : il lisait un fichier, affichait un résultat. Sur une carte ESP32, le même langage sert à autre chose : allumer une LED, lire un bouton, poser une tension sur un fil. Ce chapitre fait le pont. On va voir ce que la carte ajoute, ce qu'elle retire, et comment écrire dès aujourd'hui (sur le PC, sans matériel) un programme dont la logique sera exactement celle qui tournera sur la carte.

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.

Définition

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.

Définition

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é :

Définition

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 de matplotlib, pas de pandas, pas de scipy. Ces bibliothèques n'existent pas en MicroPython. Un import numpy nous renverra ImportError: 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.
À retenir
Règle de travail, recommandée par le sujet du projet : tout code qui n'a pas besoin des broches se met au point sur le PC, où le débogage est confortable. On ne passe sur la carte que pour ce qui touche vraiment à l'électronique.

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.

main.py
Sortie
>_ Prêt à exécuter…

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.

main.py
Sortie
>_ Prêt à exécuter…

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.

Attention

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 :

main.py
Sortie
>_ Prêt à exécuter…

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.

Définition

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.

main.py
Sortie
>_ Prêt à exécuter…

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.

1 ms par bit1 000 bauds8 bits, 8 ms en tout
TX0110100108 ms
Un octet à 1000 bauds. Chaque palier dure une milliseconde, l'octet entier passe en huit. Survoler le dessin donne l'instant de chaque bit.
100 ms par bit10 bauds8 bits, 800 ms en tout
TX011010010800 ms
Le même octet à 10 bauds. Le dessin est identique, seule l'échelle de temps a changé : l'octet prend maintenant huit dixièmes de seconde. C'est la seule chose que fixe le débit.

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.

1 ms par bit1 000 bauds16 bits, 16 ms en tout
NRZ00001111Manchester010101011010101008 ms16 ms
Le même message dans les deux codages, sur la même échelle de temps. En NRZ, quatre millisecondes sans le moindre front : le récepteur doit compter à l'aveugle, et il termine en 8 ms. En Manchester, une transition au milieu de chaque bit quoi qu'il arrive, mais 16 ms de ligne occupée.

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.

Définition

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.

main.py
Sortie
>_ Prêt à exécuter…

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.

main.py
Sortie
>_ Prêt à exécuter…

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 :

main.py
Sortie
>_ Prêt à exécuter…

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.

À retenir
Un récepteur doit se méfier de tout : une trame peut arriver tronquée, sans >, 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.

1 ms par bit1 000 bauds10 bits, 10 ms en tout
AZZ0110ZZZZBZZZZZZZ10Zle filZZ0110Z10ZA prend la paroleB attend puis parle08 ms
  • 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.
Bus partagé, règle respectée. À aucun instant deux stations ne forcent la ligne. La ligne « le fil » est ce qu'un observateur mesurerait au bout du câble.

Et voici ce qui se passe quand B n'écoute pas.

1 ms par bit1 000 bauds8 bits, 8 ms en tout
AZZ0110ZZBZZZ00ZZZle filZZ0000ZZB émet sans écouter08 ms
  • 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.
Collision. Aux bits 3 et 4, A force le niveau haut et B le niveau bas sur le même fil. La zone rouge marque les instants où la tension n'a plus de valeur définie, et les deux messages y sont perdus.

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.

main.py
Sortie
>_ Prêt à exécuter…

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

Vérification rapideon peut se reprendre

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 True sans 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 etat et des if suffisent à é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.