Séance 25 · Partie D · Électronique et embarqué
MicroPython 2 :
bus et WiFi.
Les capteurs sérieux ne renvoient pas une tension : ils parlent. Un gyroscope, un écran, un capteur de pression échangent des octets sur un bus. Et un ESP32 parle aussi au reste du monde : votre robot devient un objet connecté.
Durée : 2 séances · Objectifs : le bus I2C (scan, registres, MPU6050, écran OLED), un mot sur SPI et UART, WiFi sur ESP32 / Pico W, requêtes HTTP, un serveur web sur la carte, asyncio embarqué.
Ce que vous saurez faire à la fin
- Brancher n’importe quel capteur I2C et lire ses registres, avec ou sans bibliothèque.
- Afficher des informations sur un petit écran.
- Connecter la carte au WiFi, envoyer des mesures à un serveur, piloter le robot depuis un téléphone.
- Faire cohabiter capteurs, réseau et contrôle avec
asyncio.
Matériel : ESP32 (ou Pico W), MPU6050 (gyroscope/accéléromètre, 3 €), écran OLED SSD1306 128×64 I2C (4 €), fils Dupont. Un réseau WiFi 2,4 GHz avec son mot de passe.
01 / I2C
Le bus I2C : deux fils, jusqu’à 127 périphériques
Carte Capteur 1 Capteur 2
SDA ──────┬──────────── SDA ──────── SDA
SCL ──────┼─┬────────── SCL ──────── SCL
3V3 ─[4k7]┘ │ VCC VCC
3V3 ─[4k7]──┘
GND ────────────────── GND ──────── GND
SDA : données (dans les deux sens)
SCL : horloge, imposée par la carte (maître)
Chaque périphérique a une ADRESSE (7 bits)
Résistances de rappel : souvent déjà sur les modulesfrom machine import Pin, I2C
# ESP32 : SDA=21, SCL=22 par défaut ; Pico : I2C(0, sda=Pin(4), scl=Pin(5))
i2c = I2C(0, sda=Pin(21), scl=Pin(22), freq=400_000)
adresses = i2c.scan()
print([hex(a) for a in adresses])
# ex. ['0x3c', '0x68'] : l'écran OLED et le MPU6050Le scan est le premier réflexe : si l’adresse n’apparaît pas, c’est un problème de câblage, pas de code.
Adresses courantes : 0x68 MPU6050, 0x3C OLED, 0x76 BMP280 (pression), 0x27 LCD, 0x48 ADS1115 (ADC 16 bits).
Comment ça marche
Le maître envoie l’adresse ; le périphérique concerné répond ; puis on lit ou écrit des octets. Le bus est partagé : un seul parle à la fois, l’horloge synchronise. 100 ou 400 kHz : lent (40 Ko/s) mais suffisant pour des capteurs. Les octets ont une structure : le périphérique expose des registres numérotés (séance 13), comme une petite mémoire ; lire le registre 0x3B du MPU6050 donne l’accélération X. Les fiches techniques (datasheets) sont des cartes de ces registres.
01 / I2C
Lire un MPU6050 sans bibliothèque : parler aux registres
from machine import Pin, I2C
import struct, time
i2c = I2C(0, sda=Pin(21), scl=Pin(22))
MPU = 0x68
# Réveiller le capteur : registre PWR_MGMT_1 (0x6B) ← 0
i2c.writeto_mem(MPU, 0x6B, bytes([0]))
def lire_brut():
# 14 octets à partir de 0x3B : ax ay az temp gx gy gz, big-endian, signés 16 bits
d = i2c.readfrom_mem(MPU, 0x3B, 14)
return struct.unpack(">7h", d) # séance 13 : struct et entiers signés
while True:
ax, ay, az, t, gx, gy, gz = lire_brut()
# Pleine échelle par défaut : ±2 g → 16384 LSB/g ; ±250 °/s → 131 LSB/(°/s)
print(f"a = ({ax/16384:+.2f}, {ay/16384:+.2f}, {az/16384:+.2f}) g "
f"gyro = ({gx/131:+6.1f}, {gy/131:+6.1f}, {gz/131:+6.1f}) °/s "
f"T = {t/340 + 36.53:.1f} °C")
time.sleep(0.2)Pourquoi lire les registres soi-même
Une bibliothèque cache ces cinq lignes ; mais quand elle n’existe pas pour votre capteur, ou qu’elle bogue, savoir lire la datasheet et écrire readfrom_mem vous rend autonome. Les données arrivent en big-endian (octet de poids fort d’abord) et signées : struct fait la conversion. Le vecteur accélération au repos pointe vers le haut (réaction à la gravité) et donne l’inclinaison ; le gyroscope donne la vitesse de rotation, à intégrer pour l’angle — avec la dérive de la séance 19. La fusion des deux (filtre complémentaire) est le défi ★★.
01 / I2C
Un écran OLED : afficher l’état du robot
# Copier ssd1306.py (bibliothèque MicroPython officielle) sur la carte
from machine import Pin, I2C
import ssd1306, time
i2c = I2C(0, sda=Pin(21), scl=Pin(22))
ecran = ssd1306.SSD1306_I2C(128, 64, i2c)
ecran.fill(0) # tout noir
ecran.text("ROVER-1", 0, 0) # texte 8x8 px, position (x, y)
ecran.text("bat 87%", 0, 12)
ecran.rect(0, 30, 128, 10, 1) # cadre
ecran.fill_rect(2, 32, int(124 * 0.87), 6, 1) # barre de batterie
ecran.line(0, 50, 127, 63, 1)
ecran.show() # envoyer le tampon à l'écran (~20 ms)
# Un graphe défilant de la distance
historique = [0] * 128
while True:
d = 40 + 20 * math.sin(time.ticks_ms() / 500) # remplacer par le capteur
historique.pop(0); historique.append(int(d))
ecran.fill(0)
for x, v in enumerate(historique):
ecran.pixel(x, 63 - min(63, v), 1)
ecran.show(); time.sleep_ms(50)L’écran est un tableau de 128 × 64 bits en RAM (1 Ko) : on dessine dedans, puis show() l’envoie par I2C.
text,pixel,line,rect,fill_rect: c’est le moduleframebuf.- Un affichage remplace le port série pour déboguer un robot autonome.
- Ne pas appeler
show()plus de 20 fois par seconde : le bus sature.
SPI et UART, en deux mots
SPI : 4 fils (horloge, deux données, sélection), beaucoup plus rapide (10+ Mbit/s) : écrans couleur, cartes SD, radios. UART : la liaison série de la séance 20, entre deux cartes ou vers un GPS, un module Bluetooth HC-05. Chaque bus a son module dans machine avec la même logique : configurer, write, read.
02 / WiFi
Connecter la carte au réseau
# boot.py ou début de main.py
import network, time
def connecter(ssid, mot_de_passe, essais=20):
wlan = network.WLAN(network.STA_IF) # mode station (client)
wlan.active(True)
if not wlan.isconnected():
wlan.connect(ssid, mot_de_passe)
for _ in range(essais):
if wlan.isconnected(): break
time.sleep(0.5)
if wlan.isconnected():
print("Connecté :", wlan.ifconfig()[0]) # l'adresse IP
return wlan
print("Échec WiFi")
return None
wlan = connecter("MonReseau", "motdepasse")# Envoyer une mesure à un serveur (HTTP)
import urequests as requests # ou requests selon la version
def envoyer(temp):
try:
r = requests.post("http://192.168.1.10:8000/mesure",
json={"robot": "rover1", "temp": temp},
timeout=5)
print(r.status_code, r.text)
r.close() # important : libérer la mémoire
except OSError as e:
print("réseau :", e)
# Lire l'heure sur Internet
import ntptime
ntptime.settime() # règle l'horloge de la carte (UTC)
print(time.localtime())Sécurité et bon sens
Ne mettez jamais le mot de passe WiFi dans un fichier que vous partagez : un secrets.py non versionné, importé. Le réseau est lent et peut échouer : timeout, try/except OSError, et jamais d’appel réseau dans la boucle de contrôle des moteurs (séance 20 : bloquant). Le mode point d’accès (network.AP_IF) crée un WiFi propre à la carte : le téléphone s’y connecte directement, sans box.
02 / WiFi
Un serveur web sur la carte : piloter depuis le téléphone
import socket
PAGE = """HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n
<html><body style="font-family:sans-serif;text-align:center">
<h1>Rover</h1><p>Distance : {d} cm</p>
<p><a href="/avance"><button style="font-size:2em">▲</button></a></p>
<p><a href="/gauche"><button style="font-size:2em">◀</button></a>
<a href="/stop"><button style="font-size:2em">■</button></a>
<a href="/droite"><button style="font-size:2em">▶</button></a></p>
</body></html>"""
def servir(wlan, moteurs, capteur):
s = socket.socket()
s.bind(("0.0.0.0", 80)); s.listen(2)
print("Ouvrir http://" + wlan.ifconfig()[0])
while True:
client, addr = s.accept() # bloquant : voir asyncio plus loin
requete = client.recv(512).decode()
chemin = requete.split(" ")[1] if " " in requete else "/"
if chemin == "/avance": moteurs.avancer(70)
elif chemin == "/gauche": moteurs.tourner(-50)
elif chemin == "/droite": moteurs.tourner(50)
elif chemin == "/stop": moteurs.stop()
client.send(PAGE.format(d=capteur.distance() or "?"))
client.close()Ce qui se passe
Un serveur web est un programme qui attend des connexions sur le port 80, lit une requête texte (GET /avance HTTP/1.1), et renvoie du texte (une page HTML). Le navigateur du téléphone fait le reste. Trente lignes, et votre robot a une télécommande. Limites : accept() bloque, donc pas de contrôle en parallèle — d’où asyncio ; et aucune sécurité — n’importe qui sur le WiFi pilote le robot. Pour du sérieux : WebSocket (temps réel) ou MQTT (messages légers entre objets).
02 / WiFi
Tout ensemble : asyncio sur la carte
import asyncio # uasyncio sur les anciennes versions
from machine import Pin
etat = {"distance": None, "commande": "stop"}
async def tache_capteur(capteur):
while True:
etat["distance"] = capteur.distance()
await asyncio.sleep_ms(60)
async def tache_controle(moteurs):
while True:
d = etat["distance"]
if d is not None and d < 15:
moteurs.stop(); etat["commande"] = "stop" # sécurité prioritaire
elif etat["commande"] == "avance":
moteurs.avancer(70)
await asyncio.sleep_ms(50)
async def gerer_client(lecteur, ecrivain):
requete = await lecteur.readline()
chemin = requete.decode().split(" ")[1] if b" " in requete else "/"
if chemin in ("/avance", "/stop"): etat["commande"] = chemin[1:]
ecrivain.write(PAGE.format(d=etat["distance"] or "?"))
await ecrivain.drain(); await ecrivain.wait_closed()
async def clignoter(led):
while True:
led.toggle(); await asyncio.sleep_ms(500)
async def main(capteur, moteurs):
asyncio.create_task(tache_capteur(capteur))
asyncio.create_task(tache_controle(moteurs))
asyncio.create_task(clignoter(Pin(2, Pin.OUT)))
await asyncio.start_server(gerer_client, "0.0.0.0", 80)
while True: await asyncio.sleep(1)
asyncio.run(main(capteur, moteurs))La séance 20 sur une puce
Quatre coroutines, un seul cœur, aucune ne bloque les autres : le capteur lit à 16 Hz, le contrôle décide à 20 Hz, le serveur répond quand un client arrive, la LED clignote. L’état partagé est un dictionnaire — sûr ici car asyncio ne change de tâche qu’aux await. La sécurité (obstacle → stop) est dans la tâche de contrôle, jamais dans le serveur : même si le réseau plante, le robot s’arrête devant le mur. C’est l’architecture d’un vrai robot connecté.
03 / Défis
Défi ★ — Tableau de bord
Consigne
Sur l’écran OLED : nom du robot, adresse IP (ou « pas de WiFi »), distance ultrason en gros chiffres, inclinaison du MPU6050 en degrés, une barre de batterie (tension lue sur un pont diviseur, séance 21). Rafraîchi 5 fois par seconde, sans sleep bloquant (timer ou ticks). Bonus : l’écran affiche « ! » clignotant si la distance < 15 cm.
Correction (structure)
def afficher():
ecran.fill(0)
ecran.text("ROVER-1 " + (wlan.ifconfig()[0] if wlan else "no wifi"), 0, 0)
d = capteur.distance()
ecran.text(f"{d:5.1f} cm" if d else " --- cm", 0, 16)
ecran.text(f"incl {inclinaison():+5.1f}", 0, 28)
ecran.rect(0, 44, 128, 8, 1); ecran.fill_rect(1, 45, int(126 * batterie()), 6, 1)
if d and d < 15 and (time.ticks_ms() // 250) % 2:
ecran.text("!", 120, 16)
ecran.show()
Timer(period=200, mode=Timer.PERIODIC, callback=lambda t: micropython.schedule(lambda _: afficher(), 0))micropython.schedule sort l’affichage (lent, alloue de la mémoire) de l’interruption du timer pour l’exécuter dans le fil principal. Sans cela : erreur « schedule queue full » ou plantage.
03 / Défis
Défi ★★ — Filtre complémentaire : un angle fiable
Consigne
L’accéléromètre donne un angle juste au repos mais bruité et faussé par les mouvements ; le gyroscope donne une vitesse angulaire précise mais qui dérive une fois intégrée (séance 19). Le filtre complémentaire les combine : angle = α × (angle + gyro × dt) + (1 − α) × angle_accel, avec α ≈ 0,98.
Implémenter sur la carte à 100 Hz. Comparer les trois angles (accel seul, gyro seul, filtré) envoyés au traceur série de Thonny (ou à liaison.py). Incliner lentement, puis secouer : que voit-on ? Tester α = 0,9 et 0,999.
Lecture
Le gyro seul dérive (biais intégré : 0,5 °/s × 10 s = 5°). L’accéléromètre seul est bruité et voit le choc. Le filtre à 0,98 prend le meilleur des deux : le gyro pour les mouvements rapides, l’accéléromètre pour recaler lentement. α trop grand : la dérive revient ; trop petit : le bruit revient. C’est un filtre de Kalman du pauvre, et il stabilise des drones et des gyropodes. Avec le vrai capteur, calibrez d’abord le biais du gyro (moyenne de 200 lectures au repos, à soustraire).
03 / Défis
Défi ★★★ — Esprit prépa : télémétrie robuste et horloge
Consigne
1. Concevoir un protocole binaire pour envoyer par WiFi (UDP, socket.SOCK_DGRAM) 50 fois par seconde : horodatage (ms, 32 bits), 3 accélérations et 3 vitesses angulaires (int16), distance (uint16), batterie (uint8), et un CRC-8 pour détecter les paquets corrompus. Utiliser struct.pack (séance 13). Quelle taille ? Quel débit ?
2. Sur PC, un récepteur Python qui vérifie le CRC, compte les paquets perdus (numéro de séquence) et trace la latence.
3. Les horloges de la carte et du PC ne sont pas synchronisées : proposer une méthode pour estimer le décalage et la dérive (indice : échange aller-retour, comme NTP), et la tester.
Direction
22 octets par paquet (21 + CRC), 8,8 kbit/s : négligeable pour le WiFi, énorme pour une liaison série à 9600 bauds — le binaire compte. Le CRC détecte toute erreur sur 1 bit et 99,6 % des erreurs multiples ; c’est un polynôme sur le corps à deux éléments (séance 13, XOR = addition modulo 2), théorie que vous verrez en prépa sous le nom de codes cycliques. UDP ne garantit ni l’ordre ni l’arrivée : le numéro de séquence mesure les pertes. Pour l’horloge : envoyer t₁, recevoir la réponse avec t₂ (carte) et t₃ (PC) ; décalage ≈ t₂ − (t₁ + t₃)/2 ; répéter et garder les aller-retours les plus courts. C’est l’algorithme de NTP, qui synchronise les horloges d’Internet à la milliseconde.
04 / Vérification
Que renvoie i2c.scan() ?
Deux questions supplémentaires
1. Pourquoi r.close() après une requête HTTP ? Pour libérer la mémoire du socket : sur 264 Ko, quelques requêtes non fermées suffisent à tout bloquer.
2. Où placer la sécurité « obstacle → stop » : dans le serveur web ou dans la tâche de contrôle ? Dans la tâche de contrôle, qui tourne quoi qu’il arrive au réseau.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| I2C | Bus 2 fils, maître/esclaves adressés, ~400 kbit/s. |
| Registre (capteur) | Case mémoire numérotée exposée par un périphérique. |
| SPI / UART | Bus rapide 4 fils / liaison série point à point. |
network.WLAN | Interface WiFi ; STA (client) ou AP (point d’accès). |
| HTTP | Requête texte / réponse ; base du web et des API. |
| Filtre complémentaire | Fusion gyro (rapide) + accéléromètre (stable). |
| CRC | Somme de contrôle polynomiale pour détecter les erreurs. |
Pour continuer
Le robot a des sens et une voix
Dernière séance de la partie D : assembler la liaison PC ↔ carte complète, avec un tableau de bord Python en direct.
À faire chez soi
- Réaliser le tableau de bord OLED et le filtre complémentaire avec le vrai MPU6050.
- Télécommander une LED (puis un servo) depuis le téléphone via le serveur web.
- Lire la datasheet du MPU6050 (registres 0x1B, 0x1C : pleine échelle) et changer la sensibilité du gyroscope à ±500 °/s.