Aller au contenu
Mission 5Objectif 5 · Planète Jakkumulateur

C'est un piège ! (les moteurs)

Maître du PWMSeigneur Sith du PWM ★★★ 🚫 Un moteur branché direct sur une broche ? C'est un piège ! Il faut un driver.🚫 C'est un piège ! Ne fais pas le travail des rebelles à leur place : utilise le driver.

📡 Transmission de l'Alliance · R2-DuPont

« C'est un piège ! Les moteurs arrivent : plus de puissance, plus de courant, plus de possibilités de faire quelque chose de spectaculaire… involontairement. Rappelle-toi : un moteur ne se branche pas directement sur une broche, il lui faut un driver. Crois-moi, c'est moins spectaculaire qu'une fumée mystérieuse, mais nettement meilleur pour le matériel. »

🔊 Écouter le briefing de R2-DuPont

🛰️ Haut commandement · Opération Contre-Circuit

« C'est un piège. Les moteurs sont leur avant-dernier objectif. Ils savent qu'une mauvaise alimentation pourrait endommager tout le système : ils espèrent donc que tu feras leur travail à leur place. Ne leur donne pas cette satisfaction. Utilise le driver. »

🔊 Écouter le briefing de R2-DuPont

Objectif : faire tourner les moteurs EF09087 et EF10154 dans les deux sens et à la vitesse voulue, grâce à un driver, puis les piloter à la manette : aux gâchettes, ou en mode char avec un joystick par moteur.

🧭 Notions de la mission : Le moteur et son driver · Le PWM · L'intensité · La tension · Les circuits

Équipement de mission

🧰 Matériel

  • ESP32 sur son support + câble USB de données
  • Moteurs EF09087 et EF10154
  • 2 piles AA + boîtier (phase 1)
  • Module driver DRV8833 (HALJIA, broches soudées)
  • Alimentation 5 V · 3 A + adaptateur jack → bornier (phase 2)
  • Condensateur électrolytique 100 à 1 000 µF, 16 V ou plus (phase 2)
  • Plaque d'essai + câbles Dupont
  • Manette PS4 chargée
  • En option : un écran OLED, voir l'holo-écran

💻 Logiciels

  • Arduino IDE 2 + cartes « ESP32 + Bluepad32 »

🌐 Connexion internet

Non

🔓 Missions à réussir avant

Mission 1 (la manette)

🛠️ Outils

Petit tournevis plat

⏱️ Durée

15 min (phase 1) + 1 h (phase 2)

Jamais directement sur une broche de l'ESP32

Une broche fournit une vingtaine de mA sans souci ; un moteur en demande une centaine en tournant, et jusqu'à plus de 500 mA quand il force. Il faut un driver entre les deux.

Comment ça marche ?

Un moteur à courant continu est le plus simple des moteurs : deux fils, et il tourne dès qu'on l'alimente. Il ne sait pas où il est (ce n'est pas un servo) et ne comprend aucun signal : il obéit seulement à la tension qu'on lui donne.

  • Plus de tension → il tourne plus vite.
  • Tension inversée → il tourne dans l'autre sens.

Le driver est l'intermédiaire : il prend le courant à l'alimentation et l'envoie au moteur, dans un sens ou dans l'autre, en suivant les ordres de l'ESP32. Et pour la vitesse, l'ESP32 lui envoie du PWM. Tout le fonctionnement est dans la fiche Le moteur et son driver.

Phase 1 · Sans ESP32 : le moteur tout seul

  1. Mettre 2 piles AA dans le boîtier (1,5 V + 1,5 V = 3 V).
  2. Toucher les deux fils du moteur EF10154 avec les deux fils du boîtier : il tourne !
  3. Inverser les deux fils : il tourne dans l'autre sens.
  4. Recommencer avec l'EF09087.

L'EF09087 paraît mou ?

Il est prévu pour 3,3 à 6 V, avec 4,8 V comme tension idéale : à 3 V, il tourne plus lentement. À l'étape suivante, il recevra jusqu'à 5 V et montrera toute sa vitesse.

🎓 Académie : Le moteur et son driver · La tension

Phase 2 · Avec le driver DRV8833

Le module

Le DRV8833 est une petite puce capable de piloter deux moteurs (A et B). Elle accepte de 2,7 à 10,8 V pour les moteurs, environ 1 A par moteur en continu, et comprend sans problème les signaux en 3,3 V de l'ESP32.

Les petits modules vendus en ligne n'impriment pas tous les mêmes noms :

Rôle Noms courants sur le module
Commandes du moteur A IN1, IN2 (ou AIN1, AIN2)
Commandes du moteur B IN3, IN4 (ou BIN1, BIN2)
Sorties vers le moteur A OUT1, OUT2 (ou AOUT1, AOUT2)
Sorties vers le moteur B OUT3, OUT4 (ou BOUT1, BOUT2)
Alimentation des moteurs VCC (ou VM, VIN) et GND
Réveil de la puce EEP (ou SLP, nSLEEP)
Signal de panne ULT (ou FLT, nFAULT) : on ne s'en sert pas
DRV8833J1 en / sleepIN4IN3GNDVCCIN2IN1EEPOUT1OUT2OUT3OUT4ULTcommande B, sens 2commande B, sens 1la massel'énergie des moteurscommande A, sens 2commande A, sens 1réveil : à mettre à 1moteur Amoteur Amoteur Bmoteur Bsignal de panne (inutile)
Notre module (HALJIA), vu côté inscriptions : les commandes et l'alimentation à gauche, le réveil et les moteurs à droite.

Le réveil : EEP sur 3V3

La puce dort tant que sa broche EEP n'est pas à 1. Sur notre module, un petit pont à souder marqué J1 (« en / sleep ») décide : s'il est fermé par une goutte de soudure, la puce est toujours réveillée ; s'il est ouvert (c'est le cas à la livraison), elle dort tant qu'on ne met pas EEP à 1. Pour ne pas se poser la question : un fil de EEP vers 3V3.

