Écran SPI et game loop
Ce tutoriel ajoute la dernière brique matérielle du montage : l’écran. Le câblage des boutons et du joystick posé au tutoriel Boutons, joystick et moniteur série ne bouge pas. À la fin, une balle rebondit sur l’écran — la première brique visuelle du Pong.
Câblage additionnel de l’atelier
| Signal |
Broche |
Rôle |
| CS |
GPIO10 |
Sélection de l’écran sur le bus SPI |
| DC |
GPIO11 |
Bascule commande / donnée |
| RST |
GPIO12 |
Reset matériel de l’écran |
| SCLK |
GPIO13 |
Horloge SPI |
| MOSI |
GPIO14 |
Données CPU → écran |
| BLK (rétroéclairage) |
GPIO15 ou 3,3 V direct |
Allumage du rétroéclairage |
graph LR
CS --> GPIO10["GPIO10"]
DC --> GPIO11["GPIO11"]
RST --> GPIO12["GPIO12"]
SCLK --> GPIO13["GPIO13"]
MOSI --> GPIO14["GPIO14"]
BLK --> GPIO15["GPIO15 (ou 3,3 V)"]
L’écran ne renvoie jamais de données utiles à l’ESP32 : seul le sens CPU → écran (MOSI) est câblé. C’est la différence entre un bus SPI complet et cet usage en écriture seule — voir le concept Les bus de communication.
1 — Installer TFT_eSPI et configurer User_Setup
Ajoute la bibliothèque TFT_eSPI à ton platformio.ini, puis copie le fichier User_Setup.h fourni (identique pour tout le groupe) dans le dossier de la librairie. C’est le point de blocage classique de cette étape : un User_Setup mal configuré donne un écran noir, pas une erreur de compilation.
[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
lib_deps =
bodmer/TFT_eSPI@^2.5.0
build_flags =
-DUSER_SETUP_LOADED=1
-include User_Setup.h
// User_Setup.h (extrait clé en main — pins figées de l'atelier)
#define ST7789_DRIVER // adapter selon le contrôleur de ton écran
#define TFT_CS 10
#define TFT_DC 11
#define TFT_RST 12
#define TFT_SCLK 13
#define TFT_MOSI 14
#define SPI_FREQUENCY 40000000
Vérifie dans l’ordre : le rétroéclairage (BLK) est-il alimenté ? Le driver (ST7789_DRIVER / ILI9341_DRIVER) correspond-il à ton écran ? Les pins du User_Setup.h correspondent-elles au câblage réel ? Ces trois points couvrent la quasi-totalité des écrans noirs.
À ce stade, le projet doit compiler et se flasher sans erreur — même si l’écran reste noir : aucun dessin n’a encore été envoyé. Une erreur de compilation ici vient presque toujours d’un chemin ou d’un nom de fichier incorrect pour User_Setup.h.
2 — Premières primitives
TFT_eSPI expose un système de coordonnées (0,0) en haut à gauche. Teste les formes et le texte de base avant de construire quoi que ce soit d’animé.
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
void setup() {
tft.init();
tft.setRotation(1); // paysage
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextSize(2);
tft.setCursor(10, 10);
tft.print("PONG");
tft.drawRect(20, 40, 60, 20, TFT_WHITE); // rectangle contour
tft.fillCircle(160, 60, 8, TFT_WHITE); // cercle plein
}
void loop() {}
Tu dois voir le texte « PONG », un rectangle et un cercle blancs à l’écran. Si l’écran reste noir, reviens à la configuration de l’étape précédente avant d’aller plus loin.
3 — Une raquette qui suit le joystick
Réutilise la lecture ADC du tutoriel précédent pour déplacer un rectangle (la raquette) verticalement. On efface l’ancienne position avant de redessiner — sans ça, la raquette laisse une traînée à l’écran.
#define JOY_Y 2
int raquetteY = 60;
void dessinerRaquette(int y, uint16_t couleur) {
tft.fillRect(10, y, 6, 30, couleur);
}
void loop() {
int y = map(analogRead(JOY_Y), 0, 4095, 0, tft.height() - 30);
if (y != raquetteY) {
dessinerRaquette(raquetteY, TFT_BLACK); // efface l'ancienne position
dessinerRaquette(y, TFT_WHITE); // dessine la nouvelle
raquetteY = y;
}
}
La raquette doit suivre le joystick sans à-coups, et sans laisser de traînée blanche derrière elle en se déplaçant.
4 — La game loop non bloquante
delay() bloquerait toute lecture d’entrée pendant l’attente — inacceptable pour un jeu. millis() permet de cadencer les mises à jour sans jamais bloquer le CPU, en comparant le temps écoulé à chaque passage de loop().
unsigned long dernierUpdate = 0;
const unsigned long INTERVALLE = 16; // ~60 images/seconde
void loop() {
unsigned long maintenant = millis();
if (maintenant - dernierUpdate >= INTERVALLE) {
dernierUpdate = maintenant;
lireEntrees();
mettreAJourJeu();
redessiner();
}
}
Cette structure lire → mettre à jour → redessiner est la charpente de toute la suite du projet : le tutoriel Collisions, score et machine à états et le projet Snake ne feront qu’enrichir mettreAJourJeu() et redessiner().
5 — Une balle qui rebondit
Assemble les deux étapes précédentes : une balle en mouvement constant, qui rebondit sur les bords haut/bas et droit de l’écran (le bord gauche sera la raquette, gérée à l’étape suivante).
int balleX = 160, balleY = 60;
int balleVX = 3, balleVY = 2;
void mettreAJourJeu() {
balleX += balleVX;
balleY += balleVY;
if (balleY <= 0 || balleY >= tft.height()) balleVY = -balleVY;
if (balleX >= tft.width()) balleVX = -balleVX;
}
void redessiner() {
static int ancienX = balleX, ancienY = balleY;
tft.fillCircle(ancienX, ancienY, 4, TFT_BLACK); // efface l'ancienne position
tft.fillCircle(balleX, balleY, 4, TFT_WHITE); // dessine la nouvelle
ancienX = balleX;
ancienY = balleY;
}
Une balle blanche doit se déplacer en continu et rebondir sur les bords haut, bas et droit, sans clignotement ni traînée visible.
Résultat attendu
Une balle blanche se déplace en continu sur l’écran et rebondit sur les bords, sans clignotement ni traînée visible, pendant que la raquette du joueur 2 suit le joystick.
Le tutoriel Collisions, score et machine à états ajoute la seconde raquette, la détection de collision balle/raquette, le score et la machine à états (menu / partie / game over) — sans rien changer au câblage.