Séance 27 · Partie E · Robotique
Moteurs et
robot mobile.
Deux moteurs, un pont en H, un châssis. Aujourd’hui le robot roule, tourne, et — grâce aux encodeurs — sait à peu près où il est. Les maths de la séance 14 deviennent du mouvement.
Durée : 2 séances · Objectifs : moteur CC et réducteur, pont en H, commande PWM en vitesse et en sens, différentiel, zone morte et calibration, encodeurs, odométrie réelle, une classe Robot réutilisable.
Ce que vous saurez faire à la fin
- Câbler et piloter deux moteurs en toute sécurité.
- Faire avancer, tourner, pivoter le robot avec des commandes de haut niveau.
- Mesurer la vitesse réelle et estimer la position (odométrie).
- Comprendre pourquoi le robot ne va pas droit, et corriger.
Matériel : châssis 2WD, 2 moteurs TT avec réducteur (6 V), pont en H L298N ou TB6612, pack 6 × AA, carte (Arduino ou Pico), encodeurs à fente si disponibles.
01 / Moteur
Le moteur à courant continu et son réducteur
- Une tension → une vitesse de rotation (à vide, proportionnelle). Inverser la tension inverse le sens.
- Le couple (la force de rotation) est proportionnel au courant. Bloqué, le moteur tire son courant maximal (« courant de blocage », 1–2 A pour un moteur TT) : chauffe et vide la batterie.
- Le réducteur (engrenages 1:48 sur les moteurs TT) divise la vitesse et multiplie le couple : 12 000 tr/min → 250 tr/min à la roue.
- En dessous d’une tension minimale, le moteur ne démarre pas : la zone morte.
Ce qu’il faut retenir
Vitesse ↔ tension, couple ↔ courant, et les deux sont liés : plus le moteur force, moins il tourne vite. Un robot qui monte une pente ralentit et consomme plus. Le courant de blocage dimensionne le pont en H et la batterie (séance 21). Une roue de 6,5 cm à 250 tr/min : 85 cm/s, largement assez pour un robot de table.
01 / Moteur
Le pont en H : sens et vitesse
+Vbat
┌───┴───┐
[S1] [S2]
│ M │
├──(M)──┤
│ │
[S3] [S4]
└───┬───┘
GND
S1+S4 fermés : courant → : avant
S2+S3 fermés : courant ← : arrière
S3+S4 fermés : moteur en court-circuit : FREIN
tous ouverts : roue libre
S1+S3 fermés : COURT-CIRCUIT de la batterie !
(les modules l'interdisent par logique)| Module | Entrées par moteur | Remarques |
|---|---|---|
| L298N | IN1, IN2 (sens), ENA (PWM vitesse) | Robuste, 2 A, mais perd ~2 V (vieille techno) : moteur 6 V → alimenter en 7–9 V |
| TB6612FNG | AIN1, AIN2, PWMA, STBY | Moderne, efficace, 1,2 A, chute < 0,5 V. Recommandé. |
| DRV8833 | IN1, IN2 (PWM sur l’une) | Petit, 1,5 A, 2 moteurs |
Masse commune entre carte, pont et batterie. Alimentation logique (5 V / 3,3 V) séparée de l’alimentation moteurs (VM).
Freiner ou rouler libre ?
Quand on coupe la commande, deux choix : court-circuiter le moteur (il freine fort : le moteur devient générateur et dissipe l’énergie) ou l’ouvrir (il roule librement). Pour un robot, le frein donne des arrêts nets et une meilleure odométrie. Les ponts modernes le font avec IN1 = IN2 = HIGH. La PWM appliquée à IN1/IN2 permet même de « freiner un peu » : c’est ce que fait un vélo électrique en récupérant l’énergie.
02 / Code
Piloter un moteur : la fonction de base
// Arduino + TB6612 : moteur A
const byte AIN1 = 7, AIN2 = 8, PWMA = 9, STBY = 6;
void setup() {
pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT);
pinMode(PWMA, OUTPUT); pinMode(STBY, OUTPUT);
digitalWrite(STBY, HIGH); // sortir de veille
}
// vitesse : -255 … 255 ; 0 = frein
void moteurA(int vitesse) {
vitesse = constrain(vitesse, -255, 255);
if (vitesse > 0) { digitalWrite(AIN1, HIGH); digitalWrite(AIN2, LOW); }
else if (vitesse < 0) { digitalWrite(AIN1, LOW); digitalWrite(AIN2, HIGH); }
else { digitalWrite(AIN1, HIGH); digitalWrite(AIN2, HIGH); } // frein
analogWrite(PWMA, abs(vitesse));
}
void loop() {
moteurA(150); delay(1000);
moteurA(0); delay(500);
moteurA(-150); delay(1000);
moteurA(0); delay(2000);
}# MicroPython (Pico) + TB6612 : moteur A
from machine import Pin, PWM
class Moteur:
def __init__(self, in1, in2, pwm, stby=None):
self.in1, self.in2 = Pin(in1, Pin.OUT), Pin(in2, Pin.OUT)
self.pwm = PWM(Pin(pwm)); self.pwm.freq(1000)
if stby is not None: Pin(stby, Pin.OUT).on()
def vitesse(self, v): # -100 … 100
v = max(-100, min(100, v))
self.in1.value(v >= 0); self.in2.value(v <= 0) # v=0 : les deux à 1 = frein
self.pwm.duty_u16(int(abs(v) / 100 * 65535))
def stop(self):
self.vitesse(0)
a = Moteur(2, 3, 4, stby=5)
a.vitesse(60)La PWM des moteurs : 1 kHz minimum, sinon on entend siffler ; 20 kHz pour le silence (le TB6612 accepte jusqu’à 100 kHz, le L298N ~20 kHz).
Sécurité logicielle
Premier geste de setup() : tous les moteurs à 0. Un chien de garde (séance 22) qui coupe les moteurs si le programme se bloque. Une rampe : passer de 0 à 255 en 200 ms plutôt qu’instantanément évite les pointes de courant qui font redémarrer la carte (séance 21). Testez d’abord roues en l’air, robot sur un support.
02 / Code
Deux moteurs : le différentiel
// Commande de haut niveau : v (avance, -255..255) et w (rotation, -255..255)
void deplacer(int v, int w) {
int gauche = constrain(v - w, -255, 255);
int droite = constrain(v + w, -255, 255);
moteurA(gauche);
moteurB(droite);
}
// deplacer(200, 0) : tout droit
// deplacer(0, 150) : pivote sur place (gauche recule, droite avance) → tourne à gauche
// deplacer(150, 60) : arc vers la gauche
// deplacer(0, 0) : freinLe carré ne se ferme pas
Dans la simulation, oui. Sur le vrai robot : les deux moteurs n’ont pas exactement la même vitesse à PWM égale (±10 % est courant), les roues patinent, la batterie se vide. Piloter « en boucle ouverte » par des durées ne marche qu’à peu près. Il faut mesurer ce que font les roues : les encodeurs.
02 / Code
Zone morte et calibration des moteurs
Procédure de calibration
Roues en l’air, un encodeur ou une marque sur la roue et un chronomètre (ou une vidéo au ralenti) : pour chaque PWM, mesurer les tours par seconde de chaque roue. Puis ajuster une droite et l’inverser. Le firmware reçoit alors des vitesses en cm/s, pas des PWM, et compense la différence entre moteurs. C’est une boucle ouverte calibrée : bien mieux, mais toujours sans retour. La boucle fermée (séance 29) corrigera le reste.
03 / Encodeurs
Mesurer la rotation : encodeurs à fente
// Un disque à 20 fentes sur l'axe, un capteur optique (fourche) : 20 impulsions / tour
const byte ENC_G = 2, ENC_D = 3; // broches d'interruption
volatile long ticks_g = 0, ticks_d = 0;
void isr_g() { ticks_g++; }
void isr_d() { ticks_d++; }
void setup() {
attachInterrupt(digitalPinToInterrupt(ENC_G), isr_g, RISING);
attachInterrupt(digitalPinToInterrupt(ENC_D), isr_d, RISING);
}
const float CM_PAR_TICK = 3.1416 * 6.5 / 20; // périmètre / fentes = 1.02 cm
void loop() {
static unsigned long t = 0;
if (millis() - t >= 100) {
t = millis();
noInterrupts(); long g = ticks_g, d = ticks_d; ticks_g = ticks_d = 0; interrupts();
float vg = g * CM_PAR_TICK * 10, vd = d * CM_PAR_TICK * 10; // cm/s (×10 car 100 ms)
Serial.print(vg); Serial.print(" "); Serial.println(vd);
}
}L’encodeur à fente ne connaît pas le sens : on le déduit de la commande. Un encodeur en quadrature (deux capteurs décalés, code de Gray de la séance 13) donne le sens et 4 fois plus de résolution.
Résolution : 1 cm par tick, c’est grossier. Sur 100 ms à 20 cm/s : 2 ticks → ±50 % d’erreur sur la vitesse instantanée. On filtre (moyenne glissante) ou on mesure la durée entre ticks (micros()) plutôt que leur nombre.
Les moteurs avec encodeur magnétique intégré (N20, 7 ticks × réduction 100 = 700 ticks/tour) sont bien meilleurs pour 3 € de plus.
03 / Encodeurs
Odométrie réelle : intégrer les ticks
Lecture
Sans patinage, le carré se ferme à la résolution des ticks près. Avec 3 % de ticks perdus (roues qui glissent, sol poussiéreux), l’erreur atteint plusieurs cm par tour — et surtout le cap dérive, ce qui fausse tout le reste. C’est la séance 14 (odométrie bruitée) sur du vrai matériel. Remèdes par ordre de coût : bons pneus, encodeurs fins, gyroscope pour le cap (séance 25, filtre complémentaire), puis capteurs externes (lidar, caméra, balises).
04 / Architecture
La classe Robot : le firmware complet des moteurs
# MicroPython — robot.py : à copier sur la carte, utilisé par les séances 28 et 29
from machine import Pin, PWM
import time, math
class Moteur:
def __init__(self, in1, in2, pwm, zone_morte=0.2, gain=1.0):
self.in1, self.in2 = Pin(in1, Pin.OUT), Pin(in2, Pin.OUT)
self.pwm = PWM(Pin(pwm)); self.pwm.freq(1000)
self.zone_morte, self.gain = zone_morte, gain # calibration (séance : zone morte, gain)
self.consigne = self.actuel = 0.0
def vitesse(self, v): # -1.0 … 1.0
self.consigne = max(-1.0, min(1.0, v))
def maj(self, rampe=0.1): # appelée à chaque cycle : rampe douce
if self.actuel < self.consigne: self.actuel = min(self.consigne, self.actuel + rampe)
else: self.actuel = max(self.consigne, self.actuel - rampe)
v = self.actuel * self.gain
duty = 0 if abs(v) < 0.02 else self.zone_morte + abs(v) * (1 - self.zone_morte)
self.in1.value(v >= 0); self.in2.value(v <= 0)
self.pwm.duty_u16(int(min(1.0, duty) * 65535))
def stop(self): self.consigne = self.actuel = 0.0; self.maj()
class Encodeur:
def __init__(self, broche):
self.ticks = 0
Pin(broche, Pin.IN, Pin.PULL_UP).irq(handler=self._isr, trigger=Pin.IRQ_RISING)
def _isr(self, pin): self.ticks += 1
def lire_et_raz(self):
n, self.ticks = self.ticks, 0
return n
class Robot:
L, CM_PAR_TICK = 12.0, math.pi * 6.5 / 20
def __init__(self):
self.g = Moteur(2, 3, 4, gain=1.0); self.d = Moteur(6, 7, 8, gain=0.93) # calibré
Pin(5, Pin.OUT).on() # STBY
self.enc_g, self.enc_d = Encodeur(10), Encodeur(11)
self.x = self.y = self.cap = 0.0
self.sens_g = self.sens_d = 1
def deplacer(self, v, w): # v, w dans -1 … 1
self.g.vitesse(v - w); self.d.vitesse(v + w)
self.sens_g = 1 if v - w >= 0 else -1; self.sens_d = 1 if v + w >= 0 else -1
def stop(self): self.g.stop(); self.d.stop()
def maj(self): # à appeler à 20-50 Hz
self.g.maj(); self.d.maj()
tg, td = self.enc_g.lire_et_raz() * self.sens_g, self.enc_d.lire_et_raz() * self.sens_d
dg, dd = tg * self.CM_PAR_TICK, td * self.CM_PAR_TICK
dist, dtheta = (dg + dd) / 2, (dd - dg) / self.L
cm = self.cap + dtheta / 2
self.x += dist * math.cos(cm); self.y += dist * math.sin(cm); self.cap += dtheta
def pose(self): return self.x, self.y, math.degrees(self.cap)Ce que cette classe apporte
Une interface de haut niveau (deplacer(v, w), pose()) qui cache le pont en H, la calibration, la rampe, les encodeurs et l’odométrie. Les séances suivantes n’écrivent plus une ligne de PWM. La rampe limite les pointes de courant. Le sens des ticks vient de la commande (les encodeurs à fente ne le donnent pas). Cette classe est la version « pour de vrai » de celle de la séance 16 : composition (deux moteurs, deux encodeurs), une responsabilité par classe.
05 / Défis
Défi ★ — Chorégraphie et calibration
Consigne
1. Avec la classe Robot : avancer 1 m, demi-tour, revenir, en boucle ouverte (durées). Mesurer l’écart au point de départ sur 5 essais.
2. Calibrer : roues en l’air, 10 valeurs de PWM, ticks d’encodeur sur 2 s pour chaque roue ; ajuster gain et zone_morte de chaque moteur. Refaire le test 1 : l’écart diminue-t-il ?
3. Ajouter à Robot une méthode avancer_cm(d) qui utilise l’odométrie pour s’arrêter à la bonne distance, et tourner_deg(a).
Correction (avancer_cm)
def avancer_cm(self, distance, v=0.5):
x0, y0 = self.x, self.y
self.deplacer(v if distance > 0 else -v, 0)
while math.hypot(self.x - x0, self.y - y0) < abs(distance):
self.maj(); time.sleep_ms(20)
self.stop()
def tourner_deg(self, angle, w=0.4):
cap0 = self.cap; cible = math.radians(angle)
self.deplacer(0, w if angle > 0 else -w)
while abs(self.cap - cap0) < abs(cible):
self.maj(); time.sleep_ms(20)
self.stop()Le robot dépasse un peu (inertie + rampe de décélération) : la séance 29 rend cela précis. Notez que ces méthodes sont bloquantes — acceptable pour une chorégraphie, pas pour un robot qui doit surveiller ses capteurs en même temps (séance 28).
05 / Défis
Défi ★★ — Aller droit malgré tout
Consigne
Même calibré, le robot dévie. Utiliser les encodeurs en boucle fermée : à chaque cycle (50 Hz), comparer les ticks cumulés gauche et droite depuis le départ ; si la gauche a plus avancé, ralentir la gauche / accélérer la droite proportionnellement à l’écart (w = −Kp × (ticks_g − ticks_d)). Trouver Kp. Mesurer la déviation latérale sur 2 m avant et après. Puis remplacer la référence « ticks égaux » par « cap du gyroscope = 0 » (séance 25) : lequel est le meilleur sur un sol glissant ?
Correction
def aller_droit(self, distance, v=0.5, kp=0.02):
x0, y0 = self.x, self.y; cumul_g = cumul_d = 0
while math.hypot(self.x - x0, self.y - y0) < distance:
self.maj()
cumul_g += self.enc_g_derniers; cumul_d += self.enc_d_derniers # à exposer dans maj()
w = -kp * (cumul_g - cumul_d)
self.deplacer(v, max(-0.3, min(0.3, w)))
time.sleep_ms(20)
self.stop()C’est un correcteur proportionnel sur la différence de distance : le robot oscille légèrement autour de la ligne droite puis se stabilise. Avec le gyroscope, on asservit le cap lui-même : insensible au patinage d’une roue, mais sensible à la dérive du gyro. Le meilleur : les deux, fusionnés. Vous venez de faire de la commande : la séance 29 la formalise.
05 / Défis
Défi ★★★ — Esprit prépa : suivre une trajectoire courbe
Consigne
Le robot doit suivre un cercle de rayon R = 40 cm à vitesse v = 20 cm/s. 1. Calculer les vitesses des deux roues (v_g, v_d) en fonction de R, v et L (indice : les deux roues décrivent des cercles concentriques de rayons R ± L/2, en un même temps). 2. En boucle ouverte, avec la calibration : mesurer l’écart au cercle après un tour. 3. En boucle fermée « pure pursuit » : à chaque cycle, viser un point du cercle situé à 15 cm devant le robot (sur la trajectoire), calculer la courbure nécessaire pour l’atteindre (κ = 2·sin(α)/d, α = angle entre le cap et la direction du point), et en déduire w. Simuler avec le modèle de la séance 14, puis sur le robot.
Direction
v_g = 17 cm/s, v_d = 23 cm/s : le tour dure 12,6 s. En boucle ouverte, une erreur de 5 % sur un moteur transforme le cercle en spirale. Pure pursuit (1985, encore utilisé sur les voitures autonomes) corrige en visant toujours un point devant : un lookahead court suit précisément mais oscille, un long est doux mais coupe les virages. La formule κ = 2 sin α / d vient de la géométrie de l’arc de cercle passant par le robot, tangent à son cap, et par le point visé — démontrez-la. En prépa, ce type de commande géométrique se relie aux courbes paramétrées et à la courbure.
06 / Vérification
Un robot monte une pente à PWM constante. Que se passe-t-il ?
Deux questions supplémentaires
1. Que fait un pont en H avec les deux entrées à HIGH ? Frein (moteur en court-circuit) sur la plupart des modules.
2. Pourquoi l’odométrie dérive-t-elle ? Chaque erreur (tick perdu, patinage) s’accumule et n’est jamais corrigée sans capteur externe.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| Pont en H | 4 interrupteurs pour choisir le sens du courant dans un moteur. |
| Couple | Force de rotation ; proportionnel au courant. |
| Zone morte | Plage de PWM où le moteur ne démarre pas. |
| Différentiel | Deux roues motrices indépendantes ; v et w. |
| Encodeur | Capteur qui compte la rotation d’une roue. |
| Boucle ouverte / fermée | Sans / avec mesure du résultat. |
| Rampe | Variation progressive de la commande. |
Pour continuer
Le robot roule
Séance suivante : il regarde où il va. Capteurs et machine à états pour un comportement autonome et robuste.
À faire chez soi
- Calibrer vos deux moteurs et remplir la table PWM → vitesse.
- Tester la rampe : mesurer le courant de démarrage avec et sans (multimètre en série, séance 21).
- Faire dessiner un carré de 50 cm au robot avec un feutre fixé au châssis. Mesurer l’erreur de bouclage.
← Séance 26SommaireSéance 28 : Capteurs et machine à états →