Notre module : le DRV8833 de HALJIA (lot de 3)

Un module suffit pour les deux moteurs : les deux autres sont des pièces de rechange. Sur ses photos, il est livré sans broches soudées : il faudra souder deux rangées de 6 broches (12 points de soudure) pour pouvoir le planter sur la plaque d'essai. C'est une bonne première soudure, à faire avec un adulte.

La broche ULT passe à 0 en cas de panne (surchauffe, courant trop fort) : on ne s'en sert pas, on la laisse libre.

Pourquoi une alimentation externe dès maintenant ?

On pourrait croire que deux petits moteurs tiennent sur l'USB. L'atelier a fait l'essai, avec le driver alimenté par VIN (le 5 V de l'USB)… et la carte redémarrait sans arrêt : la manette se déconnectait, puis s'éteignait. Le journal de l'Arduino IDE a montré pire encore : la carte disparaissait de l'ordinateur pendant une seconde, puis revenait.

Ce qui se passe :

  1. Au démarrage, un moteur demande d'un coup beaucoup plus de courant qu'en tournant : plusieurs centaines de mA pendant un instant (voir L'intensité).
  2. Branché sur VIN, ce courant est pris au port USB de l'ordinateur, qui n'est pas fait pour ça : sa tension s'effondre une fraction de seconde, ou l'ordinateur coupe carrément le port pour se protéger.
  3. L'ESP32 passe sous sa tension minimale et redémarre ; la puce USB de la carte se déconnecte. Et comme le Bluetooth consomme lui aussi par à-coups, ça n'arrange rien.

La solution est celle des vrais robots : séparer le cerveau et les muscles.

  • L'ESP32 reste sur l'USB : il ne fournit plus que sa propre énergie, quelques dizaines de mA.
  • Le driver (donc les moteurs) prend son énergie sur l'alimentation 5 V · 3 A, qui encaisse sans broncher les démarrages.
  • Les deux partagent la même masse : sans GND commun, les ordres de l'ESP32 n'auraient pas de référence, et le driver ne les comprendrait pas (voir Les circuits).

Et le condensateur ? C'est une petite réserve d'énergie placée tout contre le driver. Au démarrage d'un moteur, c'est lui qui fournit le premier coup de courant, en une fraction de milliseconde, le temps que l'alimentation réagisse. Il lisse aussi les petites « pointes » électriques que fabrique un moteur en tournant. Le détail est dans la fiche L'alimentation.

Câbler (USB et alimentation débranchés !)

DRV8833 Relié à Rôle
IN1 D25 commande A, sens 1
IN2 D26 commande A, sens 2
IN3 D32 commande B, sens 1 (facultatif)
IN4 D33 commande B, sens 2 (facultatif)
EEP 3V3 réveil
VCC + de l'alimentation 5 V (bornier) l'énergie des moteurs
GND GND de l'ESP32 et − de l'alimentation la masse commune
VCC ↔ GND condensateur : patte longue sur VCC, bande « − » sur GND la réserve d'énergie
OUT1 / OUT2 — les 2 fils du moteur A
OUT3 / OUT4 — les 2 fils du moteur B
ESP32 (support)D33D32GNDD26D253V3ESP32 sur l'USB(lui seul)Alimentation 5 V · 3 A(adaptateur à bornier)+−DRV8833J1 en / sleepIN4IN3GNDVCCIN2IN1EEPOUT1OUT2OUT3OUT4ULTmoteur Amoteur BULT : riencondensateur (100 µF ou plus) : bande − côté GND+ vers VCC du driver− vers GND : masse commune
Le cerveau sur l'USB, les muscles sur l'alimentation externe : l'ESP32 donne les ordres (D25, D26, D32, D33), l'alimentation 5 V fournit l'énergie au driver par VCC, et les trois masses sont reliées. Le condensateur, entre VCC et GND, encaisse les démarrages des moteurs.

Les deux règles absolues

  1. VIN n'est plus branché du tout. Le + de l'alimentation externe ne va qu'au VCC du driver, jamais sur l'ESP32 tant que l'USB est branché.
  2. Toutes les masses reliées : GND de l'ESP32, GND du driver, − de l'alimentation.

Planter le module sur la plaque d'essai (un rang de broches de chaque côté de la rainure centrale), puis relier avec des câbles Dupont. Le plus simple : une ligne − de la plaque sert de masse commune, et l'autre ligne + reçoit le + de l'alimentation (rien d'autre dessus !). Le condensateur se plante entre ces deux lignes, juste à côté du driver. Les 2 fils d'un moteur vont sur OUT1 et OUT2, dans n'importe quel sens : on choisira le sens « avant » plus tard.

