PYTHON → ROBOTIQUE · 29

Séance 29 · Partie E · Robotique

Régulation PID
et suivi de ligne.

Tenir une vitesse malgré la pente, suivre une ligne sans zigzaguer, garder un drone à l’horizontale : un même algorithme de dix lignes, inventé pour les navires en 1922, fait tout cela. C’est le PID, et il est dans chaque robot, chaque four, chaque régulateur de vitesse.

Durée : 2 séances · Objectifs : boucle fermée, les trois termes P, I, D et ce que chacun corrige, réglage (Ziegler-Nichols et méthode manuelle), saturation et anti-windup, PID de vitesse sur encodeur, PID de suivi de ligne, PID en cascade.

Ce que vous saurez faire à la fin
  • Expliquer avec les mains ce que font P, I et D, et le montrer en simulation.
  • Régler un PID méthodiquement sur un vrai robot.
  • Asservir la vitesse des roues, puis le suivi de ligne, puis les deux ensemble.
  • Reconnaître les pièges : saturation, bruit sur D, retard.

Matériel : le robot des séances 27–28 avec encodeurs et capteurs de ligne. Les simulations tournent dans la page.

01 / Boucle fermée

Le principe : mesurer, comparer, corriger

   consigne ──▶(+)── erreur ──▶ [contrôleur] ── commande ──▶ [système] ──┬──▶ sortie
                (−)                                                       │
                 ▲                                                        │
                 └──────────────────── mesure ◀───────────[capteur]◀──────┘

   erreur = consigne − mesure
   commande = f(erreur)          ← le PID est un choix de f

Boucle ouverte (séance 27) : « PWM 150 donne 20 cm/s » — vrai sur table, faux en pente, faux à batterie faible.

Boucle fermée : « je veux 20 cm/s ; je mesure 17 ; je pousse plus ». Le contrôleur ne connaît pas la pente : il voit l’erreur et la combat.

Robustesse : la boucle fermée corrige ce qu’on n’a pas prévu. C’est la raison de son omniprésence.

Vous en avez déjà fait

Séance 15 (chariot P et PD), séance 23 (luminosité), séance 27 (aller droit avec les encodeurs), séance 28 (suiveur de ligne proportionnel). Cette séance donne le cadre général et la méthode de réglage.

01 / Boucle fermée

Le banc d’essai : un moteur simulé

Le modèle

Un moteur réel ressemble à ce premier ordre : sa vitesse rejoint la valeur d’équilibre avec une constante de temps τ (séance 21, la charge d’un condensateur suit la même loi). Zone morte, bruit de mesure (encodeur grossier), retard de mesure (60 ms : le temps de compter les ticks et de filtrer) et perturbation (pente à t = 3 s) sont là pour rendre le problème réaliste — c’est le retard qui rend l’oscillation possible (séance 26). Tout ce qui suit s’exécute sur ce banc ; les cellules partagent leurs définitions, exécutez-les dans l’ordre.

02 / P, I, D

P : proportionnel à l’erreur

L’erreur statique

Avec P seul, la commande est nulle quand l’erreur est nulle : or il faut une PWM non nulle pour tenir 20 cm/s. Le système s’équilibre donc avec une erreur (d’autant plus petite que kp est grand) : c’est l’erreur statique. Augmenter kp la réduit mais amplifie le bruit et finit par faire osciller (à cause de l’inertie τ : la commande arrive « en retard »). Et la pente ajoute une erreur permanente que P ne peut pas annuler.

02 / P, I, D

I : intégrale, la mémoire de l’erreur

Ce que fait I

L’intégrale accumule l’erreur : tant qu’il reste une erreur, la commande continue de monter. À l’équilibre, l’erreur est forcément nulle — et l’intégrale a mémorisé la PWM nécessaire pour tenir la consigne, pente comprise. Trop de ki : l’intégrale a « trop d’élan » et dépasse (comme un thermostat qui chauffe trop). C’est le terme qui rend le robot exact ; c’est aussi celui qui pose le plus de problèmes (saturation, plus loin).

02 / P, I, D

