Tester avant de concevoir
La mallette contient les quatre modules de la future console : un bouton, un joystick, un buzzer et un écran. Avant de dessiner la carte qui les réunira (TD3), on vérifie chacun séparément, sur breadboard : comment il se branche, ce qu’il renvoie, comment le programmer. Ce que vous apprenez ici décidera de votre schéma.
La démarche est la même pour chaque module, et pour tout composant que vous découvrirez plus tard :
- Identifier ses broches (sérigraphie, fiche du module) et sa tension d’alimentation ;
- Câbler une seule chose à la fois ;
- Lire ou commander avec le programme le plus simple possible ;
- Valider au moniteur série (ou à l’oreille, à l’œil) avant de passer au suivant.
Alimentez tous les modules sur la broche 3V3 de la XIAO, jamais sur 5V : les entrées de l’ESP32-S3 ne supportent pas plus de 3,3 V. Un module conçu pour 5 V fonctionne presque toujours en 3,3 V ; l’inverse peut détruire la carte.
Le câblage de la mallette
On utilise dès maintenant les broches de la carte de la console : un programme écrit aujourd’hui fonctionnera sans modification sur la carte soudée au TD4.
| Module |
Broche du module |
Broche XIAO |
| Bouton |
Signal (S, OUT) |
D5 |
| Joystick |
VRx |
D0 (ADC1) |
| Joystick |
VRy |
D1 (ADC1) |
| Joystick |
SW (appui sur le stick) |
D2 |
| Buzzer |
Signal (S, I/O) |
D3 |
| Écran |
SCK (souvent sérigraphiée SCL) |
D9 |
| Écran |
SDI / MOSI (souvent SDA) |
D10 |
| Écran |
DC |
D8 |
| Écran |
RESET |
D7 |
| Écran |
CS |
GND |
| Écran |
LED (rétroéclairage) |
3V3 |
| Tous |
VCC / GND |
3V3 / GND |
Le buzzer n’est pas sur la carte de la console : il est sur D3 le temps de ce TD (sur la carte, D3 sert au connecteur I2C).
Regroupez les broches dans un fichier include/config.h de votre projet PlatformIO, que tous les programmes de la suite incluront :
// include/config.h : brochage de la console
#pragma once
#include <Arduino.h>
constexpr int PIN_BUTTON = D5;
constexpr int PIN_JOYSTICK_X = D0;
constexpr int PIN_JOYSTICK_Y = D1;
constexpr int PIN_JOYSTICK_SELECT = D2;
constexpr int PIN_BUZZER = D3; // seulement sur breadboard
constexpr int PIN_TFT_RESET = D7;
constexpr int PIN_TFT_DC = D8;
constexpr int PIN_TFT_SCK = D9;
constexpr int PIN_TFT_MOSI = D10;
constexpr int PIN_TFT_CS = -1; // CS relié à GND : pas de broche
constexpr int déclare une constante typée : le compilateur la remplace par sa valeur, comme un #define, mais vérifie son type.
Étape 1 : Le bouton, lire son niveau
Un module bouton a en général trois broches : VCC, GND et un signal. Selon le module, le signal vaut LOW quand on appuie (bouton relié à GND, avec résistance de tirage vers VCC) ou HIGH (l’inverse). Plutôt que de deviner, on mesure : le programme affiche le niveau lu, bouton relâché puis enfoncé.
INPUT_PULLUP active la résistance de tirage interne de l’ESP32-S3 : elle ne gêne pas un module qui a déjà la sienne, et elle évite une entrée flottante si le module n’en a pas (voir le concept GPIO & monde numérique).