Dans quel ordre allumer ?

USB d'abord : l'ESP32 démarre et met toutes les commandes du driver à 0. Puis l'alimentation 5 V : les moteurs ont de l'énergie, mais attendent les ordres. Pour éteindre : alimentation d'abord, puis USB. Le cerveau donne ses consignes avant que les muscles soient réveillés.

Étape 1 · Test sans manette

Ce programme fait tourner le moteur A dans un sens, puis dans l'autre, à mi-puissance.

Le programme de test · cliquer pour l'afficher
// Mission 5 · Test du moteur A, sans manette
// avance 2 s, s'arrête 1 s, recule 2 s, s'arrête 1 s… à mi-puissance
const int A_IN1 = 25, A_IN2 = 26;

void setup() {
  Serial.begin(115200);
  pinMode(A_IN1, OUTPUT);
  pinMode(A_IN2, OUTPUT);
}

void loop() {
  Serial.println("Avant");
  analogWrite(A_IN1, 128);  analogWrite(A_IN2, 0);   // IN1 en PWM, IN2 à 0 : avant
  delay(2000);

  Serial.println("Arrêt");
  analogWrite(A_IN1, 0);    analogWrite(A_IN2, 0);   // les deux à 0 : roue libre
  delay(1000);

  Serial.println("Arrière");
  analogWrite(A_IN1, 0);    analogWrite(A_IN2, 128); // l'inverse : arrière
  delay(2000);

  Serial.println("Arrêt");
  analogWrite(A_IN1, 0);    analogWrite(A_IN2, 0);
  delay(1000);
}

Ce que tu dois voir : 2 s dans un sens, 1 s d'arrêt, 2 s dans l'autre sens, 1 s d'arrêt… Le moniteur série annonce chaque étape.

Il siffle ?

Un petit sifflement aigu quand le moteur tourne doucement, c'est normal : le PWM l'allume et l'éteint 1 000 fois par seconde, et le bobinage vibre à cette fréquence, comme un minuscule haut-parleur.

Étape 2 · Le programme Jakkumulateur

Qui fait quoi

Commande Effet Barre lumineuse
R2 (gâchette) moteur A en avant, plus ou moins vite verte
L2 (gâchette) moteur A en arrière bleue
△ passer en mode char (ou revenir au mode gâchettes)
Joystick gauche ↑ ↓ (mode char) moteur A, avant ou arrière verte / bleue
Joystick droit ↑ ↓ (mode char) moteur B verte / bleue
○ maintenu frein : les moteurs s'arrêtent net rouge
R1 maintenu roue libre : plus de commande, les moteurs ralentissent tout seuls
↑ / ↓ puissance maximale (de 75 à 255)

Le code

Nouveau croquis, coller le programme ou télécharger le fichier, enregistrer sous mission5_jakkumulateur.

Le programme · cliquer pour l'afficher
// Mission 5 · Jakkumulateur : deux moteurs pilotés à la manette, via le driver DRV8833
// Mode « gâchettes » : R2 avance, L2 recule (moteur A).
// Mode « char » (△) : joystick gauche = moteur A, joystick droit = moteur B.
// ○ : frein. R1 : roue libre. ↑ ↓ : puissance maximale.

#include <Bluepad32.h>

