Séance 24 · Partie D · Électronique et embarqué
MicroPython 1 :
Pico et ESP32.
Un Raspberry Pi Pico coûte 5 €, tourne à 133 MHz avec 264 Ko de RAM, et exécute… du Python. Tout ce que vous savez depuis la séance 02 fonctionne sur une carte de la taille d’un chewing-gum.
Durée : 2 séances · Objectifs : installer MicroPython, le REPL avec Thonny, machine.Pin, PWM, ADC, timers, main.py, différences avec CPython, comparaison honnête avec l’Arduino.
Ce que vous saurez faire à la fin
- Flasher une carte et dialoguer avec elle en Python interactif.
- Refaire tout ce qui a été fait en C sur Arduino, en Python.
- Choisir entre Arduino/C et Pico/MicroPython selon le projet.
- Comprendre ce que MicroPython ne peut pas faire.
Matériel : Raspberry Pi Pico (ou Pico W) ou ESP32 DevKit, câble USB de données (pas seulement de charge), Thonny installé. Les montages de la séance 21–23 se réutilisent, en 3,3 V.
01 / Démarrer
Installer et dire bonjour
- Pico : maintenir BOOTSEL, brancher l’USB, relâcher. Un disque « RPI-RP2 » apparaît. Y glisser le fichier
.uf2de micropython.org. La carte redémarre. - ESP32 : dans Thonny, Outils → Options → Interpréteur → « Installer ou mettre à jour MicroPython », choisir le port et la famille ESP32.
- Thonny : Interpréteur → « MicroPython (Raspberry Pi Pico) » ou « (ESP32) », port automatique.
- Dans la console (REPL) en bas, taper
print("bonjour"). La carte répond.
>>> import sys
>>> sys.implementation
(name='micropython', version=(1, 22, 0), ...)
>>> import gc
>>> gc.mem_free()
187456 # octets de RAM libres
>>> from machine import Pin
>>> led = Pin("LED", Pin.OUT) # Pico ; Pin(2) sur ESP32
>>> led.on()
>>> led.off()
>>> led.toggle()Le REPL exécute chaque ligne sur la carte, immédiatement. Pas de compilation, pas de téléversement : essayer, voir, corriger.
Ce qui tourne sur la carte
MicroPython est un interpréteur Python complet réécrit pour tenir dans 256 Ko de flash : le compilateur en bytecode et la machine virtuelle de la séance 01, sans le système d’exploitation. Quand vous tapez au REPL, le texte part par USB-série, la carte le compile et l’exécute, et renvoie le résultat. Thonny n’est qu’un terminal amélioré : vous pourriez utiliser liaison.py.
01 / Démarrer
Blink, et sa version non bloquante
# Version 1 : avec sleep (bloquant)
from machine import Pin
import time
led = Pin("LED", Pin.OUT) # Pico W : "LED" ; Pico : 25 ; ESP32 : 2
while True:
led.toggle()
time.sleep(0.5)# Version 2 : avec l'horloge (non bloquant)
from machine import Pin
import time
led = Pin("LED", Pin.OUT)
prochain = time.ticks_ms()
while True:
if time.ticks_diff(time.ticks_ms(), prochain) >= 0:
led.toggle()
prochain = time.ticks_add(prochain, 500)
# ... autres tâches ...# Version 3 : avec un Timer matériel
from machine import Pin, Timer
led = Pin("LED", Pin.OUT)
Timer(period=500, mode=Timer.PERIODIC,
callback=lambda t: led.toggle())
# Le programme principal est libre : le timer
# appelle la fonction tout seul, comme une
# interruption. Sur ESP32 : Timer(0, ...)
while True:
pass # ou tout autre travailticks_ms() déborde après 2³⁰ ms : ticks_diff et ticks_add gèrent le débordement correctement (séance 13). Ne jamais soustraire directement.
Arrêter un programme
Ctrl+C dans le REPL interrompt la boucle (KeyboardInterrupt, séance 08). Ctrl+D redémarre la carte en douceur. Si la carte semble bloquée, le bouton Stop de Thonny envoie ces deux commandes. Un timer continue de tourner même après Ctrl+C : timer.deinit() pour l’arrêter.
02 / Entrées-sorties
Le module machine : Pin, ADC, PWM
from machine import Pin, ADC, PWM
import time
# Bouton avec rappel interne : pressé = 0
bouton = Pin(14, Pin.IN, Pin.PULL_UP)
print(bouton.value())
# Entrée analogique : 16 bits (0..65535) sur Pico,
# quelle que soit la résolution réelle (12 bits)
pot = ADC(26) # GP26 = ADC0 ; ESP32 : ADC(Pin(34))
brut = pot.read_u16()
tension = brut * 3.3 / 65535
print(brut, round(tension, 2), "V")
# PWM : fréquence et rapport cyclique 16 bits
led = PWM(Pin(15))
led.freq(1000)
led.duty_u16(32768) # 50 %
# ESP32 : led.duty(512) sur 10 bits, ou duty_u16 selon la version| Arduino C | MicroPython |
|---|---|
pinMode(2, OUTPUT) | p = Pin(2, Pin.OUT) |
digitalWrite(2, HIGH) | p.on() ou p.value(1) |
digitalRead(2) | p.value() |
INPUT_PULLUP | Pin.IN, Pin.PULL_UP |
analogRead(A0) 0–1023 | ADC(26).read_u16() 0–65535 |
analogWrite(9, 128) | PWM(Pin(9)).duty_u16(32768) |
delay(500) | time.sleep_ms(500) |
millis() | time.ticks_ms() |
attachInterrupt | p.irq(handler=f, trigger=Pin.IRQ_FALLING) |
3,3 V partout
Pico et ESP32 fonctionnent en 3,3 V : une entrée qui reçoit 5 V est détruite. Le HC-SR04 renvoie un écho en 5 V → pont diviseur (séance 21) ou version 3,3 V. Les LED : 100 Ω. Le servo : alimenté en 5 V (VBUS sur Pico), signal 3,3 V accepté. L’ADC du Pico est 12 bits réels (4096 niveaux), l’ESP32 aussi mais non linéaire aux extrêmes : à calibrer.
02 / Entrées-sorties
Servo, ultrason, interruption : les mêmes, en Python
# Servo : PWM 50 Hz, impulsion 1-2 ms
from machine import Pin, PWM
servo = PWM(Pin(16)); servo.freq(50)
def angle(deg):
# 1 ms = 5 % de 20 ms ; 2 ms = 10 %
largeur = 1.0 + deg / 180.0 # ms
servo.duty_u16(int(largeur / 20.0 * 65535))
angle(0); time.sleep(1); angle(180)
# Ultrason HC-SR04 (avec diviseur sur ECHO !)
from machine import time_pulse_us
trig, echo = Pin(17, Pin.OUT), Pin(18, Pin.IN)
def distance_cm():
trig.off(); time.sleep_us(2)
trig.on(); time.sleep_us(10); trig.off()
d = time_pulse_us(echo, 1, 30000) # µs, -1 ou -2 si timeout
return d * 0.01715 if d > 0 else None# Interruption : compter les impulsions d'un encodeur
from machine import Pin
impulsions = 0
def isr(pin):
global impulsions
impulsions += 1 # court, pas d'allocation
enc = Pin(19, Pin.IN, Pin.PULL_UP)
enc.irq(handler=isr, trigger=Pin.IRQ_FALLING)
import time
while True:
time.sleep(1)
n, impulsions = impulsions, 0
print(n, "impulsions/s")Dans une ISR MicroPython : pas de création d’objets (listes, chaînes, float), pas de print. Pour du travail plus lourd, micropython.schedule(f, arg) reporte l’exécution dans la boucle principale.
03 / Organisation
main.py, boot.py, et les fichiers sur la carte
- La carte a un petit système de fichiers (1–2 Mo). Thonny → Affichage → Fichiers montre « Cet ordinateur » et « Raspberry Pi Pico ».
boot.pys’exécute au démarrage (config, WiFi).main.pyensuite : c’est votre programme autonome, lancé sans PC.- Vos modules (
capteurs.py,moteurs.py) se copient sur la carte et s’importent normalement (séance 09). - Un fichier de journal s’écrit avec
open()comme en séance 07, dans la flash.
Pour reprendre la main sur une carte dont main.py boucle : Ctrl+C dans Thonny, puis supprimer ou renommer main.py.
# main.py : structure d'un programme de robot
from machine import Pin, Timer
import time, capteurs, moteurs
led = Pin("LED", Pin.OUT)
Timer(period=250, mode=Timer.PERIODIC, callback=lambda t: led.toggle()) # « je suis vivant »
def cycle():
d = capteurs.distance()
if d is not None and d < 20:
moteurs.tourner(50)
else:
moteurs.avancer(80)
try:
prochain = time.ticks_ms()
while True:
cycle()
prochain = time.ticks_add(prochain, 50)
time.sleep_ms(max(0, time.ticks_diff(prochain, time.ticks_ms())))
except KeyboardInterrupt:
pass
finally:
moteurs.stop() # toujours, même sur Ctrl+C (séance 08)03 / Organisation
MicroPython n’est pas CPython
| Présent | Absent ou réduit | Spécifique |
|---|---|---|
Toute la syntaxe : classes, générateurs, compréhensions, try, f-strings, with | numpy, matplotlib, threading (partiel), csv, dataclasses, fractions | machine (matériel), micropython, network, time.ticks_* |
math, random, json, struct, collections.deque, asyncio (uasyncio) | Certaines méthodes de chaînes, re réduit, pas de __slots__… | const(), @micropython.native pour accélérer |
Entiers illimités, float 32 bits par défaut | Précision : 7 chiffres, comme le float C | gc.collect() : forcer le nettoyage mémoire |
Bonnes pratiques mémoire
264 Ko, c’est 100 000 fois moins qu’un PC. Une liste de 10 000 floats, une chaîne construite par += dans une boucle, une lambda créée à chaque cycle : le ramasse-miettes s’essouffle et le programme saccade. Règles : préallouer, réutiliser, mesurer avec gc.mem_free(), et quand ça bloque, @micropython.native ou viper compile une fonction en code machine (10× plus vite).
03 / Organisation
Arduino ou Pico ? Un choix d’ingénieur
| Arduino Uno + C/C++ | Pico / ESP32 + MicroPython | |
|---|---|---|
| Vitesse d’exécution | Compilé : rapide et prévisible (µs) | Interprété : 10 à 100× plus lent, gigue du ramasse-miettes |
| Vitesse de développement | Compiler-téléverser à chaque essai | REPL : instantané |
| Mémoire | 2 Ko RAM : chaque octet compte | 264 Ko (Pico), 520 Ko (ESP32) |
| Écosystème | Immense, 15 ans de bibliothèques et shields | Bon et croissant ; le C reste possible (SDK Pico, ESP-IDF) |
| Connectivité | Aucune (sans shield) | WiFi et Bluetooth intégrés (Pico W, ESP32) |
| Temps réel dur | Naturel | Difficile en Python ; PIO (Pico) ou tâches C |
| Pédagogie | Apprend le C, les registres, la rigueur | Réutilise Python, va vite à l’essentiel |
Dans l’industrie : C/C++ pour le contrôle bas niveau (moteurs, sécurité), Python pour la logique, la communication et l’analyse. Un roboticien fait les deux. Vous aussi, désormais.
04 / Défis
Défi ★ — Portage
Consigne
Porter en MicroPython, sur votre carte : 1. le compteur d’appuis avec anti-rebond (séance 22) ; 2. le fondu de LED commandé par potentiomètre (séance 23), avec la correction gamma (duty = int(65535 × (brut/65535)²)) ; 3. le feu tricolore avec un Timer et une machine à états au lieu de sleep.
Correction (compteur)
from machine import Pin
import time
bouton = Pin(14, Pin.IN, Pin.PULL_UP)
compteur = 0; precedent = 1; dernier = time.ticks_ms()
while True:
etat = bouton.value()
if etat != precedent and time.ticks_diff(time.ticks_ms(), dernier) > 30:
dernier = time.ticks_ms()
if etat == 0:
compteur += 1
print("Appuis :", compteur)
precedent = etatLigne pour ligne, c’est la séance 22 sans les types ni les accolades. La logique embarquée ne dépend pas du langage.
04 / Défis
Défi ★★ — Station de mesure autonome
Consigne
Un main.py qui, toutes les 10 s, lit la température interne du Pico (ADC(4), formule 27 − (tension − 0.706) / 0.001721) et la lumière (photorésistance), et ajoute une ligne horodatée (time.ticks_ms() ou un RTC) dans journal.csv sur la carte. La LED clignote brièvement à chaque écriture. Le programme doit survivre à une erreur de lecture (try/except) et limiter le fichier à 1000 lignes (rotation). Après une heure débranchée, récupérer le fichier dans Thonny et le tracer sur PC.
Correction
from machine import Pin, ADC
import time, os
led = Pin("LED", Pin.OUT); temp = ADC(4); ldr = ADC(26)
MAX_LIGNES = 1000
def temperature():
return 27 - (temp.read_u16() * 3.3 / 65535 - 0.706) / 0.001721
def nb_lignes(nom):
try:
with open(nom) as f: return sum(1 for _ in f)
except OSError: return 0
if nb_lignes("journal.csv") == 0:
with open("journal.csv", "w") as f: f.write("t_ms,temp_c,lumiere\n")
while True:
try:
if nb_lignes("journal.csv") > MAX_LIGNES:
os.rename("journal.csv", "journal_old.csv")
with open("journal.csv", "w") as f: f.write("t_ms,temp_c,lumiere\n")
with open("journal.csv", "a") as f:
f.write(f"{time.ticks_ms()},{temperature():.1f},{ldr.read_u16()}\n")
led.on(); time.sleep_ms(50); led.off()
except Exception as e:
print("erreur :", e)
time.sleep(10)La flash a un nombre limité d’écritures (~100 000 par secteur) : écrire toutes les 10 s pendant un an, ça va ; toutes les 10 ms, non. Un vrai enregistreur tamponne en RAM et écrit par blocs.
04 / Défis
Défi ★★★ — Esprit prépa : mesurer la gigue de l’interpréteur
Consigne
Écrire une boucle qui bascule une broche toutes les 1000 µs avec time.ticks_us() et enregistre 2000 intervalles réels dans un array("I") préalloué. Calculer moyenne, écart-type, min, max. Puis :
1. Refaire en créant une liste vide et en y faisant append à chaque tour : que devient le max ? Pourquoi (ramasse-miettes) ?
2. Décorer la fonction de bascule avec @micropython.native puis @micropython.viper : gain ?
3. Comparer avec la même mesure sur Arduino (micros()). Conclure sur « temps réel » et « interprété ». Sur Pico, chercher ce qu’est le PIO et pourquoi il résout ce problème.
Correction et ouverture
from machine import Pin
import time, array, math, gc
pin = Pin(15, Pin.OUT)
N = 2000
intervalles = array.array("I", [0] * N) # préalloué : aucune allocation dans la boucle
gc.collect()
prochain = time.ticks_us()
precedent = prochain
for i in range(N):
prochain = time.ticks_add(prochain, 1000)
while time.ticks_diff(prochain, time.ticks_us()) > 0:
pass # attente active : plus précise que sleep_us
pin.toggle()
maintenant = time.ticks_us()
intervalles[i] = time.ticks_diff(maintenant, precedent)
precedent = maintenant
m = sum(intervalles) / N
s = math.sqrt(sum((x - m) ** 2 for x in intervalles) / N)
print(f"moyenne {m:.1f} µs écart-type {s:.1f} min {min(intervalles)} max {max(intervalles)}")Typique sur Pico : moyenne 1000 µs, écart-type ~2 µs, max ~20 µs. Avec une liste qui grandit : des pics à plusieurs centaines de µs quand le ramasse-miettes passe. Sur Arduino : gigue < 1 µs, sans surprise. Conclusion : MicroPython suffit pour une boucle de contrôle à 100 Hz, pas pour générer un signal à 100 kHz. Le PIO du Pico est un petit processeur programmable indépendant, dédié aux signaux précis (servos, LED adressables, protocoles) : c’est la réponse matérielle au problème logiciel. En prépa, l’analyse « pire cas » de ces systèmes est le cœur des cours de systèmes temps réel.
05 / Vérification
Pourquoi utiliser time.ticks_diff(a, b) et non a − b ?
Deux questions supplémentaires
1. Quelle tension maximale sur une entrée du Pico ? 3,3 V. Au-delà, la broche est détruite.
2. Quel fichier s’exécute automatiquement au démarrage ? main.py (après boot.py).
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| MicroPython | Interpréteur Python pour microcontrôleurs. |
| REPL | Console interactive exécutée sur la carte. |
machine | Module d’accès au matériel : Pin, ADC, PWM, Timer, I2C… |
ticks_ms / ticks_diff | Horloge et différence sûre au débordement. |
main.py | Programme lancé au démarrage, sans PC. |
| Ramasse-miettes | Libère la mémoire ; source de gigue. |
| PIO | Processeurs d’E/S programmables du Pico, pour les signaux précis. |
Pour continuer
Python sur une puce
Séance suivante : brancher des capteurs « intelligents » par I2C (gyroscope, écran) et connecter l’ESP32 au WiFi.
À faire chez soi
- Refaire les trois portages du défi ★ sans regarder.
- Laisser tourner la station de mesure une nuit et tracer les résultats.
- Lire la documentation du module
machinepour votre carte et repérer ce qu’on n’a pas vu :SPI,UART,RTC,WDT.