#include "config.h"
void setup() {
Serial.begin(115200);
pinMode(PIN_BUTTON, INPUT_PULLUP);
}
void loop() {
Serial.println(digitalRead(PIN_BUTTON) == HIGH ? "HIGH" : "LOW");
delay(200); // programme de test : on ralentit l'affichage
}
Le moniteur série (115200 bauds) affiche un niveau bouton relâché, et l’autre bouton enfoncé. Notez lequel correspond à l’appui : c’est la logique de votre module, et elle compte pour la suite.
Étape 2 : Le bouton, détecter un appui une seule fois
Pour un jeu, on ne veut pas savoir si le bouton est enfoncé, mais s’il vient de l’être : c’est la détection de front. Elle a deux pièges :
- tant que le doigt reste appuyé,
loop() voit le bouton enfoncé des milliers de fois ;
- le contact mécanique rebondit quelques millisecondes : un seul appui ressemble à plusieurs.
La fonction ci-dessous ne valide un changement que si le signal est resté stable 30 ms (l’anti-rebond), et ne renvoie true qu’au moment de l’appui. Elle ne bloque jamais : pas de delay(), seulement millis(). Adaptez PRESSED_LEVEL à ce que vous avez noté à l’étape 1.
C’est l’anti-rebond du CM2 (ANTIREBOND, dernierEtat), avec des noms en anglais comme tout le code des TD : DEBOUNCE_MS, lastRead.
constexpr int PRESSED_LEVEL = LOW; // niveau lu quand on appuie (étape 1)
constexpr unsigned long DEBOUNCE_MS = 30;
struct Button {
int pin;
int stableLevel; // dernier niveau confirmé
int lastRead; // dernière lecture brute
unsigned long lastChangeMs; // date du dernier mouvement du signal
};
Button button = {PIN_BUTTON, !PRESSED_LEVEL, !PRESSED_LEVEL, 0};
// Renvoie true une seule fois par appui, au moment où il est confirmé
bool justPressed(Button& b) {
int level = digitalRead(b.pin);
if (level != b.lastRead) { // le signal bouge : rebond ou vrai changement
b.lastRead = level;
b.lastChangeMs = millis();
}
if (millis() - b.lastChangeMs > DEBOUNCE_MS && level != b.stableLevel) {
b.stableLevel = level; // stable depuis 30 ms : changement confirmé
return level == PRESSED_LEVEL; // vrai sur l'appui, faux sur le relâchement
}
return false;
}
void loop() {
static int count = 0;
if (justPressed(button)) {
count++;
Serial.printf("Appui n°%d\n", count);
}
}
Chaque appui affiche une seule ligne, même si vous maintenez le bouton, et le compteur n’avance jamais de deux pour un seul appui.
Étape 3 : Le joystick, lire les valeurs brutes
Le module joystick a 5 broches : GND, VCC (souvent sérigraphiée +5V : branchez-la quand même sur 3V3), VRx, VRy et SW. Ses deux axes sont des potentiomètres : l’ADC 12 bits de l’ESP32-S3 les lit entre 0 et 4095. SW est un bouton (appui sur le stick) relié à GND : INPUT_PULLUP, et LOW quand on appuie.

void setup() {
Serial.begin(115200);
pinMode(PIN_JOYSTICK_SELECT, INPUT_PULLUP);
analogReadResolution(12); // lectures entre 0 et 4095
}
void loop() {
int x = analogRead(PIN_JOYSTICK_X);
int y = analogRead(PIN_JOYSTICK_Y);
bool pressed = digitalRead(PIN_JOYSTICK_SELECT) == LOW;
Serial.printf("x=%d y=%d select=%d\n", x, y, pressed);
delay(100);
}
Notez la valeur de x au repos : proche de 2048, mais presque jamais exactement (souvent entre 1800 et 2100). x et y atteignent 0 et 4095 aux butées, et select passe à 1 quand vous enfoncez le stick.
Étape 4 : Le joystick, calibrer et normaliser
Un jeu veut savoir dans quel sens et de combien le joueur pousse : une valeur entre -1 et +1, qui vaut 0 au repos. Trois précautions :
- calibrer : au démarrage, mesurer le vrai centre (moyenne de 16 lectures, stick relâché) au lieu de supposer 2048 ;
- zone morte : un écart de moins de 300 autour du centre est du bruit, on renvoie 0 ;
- course asymétrique : on divise par la course disponible du côté où se trouve le stick, pour que chaque butée donne bien -1 ou +1.
Le détail est dans le concept ADC & PWM.
constexpr int JOYSTICK_DEADZONE = 300;
int joystickCenterX = 2048;
void calibrateJoystick() {
long sum = 0;
for (int i = 0; i < 16; i++) {
sum += analogRead(PIN_JOYSTICK_X);
delay(2);
}
joystickCenterX = sum / 16;
}
// Convertit une lecture brute (0..4095) en valeur entre -1.0 et +1.0
float normalizeJoystick(int raw, int center) {
int offset = raw - center;
if (abs(offset) < JOYSTICK_DEADZONE) {
return 0.0f;
}
int range = (offset > 0) ? (4095 - center) : center;
return constrain((float)offset / range, -1.0f, 1.0f);
}
Appelez calibrateJoystick() à la fin de setup(), puis affichez normalizeJoystick(analogRead(PIN_JOYSTICK_X), joystickCenterX) dans loop().
Au repos, la valeur affichée est exactement 0.00. Elle atteint -1.00 d’un côté et +1.00 de l’autre, en passant par toutes les valeurs intermédiaires. Si le sens est inversé par rapport à ce que vous attendez, notez-le : il suffira de changer le signe.
Étape 5 : Le buzzer, jouer une note
Il existe deux sortes de buzzers, et la mallette peut contenir l’une ou l’autre :
- un buzzer actif contient son propre oscillateur : il sonne dès qu’on met sa broche à HIGH, toujours sur la même note ;
- un buzzer passif est un simple haut-parleur : il faut lui envoyer un signal carré à la fréquence de la note voulue. C’est du PWM (voir le concept ADC & PWM).
tone(broche, fréquence, durée) génère ce signal sans bloquer le programme. Testez le programme ci-dessous : un buzzer passif joue une petite gamme ; un buzzer actif fait seulement des clics, et le second test le fait sonner.