// ================= Branchements =================
const int A_IN1 = 25, A_IN2 = 26;    // moteur A (sorties OUT1 / OUT2 du driver)
const int B_IN1 = 32, B_IN2 = 33;    // moteur B (sorties OUT3 / OUT4), facultatif
const int ZONE_MORTE = 30;           // en dessous, on considère « pas de commande »

ControllerPtr manette = nullptr;
bool modeChar = false;               // faux : gâchettes ; vrai : deux joysticks
int puissanceMax = 200;              // sur 255 : réglable avec ↑ ↓
int sensPrecedent = 99;

enum { B_TRIANGLE, B_HAUT, B_BAS, NB_BOUTONS };
bool avant[NB_BOUTONS];

bool vientDAppuyer(int numero, bool appuye) {
  bool nouveau = appuye && !avant[numero];
  avant[numero] = appuye;
  return nouveau;
}

// ================= Piloter un moteur =================
// v de -255 (arrière à fond) à +255 (avant à fond), 0 = roue libre
void moteur(int in1, int in2, int v) {
  v = constrain(v, -255, 255);
  if (v > 0)      { analogWrite(in1, v);  analogWrite(in2, 0); }    // avant
  else if (v < 0) { analogWrite(in1, 0);  analogWrite(in2, -v); }   // arrière
  else            { analogWrite(in1, 0);  analogWrite(in2, 0); }    // roue libre
}

void freiner(int in1, int in2) {
  analogWrite(in1, 255);                 // les deux entrées à 1 :
  analogWrite(in2, 255);                 // le driver court-circuite le moteur, qui s'arrête net
}

// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;
  manette = ctl;
  sensPrecedent = 99;
  Serial.println("🎮 Manette connectée : moteurs prêts.");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    Serial.println("❌ Manette déconnectée : moteurs arrêtés.");
  }
}

// ================= Calculs =================
// Transforme une valeur de manette (de -min à +max) en vitesse de moteur (-puissanceMax à +puissanceMax)
int versVitesse(int valeur, int maxi) {
  if (abs(valeur) < ZONE_MORTE * maxi / 512) return 0;   // zone morte
  return constrain(map(valeur, -maxi, maxi, -puissanceMax, puissanceMax), -puissanceMax, puissanceMax);
}

void signaler(int v) {
  int sens = (v > 0) - (v < 0);
  if (sens == sensPrecedent || !manette) return;
  sensPrecedent = sens;
  if (sens > 0)      manette->setColorLED(0, 255, 0);    // vert : avant
  else if (sens < 0) manette->setColorLED(0, 80, 255);   // bleu : arrière
  else               manette->setColorLED(40, 40, 40);   // gris : arrêt
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  pinMode(A_IN1, OUTPUT); pinMode(A_IN2, OUTPUT);
  pinMode(B_IN1, OUTPUT); pinMode(B_IN2, OUTPUT);
  moteur(A_IN1, A_IN2, 0);
  moteur(B_IN1, B_IN2, 0);
  BP32.setup(&onConnectedController, &onDisconnectedController);
  BP32.forgetBluetoothKeys();   // à mettre en commentaire une fois que tout marche
  Serial.println("Jakkumulateur prêt. Maintiens SHARE + PS pour coupler la manette.");
}

void loop() {
  BP32.update();
  int vA = 0, vB = 0;

  if (manette && manette->isConnected()) {
    if (vientDAppuyer(B_TRIANGLE, manette->y())) {
      modeChar = !modeChar;
      Serial.println(modeChar ? "△ Mode char : un joystick par moteur" : "△ Mode gâchettes : R2 avance, L2 recule");
    }
    uint8_t f = manette->dpad();
    if (vientDAppuyer(B_HAUT, f & DPAD_UP))  { puissanceMax = min(puissanceMax + 25, 255); Serial.printf("Puissance max : %d / 255\n", puissanceMax); }
    if (vientDAppuyer(B_BAS, f & DPAD_DOWN)) { puissanceMax = max(puissanceMax - 25, 75);  Serial.printf("Puissance max : %d / 255\n", puissanceMax); }

    if (manette->b()) {                                 // ○ maintenu : frein
      freiner(A_IN1, A_IN2);
      freiner(B_IN1, B_IN2);
      if (sensPrecedent != 2) {                       // rouge, une seule fois
        manette->setColorLED(255, 0, 0);
        sensPrecedent = 2;
      }
      delay(15);
      return;
    }
    if (!manette->r1()) {                               // R1 maintenu : roue libre (tout à 0)
      if (modeChar) {
        vA = versVitesse(-manette->axisY(), 512);       // joystick vers le haut = avant
        vB = versVitesse(-manette->axisRY(), 512);
      } else {
        vA = versVitesse(manette->throttle() - manette->brake(), 1020);
      }
    }
  }

  moteur(A_IN1, A_IN2, vA);
  moteur(B_IN1, B_IN2, vB);
  signaler(vA != 0 ? vA : vB);
  delay(15);
}