D : dérivée, l’anticipation

Ce que fait D

La dérivée mesure la vitesse de variation : si la mesure fonce vers la consigne, D freine avant le dépassement. C’est l’amortisseur. Mais dériver une mesure bruitée amplifie le bruit (la commande devient hachée : mauvais pour les moteurs) : on filtre D, ou on le calcule sur plusieurs échantillons. Beaucoup de robots réels se contentent d’un PI ; D est indispensable pour les systèmes « rapides » (équilibre d’un drone, d’un pendule inversé).

03 / Régler

Méthode manuelle et Ziegler-Nichols

La méthode manuelle, sur le vrai robot
  1. ki = kd = 0. Monter kp jusqu’à une réponse rapide qui oscille un peu, puis réduire de moitié.
  2. Monter ki jusqu’à ce que l’erreur statique disparaisse en ~1 s sans trop de dépassement.
  3. Si ça dépasse ou oscille : un peu de kd, filtré.
  4. Tester avec des perturbations (pousser le robot, pente, batterie faible). Recommencer.

Ziegler-Nichols (1942) donne un point de départ agressif (dépassement de ~25 %) ; on affine ensuite. Chaque réglage est propre à un système : celui du robot A ne va pas au robot B. Notez vos gains dans un fichier de configuration, pas dans le code.

03 / Régler

Le piège majeur : saturation et windup

Windup

Pendant les 2 s de consigne inatteignable, la PWM est saturée à 255 mais l’intégrale continue de grossir (« s’enrouler »). Quand la consigne redescend, il faut des secondes pour la « dérouler » : le robot reste à fond bien après. Correction : ne pas intégrer quand la commande est saturée. C’est le bug PID le plus fréquent — un robot qui « ne s’arrête plus » après avoir été bloqué. Toujours : borner la commande et l’intégrale.

04 / Sur le robot

PID de vitesse par roue (firmware)

# MicroPython — pid.py : un PID réutilisable, avec tout ce qui précède
import time

class PID:
    def __init__(self, kp, ki, kd, u_min=-1.0, u_max=1.0, filtre=0.7):
        self.kp, self.ki, self.kd = kp, ki, kd
        self.u_min, self.u_max, self.filtre = u_min, u_max, filtre
        self.integrale = 0.0; self.prec = None; self.df = 0.0
    def reset(self): self.integrale = 0.0; self.prec = None; self.df = 0.0
    def calculer(self, consigne, mesure, dt):
        e = consigne - mesure
        d = 0.0 if self.prec is None else -(mesure - self.prec) / dt
        self.prec = mesure; self.df = self.filtre * self.df + (1 - self.filtre) * d
        u_brut = self.kp * e + self.ki * self.integrale + self.kd * self.df
        u = max(self.u_min, min(self.u_max, u_brut))
        if u == u_brut or (e > 0) != (u_brut > 0): self.integrale += e * dt
        return u

# Dans la classe Robot (séance 27) : asservir chaque roue en cm/s
class RobotAsservi(Robot):
    def __init__(self):
        super().__init__()
        self.pid_g = PID(0.02, 0.15, 0.0005); self.pid_d = PID(0.02, 0.15, 0.0005)
        self.v_g_cible = self.v_d_cible = 0.0; self.t_prec = time.ticks_ms()
    def vitesses(self, v_g, v_d):                       # cm/s
        self.v_g_cible, self.v_d_cible = v_g, v_d
    def maj(self):
        now = time.ticks_ms(); dt = max(1, time.ticks_diff(now, self.t_prec)) / 1000; self.t_prec = now
        tg = self.enc_g.lire_et_raz() * self.sens_g; td = self.enc_d.lire_et_raz() * self.sens_d
        v_g, v_d = tg * self.CM_PAR_TICK / dt, td * self.CM_PAR_TICK / dt      # mesures (bruitées : 1 cm/tick)
        self.g.vitesse(self.pid_g.calculer(self.v_g_cible, v_g, dt))
        self.d.vitesse(self.pid_d.calculer(self.v_d_cible, v_d, dt))
        self.g.maj(); self.d.maj()
        self._odometrie(tg, td)                          # extrait de Robot.maj
