L’écran de l’atelier (240 × 320) reçoit 76 800 pixels par image, soit plus de 150 000 octets en couleur 16 bits. Un capteur renvoie une température sur 16 bits. Une carte SD, des blocs de 512 octets.
Câbler un fil par bit serait impraticable : plus aucune broche disponible, une carte illisible, et des perturbations entre pistes.
Un bus série envoie les bits les uns après les autres, sur deux à quatre fils. On échange de la largeur contre du temps, et la puce est assez rapide pour que ça ne se voie pas.
UART : le plus simple. Deux fils, un seul interlocuteur, pas d’horloge partagée
I2C : deux fils, plusieurs composants distingués par une adresse
SPI : quatre fils, une horloge, le plus rapide des trois
Aucun n’est meilleur dans l’absolu : chacun échange de la vitesse contre des fils, ou des fils contre de la complexité. Le choix se fait par le besoin.
Le TX d’un appareil se branche sur le RX de l’autre, et réciproquement. Un câblage TX vers TX ne transmet rien, et c’est silencieux : aucun message d’erreur ne vous préviendra, vous verrez seulement un module qui « ne répond pas ».
Le start donne le signal, le récepteur compte les bits à la vitesse convenue. 8 bits, pas de parité, 1 stop : 8N1. Format ou vitesse différents : du charabia.
Serial.begin(115200);// le port série du moniteurSerial.printf("x=%d y=%d\n",x,y);
Le moniteur série se manie comme un UART. L’UART est aussi le bus des modules GPS, Bluetooth ou GSM, et le moyen le plus simple de faire dialoguer deux microcontrôleurs.
Sur beaucoup de cartes, une puce USB-série (CP2102, CH340) traduit l’UART en USB et réclame un driver sous Windows. La XIAO n’en a pas : Serial passe directement par l’USB natif de l’ESP32-S3, et l’UART0 reste libre sur D6/D7.
SDA et SCL sont en open-drain, comme au CM2 : les composants ne peuvent que tirer la ligne vers LOW, jamais vers HIGH. C’est précisément ce qui permet à plusieurs d’entre eux de partager le fil sans court-circuit.
Il faut donc deux résistances de pull-up (~4,7 kΩ) vers le 3,3 V. Sans elles, la ligne ne remonte jamais et rien ne communique.
La plupart des modules du commerce les intègrent déjà. Si vous en chaînez plusieurs, une seule paire suffit sur tout le bus : inutile de les cumuler. ↓ pour le détail.
Largement suffisant pour des capteurs ou un petit écran OLED monochrome, sur un ou deux mètres au plus. Bien trop lent pour un écran TFT couleur : à 400 kHz, une image de 240 × 320 pixels demanderait plus de trois secondes.
De mauvaises broches donnent un écran noir, pas une erreur de compilation. Si l’écran reste éteint alors que tout compile, vérifier en premier les broches passées à SPI.begin() (SCK sur D9) et le SPI_MODE3, obligatoire quand CS est relié à GND.
flowchart LR
B{"Besoin de<br/>vitesse ?"} -->|Oui| C["SPI<br/>écran, carte SD"]
B -->|Non| D{"Plusieurs<br/>composants ?"}
D -->|Oui| E["I2C<br/>capteurs, OLED"]
D -->|Non| F["UART<br/>debug, GPS, BT"]
Notre projet : un port série pour le moniteur, SPI pour l’écran. ↓ pour la page.
par image, soit 60 images par seconde. En dessous, l’œil voit saccader.
4 M
cycles CPU disponibles dans ces 16 ms, à 240 MHz. De quoi faire beaucoup.
Si une image prend plus de 16 ms à calculer, le jeu ralentit au lieu de sauter des images. Premier suspect en cas de lenteur : un delay() oublié, ou un redessin complet de l’écran au lieu des seules zones modifiées.
case MENU:
if (appuiStart()) { afficherJeu(); etat = PARTIE; }
break;
case PARTIE:
mettreAJourJeu(); // et redessine ce qui a bougé
if (vieJoueur == 0) { afficherGameOver(); etat = GAME_OVER; }
break;
case GAME_OVER:
if (appuiRetry()) { afficherMenu(); etat = MENU; }
break;
Un écran complet se dessine une fois, à la transition, jamais à chaque tour. Et appuiStart() ne vaut true qu’une fois par appui (front).
Dans la même séance, le mot état désigne deux choses : les données du jeu (position de la balle, score, ce sont des variables) et la phase du jeu (MENU, PARTIE, GAME_OVER, c’est la FSM). Gardez la distinction en tête pour ne pas les mélanger.
Réinitialiser le score ou effacer l’écran ne doit pas se répéter 60 fois par seconde. On l’exécute au moment de la transition, pas dans le case :
voidallerVers(EtatJeunouvel){etat=nouvel;entreeEtat=millis();// date d'entréeif(nouvel==PARTIE)initialiserPartie();if(nouvel==GAME_OVER)afficherGameOver();}
C’est l’action d’entrée (onEnter). Toutes les transitions passent par là.
FSM et millis() se combinent naturellement : c’est la transition temporisée. La date d’entrée dans l’état, mémorisée par allerVers(), sert de référence.