Comment il est construit

Bloc Son travail
moteur(in1, in2, v) le cœur : PWM sur une entrée et 0 sur l'autre ; le signe de v choisit laquelle, donc le sens
freiner(in1, in2) les deux entrées à 1 : le driver relie les deux fils du moteur, qui s'arrête net
versVitesse transforme une gâchette ou un joystick en vitesse, avec une zone morte
throttle() - brake() R2 moins L2 : un seul nombre, positif en avant, négatif en arrière
modeChar un interrupteur logiciel : △ passe d'un mode de pilotage à l'autre
IN1 IN2 Ce que fait le moteur
0 0 roue libre
PWM 0 avant, plus ou moins vite
0 PWM arrière, plus ou moins vite
1 1 frein

Ce qu'on apprend ici

  • Un signe pour le sens : une seule variable, de −255 à +255, porte à la fois le sens et la vitesse.
  • Le PWM pour la puissance : le moteur ne voit que la tension moyenne, comme l'œil avec une LED.
  • Deux façons d'arrêter : la roue libre (on coupe) et le frein (on court-circuite le moteur).

🎓 Académie : Le moteur et son driver · Le PWM

Option · L'holo-écran

Avec l'holo-écran de la mission 1 bis, Jakkumulateur affiche le mode de pilotage, une barre par moteur (vers le haut : en avant, vers le bas : en arrière), la tension moyenne reçue par chaque moteur, la puissance maximale, et FREIN ou ROUE LIBRE.

L'écran en mode char
Mode char : le moteur A en avant, le B en arrière.
L'écran pendant le freinage
○ maintenu : FREIN, les deux moteurs sont bloqués.

Aperçus calculés sur ordinateur, à partir du vrai programme et de la vraie bibliothèque graphique d'Adafruit : c'est ce que l'écran devrait afficher, avec sa bande jaune en haut et le reste en bleu.

Brancher l'écran

Les quatre fils de la mission 1 bis, USB débranché : VCC → 3V3, GND → GND, SCL → D22, SDA → D21. Le driver et ses fils ne bougent pas. L'écran prend sa place sur la plaque d'essai, et se partage la masse avec le driver.

ESP32 (support)D21D223V3GNDNATHAN0,96″ · 128 × 64GNDVCCSCLSDAVCC → 3V3 · GND → GNDSCL → D22 · SDA → D21l'ordre des broches changeselon les modèles : lis les noms !
Les mêmes quatre fils qu'à la mission 1 bis, en plus du montage de la mission : aucune de ses broches n'est utilisée par l'écran.

Le 3V3 sert déjà à EEP ? On le partage

Le 3V3 de l'ESP32 alimente déjà la broche EEP du driver. Pour brancher aussi l'écran : un fil du 3V3 vers une colonne libre de la plaque d'essai (ses 5 trous sont reliés entre eux), puis un fil de cette colonne vers EEP, et un autre vers le VCC de l'écran. Ne jamais utiliser pour ça la ligne + qui reçoit le 5 V de l'alimentation externe : 3,3 V et 5 V ne se mélangent pas.

Ce qu'il affiche

Zone Ce qu'on y voit
En haut MODE GACHETTES ou MODE CHAR (△), et la batterie de la manette
Les barres A et B la vitesse de chaque moteur : vers le haut, en avant ; vers le bas, en arrière
A et B en volts la tension moyenne reçue par chaque moteur
Max la puissance maximale, réglée avec ↑ ↓ (sur 255)
FREIN, ROUE LIBRE en noir sur fond blanc, quand ○ ou R1 est maintenu

Le code

Nouveau croquis, coller le programme ou télécharger le fichier, enregistrer sous mission5_jakkumulateur_ecran. C'est le programme de la mission, avec l'écran en plus : les commandes ne changent pas.

Le programme holo-écran · cliquer pour l'afficher
// Mission 5 · Jakkumulateur, version holo-écran
// Le même programme que Jakkumulateur, avec en plus l'écran OLED qui affiche :
// le mode, une barre par moteur (vers le haut : avant, vers le bas : arrière),
// la tension moyenne reçue par chaque moteur, la puissance maximale, et FREIN / ROUE LIBRE.
// Sans écran branché, tout marche quand même.

#include <Bluepad32.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ================= Branchements =================
const int A_IN1 = 25, A_IN2 = 26;    // moteur A (sorties OUT1 / OUT2 du driver)
const int B_IN1 = 32, B_IN2 = 33;    // moteur B (sorties OUT3 / OUT4), facultatif
const int ZONE_MORTE = 30;           // en dessous, on considère « pas de commande »