Cadence et résolution

À 50 Hz avec 1 cm par tick, une roue à 20 cm/s donne 0,4 tick par cycle : la mesure de vitesse est très bruitée (0, 1 ou 2 ticks). Solutions : cadence du PID à 10–20 Hz (plus de ticks par cycle), ou mesurer la durée entre ticks avec ticks_us dans l’ISR, ou encodeurs plus fins. Les gains ci-dessus sont des ordres de grandeur pour une commande normalisée −1…1 : à régler avec la méthode manuelle et le tableau de bord (séance 26) qui trace consigne et mesure.

04 / Sur le robot

PID de suivi de ligne, puis cascade

Cascade

Le PID de ligne produit une rotation w ; il suppose que le robot tourne exactement de w. Or les roues ont leur propre dynamique. D’où la cascade : le PID de ligne (lent, 20 Hz) donne des consignes de vitesses v ± wL/2 aux PID de roues (rapides, 50 Hz) qui les réalisent. Chaque boucle est plus simple à régler, et la boucle interne absorbe pente et batterie. On règle toujours l’interne d’abord. Les drones ont trois niveaux : position → vitesse → attitude → vitesse angulaire des moteurs.

05 / Défis

Défi ★ — Régler sur le banc, puis sur le robot

Consigne

1. Sur le banc simulé : trouver, avec la méthode manuelle, un PID qui atteint 20 cm/s en moins de 0,5 s, avec moins de 10 % de dépassement, et qui absorbe la pente en moins de 1 s. Le vérifier avec un « score » automatique : temps de montée, dépassement, erreur RMS après la pente.
2. Sur le robot : implémenter RobotAsservi, tracer consigne/mesure avec le tableau de bord, régler. Pousser le robot avec la main : il doit résister.

Correction (score)
def score(ctrl):
    t, v, u = essai(ctrl)
    montee = t[np.argmax(v > 18)] if (v > 18).any() else np.inf
    depassement = max(0, v[:150].max() - 20) / 20 * 100
    apres_pente = np.sqrt(np.mean((v[t > 4] - 20) ** 2))
    return montee, depassement, apres_pente

for nom, c in (("PI 8/80", PI(8, 80)), ("PID 12/100/0.4", PID_antiwindup(12, 100, 0.4)), ("PID 20/150/0.6", PID_antiwindup(20, 150, 0.6))):
    m, d, a = score(c)
    print(f"{nom:16} montée {m:.2f} s  dépassement {d:4.1f} %  RMS après pente {a:.2f}")

Un score chiffré permet de comparer des réglages sans se fier à l’œil, et même de lancer une recherche automatique (grille ou optimisation) : c’est ce que font les outils d’auto-réglage industriels.

05 / Défis

Défi ★★ — Aller à un point, précisément

Consigne

Le robot doit rejoindre (x, y) donné, en partant de n’importe où, et s’y arrêter à ±2 cm avec le bon cap. Deux PID : un sur la distance (→ v, saturé à v_max, ralentissant à l’approche) et un sur l’erreur de cap vers la cible (→ w). Puis un troisième, une fois arrivé, pour aligner le cap final. Simuler avec le modèle de la séance 14 (odométrie parfaite), puis avec 3 % de bruit d’odométrie. Mesurer l’erreur d’arrivée sur 50 essais.