void setup() {
pinMode(PIN_BUZZER, OUTPUT);
// Test 1 : buzzer passif, une gamme do-ré-mi-fa-sol
const int notes[] = {262, 294, 330, 349, 392}; // fréquences en Hz
for (int note : notes) {
tone(PIN_BUZZER, note, 200);
delay(250); // test de démarrage : attendre la fin de la note
}
delay(1000);
// Test 2 : buzzer actif, un bip d'une demi-seconde
digitalWrite(PIN_BUZZER, HIGH);
delay(500);
digitalWrite(PIN_BUZZER, LOW);
}
void loop() {}
Vous savez si votre buzzer est actif ou passif, et vous l’avez fait sonner. Les delay() sont acceptables ici, dans un programme de test qui ne fait rien d’autre ; dans un jeu, une note se lance avec tone() et on ne l’attend pas.
Étape 6 : L'écran, premier affichage
L’écran se pilote en SPI avec la bibliothèque Adafruit ST7735 and ST7789, à déclarer dans platformio.ini. On démarre d’abord le bus SPI sur les broches du montage, puis on initialise l’écran en mode SPI 3 : son CS est relié à GND, et c’est le mode qu’il lui faut dans ce cas (voir le concept Les bus de communication).
Le repère a son origine (0, 0) en haut à gauche : x vers la droite, y vers le bas. L’écran est utilisé en portrait, 240 pixels de large et 320 de haut.


lib_deps =
adafruit/Adafruit ST7735 and ST7789 Library@^1.11.0
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include "config.h"
Adafruit_ST7789 tft(&SPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RESET);
void setup() {
SPI.begin(PIN_TFT_SCK, -1, PIN_TFT_MOSI, PIN_TFT_CS); // pas de MISO
tft.init(240, 320, SPI_MODE3);
tft.setRotation(2); // portrait
tft.fillScreen(ST77XX_BLACK);
tft.setTextSize(4);
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(72, 40);
tft.print("PONG");
tft.drawRect(20, 120, 200, 60, ST77XX_WHITE); // rectangle
tft.fillCircle(120, 250, 20, ST77XX_YELLOW); // disque
}
void loop() {}
Le texte « PONG », un rectangle et un disque jaune s’affichent. Écran noir ou blanc ? Suivez le guide Dépanner un écran TFT SPI qui n’affiche rien.
Étape 7 : Les quatre modules ensemble
Dernier test, qui servira de programme de test de la carte au TD4 : un point blanc se déplace à l’écran avec le joystick, et le buzzer bipe à chaque appui sur le bouton. Il réutilise tout ce qui précède : justPressed(), la calibration et la normalisation du joystick, l’initialisation de l’écran.
La boucle ne contient aucun delay() : une image toutes les 16 ms, cadencée par millis(). C’est la game loop du CM3, que le Pong reprendra.
constexpr uint32_t FRAME_TIME_MS = 16;
float dotX = 120, dotY = 160; // position calculée
int drawnX = 120, drawnY = 160; // position affichée
void loop() {
static uint32_t lastFrameMs = 0;
uint32_t nowMs = millis();
if (nowMs - lastFrameMs < FRAME_TIME_MS) {
return;
}
lastFrameMs = nowMs;
// 1. Lire
float x = normalizeJoystick(analogRead(PIN_JOYSTICK_X), joystickCenterX);
bool beep = justPressed(button);
// 2. Calculer
dotX = constrain(dotX + x * 4.0f, 0.0f, 235.0f);
if (beep) {
tone(PIN_BUZZER, 880, 50);
}
// 3. Afficher : effacer l'ancien point, dessiner le nouveau
if ((int)dotX != drawnX) {
tft.fillRect(drawnX, drawnY, 5, 5, ST77XX_BLACK);
drawnX = (int)dotX;
tft.fillRect(drawnX, drawnY, 5, 5, ST77XX_WHITE);
}
}
Le setup() regroupe les initialisations des étapes précédentes : moniteur série, bouton, joystick (avec sa calibration), buzzer, écran.
Le point suit le joystick sans laisser de traînée, et chaque appui sur le bouton fait un bip court, sans ralentir le point. Gardez ce programme : il testera la carte soudée.
Résultat attendu
Les quatre modules sont testés, et vous savez pour chacun :
- comment il se branche, et sous quelle tension ;
- ce qu’il renvoie (niveau du bouton, plage et centre du joystick) ou ce qu’il attend (type de buzzer, mode SPI de l’écran) ;
- quel programme minimal le fait fonctionner.
Notez ces résultats dans votre journal de bord : ce sont les données d’entrée du schéma de la carte.
Le TD3 réunit ces modules sur une carte : voir Concevoir la carte de la console.