ControllerPtr manette = nullptr;
bool modeChar = false;               // faux : gâchettes ; vrai : deux joysticks
int puissanceMax = 200;              // sur 255 : réglable avec ↑ ↓
int sensPrecedent = 99;
int vitesseA = 0, vitesseB = 0;      // les dernières vitesses envoyées, pour l'écran
const char* etat = "";               // "FREIN", "ROUE LIBRE" ou rien
const float TENSION_DRIVER = 5.0;    // la tension qui alimente le driver (l'alimentation externe, 5 V)

enum { B_TRIANGLE, B_HAUT, B_BAS, NB_BOUTONS };
bool avant[NB_BOUTONS];

bool vientDAppuyer(int numero, bool appuye) {
  bool nouveau = appuye && !avant[numero];
  avant[numero] = appuye;
  return nouveau;
}

// ================= Piloter un moteur =================
// v de -255 (arrière à fond) à +255 (avant à fond), 0 = roue libre
void moteur(int in1, int in2, int v) {
  v = constrain(v, -255, 255);
  if (v > 0)      { analogWrite(in1, v);  analogWrite(in2, 0); }    // avant
  else if (v < 0) { analogWrite(in1, 0);  analogWrite(in2, -v); }   // arrière
  else            { analogWrite(in1, 0);  analogWrite(in2, 0); }    // roue libre
}

void freiner(int in1, int in2) {
  analogWrite(in1, 255);                 // les deux entrées à 1 :
  analogWrite(in2, 255);                 // le driver court-circuite le moteur, qui s'arrête net
}

// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;
  manette = ctl;
  sensPrecedent = 99;
  Serial.println("🎮 Manette connectée : moteurs prêts.");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    Serial.println("❌ Manette déconnectée : moteurs arrêtés.");
  }
}

// ================= Calculs =================
// Transforme une valeur de manette (de -min à +max) en vitesse de moteur (-puissanceMax à +puissanceMax)
int versVitesse(int valeur, int maxi) {
  if (abs(valeur) < ZONE_MORTE * maxi / 512) return 0;   // zone morte
  return constrain(map(valeur, -maxi, maxi, -puissanceMax, puissanceMax), -puissanceMax, puissanceMax);
}

void signaler(int v) {
  int sens = (v > 0) - (v < 0);
  if (sens == sensPrecedent || !manette) return;
  sensPrecedent = sens;
  if (sens > 0)      manette->setColorLED(0, 255, 0);    // vert : avant
  else if (sens < 0) manette->setColorLED(0, 80, 255);   // bleu : arrière
  else               manette->setColorLED(40, 40, 40);   // gris : arrêt
}

// ================= L'holo-écran =================
Adafruit_SSD1306 ecran(128, 64, &Wire, -1);
bool ecranPret = false;            // faux si l'écran n'est pas branché : le reste marche quand même
unsigned long derniereImage = 0;
const int PERIODE_ECRAN = 100;     // une image toutes les 100 ms : 10 images par seconde

void demarrerEcran() {
  Wire.begin(21, 22);              // SDA sur D21, SCL sur D22
  ecranPret = ecran.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  if (!ecranPret) {
    Serial.println("📟 Pas d'écran trouvé : on continue sans lui.");
    return;
  }
  ecran.setTextColor(SSD1306_WHITE);
  ecran.setTextWrap(false);
}

// Une jauge horizontale : un cadre, rempli selon la valeur (0 à maxi)
void dessinerJauge(int x, int y, int largeur, int valeur, int maxi) {
  ecran.drawRect(x, y, largeur, 7, SSD1306_WHITE);
  int plein = (largeur - 2) * constrain(valeur, 0, maxi) / maxi;
  ecran.fillRect(x + 1, y + 1, plein, 5, SSD1306_WHITE);
}

// Le titre dans la bande jaune (lignes 0 à 15), avec la batterie de la manette à droite (ou « ... » qui clignote sans manette)
void dessinerTitre(const char* titre) {
  ecran.setTextSize(1);
  ecran.setCursor(0, 3);
  ecran.print(titre);
  if (manette && manette->battery() > 0) dessinerJauge(100, 3, 28, manette->battery(), 255);
  else if (!manette && (millis() / 500) % 2) { ecran.setCursor(110, 3); ecran.print("..."); }
  ecran.drawFastHLine(0, 13, 128, SSD1306_WHITE);  // la bande jaune s'arrête à la ligne 15
}