Direction
def aller_a(cible_x, cible_y, cap_final, dt=0.05):
    pid_d, pid_cap = PIDsim(0.8, 0.0, 0.1, u_max=25), PIDsim(3.0, 0.0, 0.2, u_max=3.0)   # version Python de la classe PID
    x, y, cap = 0.0, 0.0, 0.0; phase = "aller"
    for _ in range(600):
        dx, dy = cible_x - x, cible_y - y; dist = math.hypot(dx, dy)
        if phase == "aller":
            err_cap = (math.atan2(dy, dx) - cap + math.pi) % (2 * math.pi) - math.pi
            v = pid_d.calculer(0, -dist, dt) * max(0.0, math.cos(err_cap))   # ne pas avancer si on pointe à l'opposé
            w = pid_cap.calculer(0, -err_cap, dt)
            if dist < 2: phase = "aligner"
        else:
            err_cap = (cap_final - cap + math.pi) % (2 * math.pi) - math.pi
            v, w = 0.0, pid_cap.calculer(0, -err_cap, dt)
            if abs(err_cap) < math.radians(3): break
        x += v * math.cos(cap) * dt; y += v * math.sin(cap) * dt; cap += w * dt
    return math.hypot(cible_x - x, cible_y - y), math.degrees(abs(cap_final - cap))

Le facteur cos(err_cap) empêche d’avancer tant que le robot ne regarde pas vers la cible : c’est une petite machine à états implicite. Avec du bruit d’odométrie, l’erreur d’arrivée reflète la dérive (séance 27) : ~3–5 cm pour un trajet d’un mètre. Pour faire mieux, il faut un repère externe (balise, ligne au sol, caméra).

05 / Défis

Défi ★★★ — Esprit prépa : le pendule inversé

Consigne

Le pendule inversé (un bâton en équilibre sur un chariot ; un gyropode ; une fusée à l’atterrissage) est instable : sans commande, il tombe. Équations (petits angles) : θ'' ≈ (g/l)·θ − (1/l)·a, où a est l’accélération du chariot que l’on commande.

1. Simuler (Euler, séance 15) avec un PID sur θ : trouver des gains qui le maintiennent debout depuis θ₀ = 10°, avec une accélération bornée à ±10 m/s².
2. Ajouter un bruit de mesure de 0,5° et un retard de mesure de 40 ms. Le PID tient-il ? Quel gain kd minimal ?
3. Montrer par simulation qu’un contrôleur P seul ne peut jamais stabiliser ce système (il oscille ou diverge quel que soit kp), et expliquer pourquoi le D est indispensable ici (indice : l’équation est du second ordre sans amortissement).

Direction

Avec P seul, l’équation devient θ'' = (g/l − kp/l)·θ : si kp < g, ça diverge ; si kp > g, c’est un oscillateur harmonique sans amortissement : ça oscille pour toujours (et le moindre retard fait diverger). Le terme D ajoute l’amortissement : θ'' + (kd/l)θ' + ((kp − g)/l)θ = 0, une équation du second ordre stable dès que kd > 0 et kp > g. En prépa, vous étudierez exactement cette équation (oscillateur amorti) et le critère de stabilité par les racines du polynôme caractéristique — puis, plus tard, la commande optimale (LQR) qui calcule les meilleurs gains. Le retard limite kd : trop de D avec un retard, et l’on corrige une pente périmée. C’est le problème que résout un gyropode 400 fois par seconde.

06 / Vérification

Quel terme du PID annule l’erreur due à une pente constante ?

Deux questions supplémentaires

1. Qu’est-ce que le windup ? L’intégrale qui grossit pendant que la commande est saturée, provoquant un dépassement long. Remède : ne pas intégrer en saturation.

2. Pourquoi filtrer le terme D ? Parce que dériver une mesure bruitée amplifie le bruit et hache la commande.

Référence

Les mots à retenir

MotDéfinition
Consigne / mesure / erreurCe qu’on veut / ce qu’on a / la différence.
PCommande proportionnelle à l’erreur ; laisse une erreur statique.
ISomme des erreurs ; annule l’erreur statique ; risque de windup.
DVitesse de variation ; amortit ; sensible au bruit.
SaturationCommande bornée par le matériel.
Anti-windupStopper l’intégration en saturation.
CascadePID lent qui donne la consigne d’un PID rapide.
Ziegler-NicholsRéglage à partir du gain critique et de la période d’oscillation.

Pour continuer

Le robot est précis

Il roule, perçoit, décide, se régule. Il reste à en faire un projet présentable — et à parler de la suite : le lycée, la prépa, l’X, l’ENS.

À faire chez soi

← Séance 28SommaireSéance 30 : Projet final et orientation →