// Une barre verticale centrée sur zéro : vers le haut = avant, vers le bas = arrière
void dessinerBarreMoteur(int x, const char* nom, int v) {
  const int milieu = 36, demi = 17;
  ecran.drawRect(x, milieu - demi - 1, 14, 2 * demi + 3, SSD1306_WHITE);
  ecran.drawFastHLine(x - 3, milieu, 20, SSD1306_WHITE);           // le repère du zéro
  int h = demi * abs(v) / 255;
  if (v > 0) ecran.fillRect(x + 2, milieu - h, 10, h, SSD1306_WHITE);
  else       ecran.fillRect(x + 2, milieu + 1, 10, h, SSD1306_WHITE);
  ecran.setCursor(x + 4, 57);
  ecran.print(nom);
}

void mettreAJourEcran() {
  if (!ecranPret || millis() - derniereImage < PERIODE_ECRAN) return;
  derniereImage = millis();
  ecran.clearDisplay();
  dessinerTitre(modeChar ? "MODE CHAR" : "MODE GACHETTES");

  dessinerBarreMoteur(6, "A", vitesseA);
  dessinerBarreMoteur(30, "B", vitesseB);

  // La tension moyenne que reçoit chaque moteur : la part du temps « allumé » x la tension du driver
  ecran.setTextSize(1);
  ecran.setCursor(56, 18);
  ecran.printf("A %.1f V", abs(vitesseA) / 255.0 * TENSION_DRIVER);
  ecran.setCursor(56, 28);
  ecran.printf("B %.1f V", abs(vitesseB) / 255.0 * TENSION_DRIVER);
  ecran.setCursor(56, 38);
  ecran.printf("Max %d", puissanceMax);

  // FREIN ou ROUE LIBRE : en noir sur fond blanc, pour qu'on le voie bien
  if (strlen(etat) > 0) {
    ecran.fillRect(54, 47, 74, 11, SSD1306_WHITE);
    ecran.setTextColor(SSD1306_BLACK);
    ecran.setCursor(57, 49);
    ecran.print(etat);
    ecran.setTextColor(SSD1306_WHITE);
  }
  ecran.display();
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  pinMode(A_IN1, OUTPUT); pinMode(A_IN2, OUTPUT);
  pinMode(B_IN1, OUTPUT); pinMode(B_IN2, OUTPUT);
  moteur(A_IN1, A_IN2, 0);
  moteur(B_IN1, B_IN2, 0);
  demarrerEcran();
  BP32.setup(&onConnectedController, &onDisconnectedController);
  BP32.forgetBluetoothKeys();   // à mettre en commentaire une fois que tout marche
  Serial.println("Jakkumulateur prêt. Maintiens SHARE + PS pour coupler la manette.");
}

void loop() {
  BP32.update();
  int vA = 0, vB = 0;
  bool frein = false;
  etat = "";

  if (manette && manette->isConnected()) {
    if (vientDAppuyer(B_TRIANGLE, manette->y())) {
      modeChar = !modeChar;
      Serial.println(modeChar ? "△ Mode char : un joystick par moteur" : "△ Mode gâchettes : R2 avance, L2 recule");
    }
    uint8_t f = manette->dpad();
    if (vientDAppuyer(B_HAUT, f & DPAD_UP))  { puissanceMax = min(puissanceMax + 25, 255); Serial.printf("Puissance max : %d / 255\n", puissanceMax); }
    if (vientDAppuyer(B_BAS, f & DPAD_DOWN)) { puissanceMax = max(puissanceMax - 25, 75);  Serial.printf("Puissance max : %d / 255\n", puissanceMax); }

    if (manette->b()) {                                 // ○ maintenu : frein
      frein = true;
    } else if (!manette->r1()) {                        // R1 maintenu : roue libre (tout à 0)
      if (modeChar) {
        vA = versVitesse(-manette->axisY(), 512);       // joystick vers le haut = avant
        vB = versVitesse(-manette->axisRY(), 512);
      } else {
        vA = versVitesse(manette->throttle() - manette->brake(), 1020);
      }
    } else {
      etat = "ROUE LIBRE";
    }
  }

  if (frein) {
    freiner(A_IN1, A_IN2);
    freiner(B_IN1, B_IN2);
    if (sensPrecedent != 2) {                           // rouge, une seule fois
      manette->setColorLED(255, 0, 0);
      sensPrecedent = 2;
    }
    etat = "FREIN";
  } else {
    moteur(A_IN1, A_IN2, vA);
    moteur(B_IN1, B_IN2, vB);
    signaler(vA != 0 ? vA : vB);
  }
  vitesseA = vA;                                        // mémorisées pour l'écran
  vitesseB = vB;
  mettreAJourEcran();
  delay(15);
}

Ce qui change par rapport au programme de base

Ajout Son travail
#include <Wire.h>, <Adafruit_GFX.h>, <Adafruit_SSD1306.h> les bibliothèques de l'écran, déjà installées à la mission 1 bis
demarrerEcran(), dans setup() démarre le bus I2C et l'écran ; s'il n'est pas branché, ecranPret reste faux et le programme continue sans lui
mettreAJourEcran(), dans loop() redessine l'écran, mais toutes les 100 ms seulement (10 images par seconde)
dessinerTitre(), dessinerJauge() les petites fonctions de dessin, les mêmes dans toutes les missions
vitesseA, vitesseB, etat mémorisent ce qui a été envoyé aux moteurs, pour l'afficher
TENSION_DRIVER la tension qui alimente le driver : les 5 V de l'alimentation externe
loop() réorganisée le freinage ne fait plus return au milieu : l'écran est mis à jour à chaque tour, même pendant le freinage

Pourquoi seulement 10 images par seconde ?

Envoyer une image à l'écran prend environ 23 ms (le calcul est dans la fiche I2C). Pendant ce temps, le programme ne fait rien d'autre. En redessinant toutes les 100 ms, l'écran ne prend qu'un quart du temps, et la manette et les moteurs restent réactifs. Pour des chiffres, 10 images par seconde, c'est largement assez.

À essayer · Le PWM en volts

La tension affichée, c'est le principe du PWM : le driver allume et éteint très vite, et le moteur ne « sent » que la moyenne. Avec une vitesse de 200 sur 255 : 200 ÷ 255 × 5 V ≈ 3,9 V. Vérifie-le en mettant la puissance max à 200 et une gâchette à fond !

Le programme suppose 5 V, ce que donne l'alimentation externe. Le multimètre peut le vérifier entre VCC et GND du driver.

🎓 Académie : Le bus I2C · Le temps · Le PWM · Le moteur et son driver

Si ça coince

Symptôme Cause probable Solution
Rien ne tourne le driver dort fil EEP → 3V3
Rien ne tourne pas d'énergie pour les moteurs alimentation 5 V allumée ? + du bornier sur VCC du driver, masse commune
L'ESP32 redémarre sans arrêt, la manette se déconnecte puis s'éteint les moteurs sont alimentés par l'USB (VCC du driver sur VIN) alimentation externe sur VCC, et le condensateur
La carte « disparaît » de l'ordinateur un instant (le port série se ferme tout seul) même cause : l'ordinateur coupe son port USB, surchargé même solution
Un sens marche, l'autre non ; et ○ fait avancer au lieu de freiner une des deux commandes du moteur (IN2 ou IN4) n'arrive pas au driver vérifier le fil : sur l'ESP32, D34 et D35, juste à côté de D32 et D33, ne savent que lire
Il tourne dans le mauvais sens les 2 fils du moteur inversés échanger les fils sur OUT1 / OUT2 (ou dans le code, IN1 et IN2)
Il ne démarre qu'à forte gâchette il faut une tension minimale pour vaincre les frottements normal ; on peut augmenter ZONE_MORTE
Le moniteur série est inondé de lignes de chiffres (« 8 D5 68 5C… ») des messages de débogage internes Outils → Core Debug Level → None, puis re-téléverser
Le condensateur tiédit il est branché à l'envers tout débrancher, le retourner : bande « − » côté GND
Le driver chauffe moteur bloqué libérer la mécanique, ne pas le laisser forcer

Défis bonus

Les solutions sont rangées dans des holocrons scellés : essaie d'abord, l'indice est là si tu bloques, et le code d'accès du maître de l'atelier ouvre la solution.

Défi 1 · Le turbo

Tant que ✕ est maintenu, la vitesse peut monter jusqu'à 255, quelle que soit la puissance max réglée avec les flèches.

Indice

Une règle de trois : si vA est sur une échelle de 0 à puissanceMax, combien vaut-il sur une échelle de 0 à 255 ? Attention à l'ordre des opérations avec les nombres entiers.

Défi 2 · L'accélération progressive

Les moteurs accélèrent et freinent en douceur au lieu de démarrer d'un coup.

Indice

Comme le « démarrage en douceur » de la mission 4 : une vitesse adoucie qui suit la vitesse demandée, sans jamais bouger de plus d'un petit pas par tour. Une fonction évitera d'écrire deux fois la même chose pour les deux moteurs.

Validation

  • Phase 1 : les deux moteurs tournent sur les piles, dans les deux sens
  • Le driver est alimenté par l'alimentation externe, avec le condensateur et la masse commune
  • Le test avec le driver : avant, arrêt, arrière
  • R2 et L2 pilotent le moteur A, plus ou moins vite
  • Le mode char fait tourner les deux moteurs avec les joysticks
  • ○ freine, R1 met en roue libre
  • Bonus : au moins un défi réussi