PYTHON → ROBOTIQUE · 23

Séance 23 · Partie D · Électronique et embarqué

Arduino 2 :
capteurs et actionneurs.

Lire le monde (lumière, distance, position d’un bouton rotatif) et agir dessus (intensité d’une LED, angle d’un servo, note d’un buzzer). Avec un principe qui revient partout : le rapport cyclique.

Durée : 2 séances · Objectifs : analogRead et calibration, map/constrain, PWM et analogWrite, servomoteur, capteur ultrason HC-SR04, buzzer, bibliothèques, interruptions.

Ce que vous saurez faire à la fin
  • Convertir une valeur brute de capteur en grandeur physique, avec calibration.
  • Expliquer la PWM et l’utiliser pour varier une intensité ou une vitesse.
  • Piloter un servo et mesurer une distance par ultrasons.
  • Réagir instantanément à un événement avec une interruption.

Matériel : Arduino, potentiomètre, photorésistance + 10 kΩ, LED + 220 Ω, servo SG90, capteur HC-SR04, buzzer piézo.

01 / Analogique

analogRead : de 0 à 1023

const byte POT = A0;      // curseur du potentiomètre sur A0

void setup() { Serial.begin(115200); }

void loop() {
  int brut = analogRead(POT);              // 0 … 1023, ~100 µs
  float tension = brut * 5.0 / 1023.0;     // 5.0 : force le calcul en float
  int pourcent = map(brut, 0, 1023, 0, 100);
  Serial.print(brut); Serial.print("\t");
  Serial.print(tension, 2); Serial.print(" V\t");
  Serial.print(pourcent); Serial.println(" %");
  delay(100);
}
  • 6 entrées analogiques A0–A5, 10 bits, référence 5 V (séance 21).
  • map(x, a, b, c, d) : règle de trois entière de [a, b] vers [c, d]. Ne borne pas !
  • constrain(x, mini, maxi) : borne.
  • Traceur série (IDE → Outils) : affiche une courbe des valeurs envoyées.

Tournez le potentiomètre, regardez le traceur. Puis remplacez-le par le pont diviseur à photorésistance.

map() est une division entière

map(512, 0, 1023, 0, 5) donne 2, pas 2,5 : tout est calculé en long. Pour une tension, faites le calcul en float vous-même. Et map ne borne pas : map(1100, 0, 1023, 0, 100) renvoie 107. Combinez avec constrain.

01 / Analogique

Calibrer un capteur : du brut à la grandeur

Méthode générale

1. Mesurer le brut dans plusieurs conditions connues (avec un instrument de référence). 2. Tracer, choisir un modèle (linéaire, puissance, exponentiel). 3. Ajuster les coefficients (régression, séance 19). 4. Coder la formule dans la carte. 5. Vérifier sur de nouvelles conditions. Un capteur sans calibration donne des nombres, pas des mesures. Pour un capteur non linéaire sans modèle simple, on interpole dans une table (multiMap).

02 / PWM

Le rapport cyclique : faire de l’analogique avec du numérique

PWM (Pulse Width Modulation) : la broche est à 5 V pendant une fraction de la période, à 0 V le reste. La moyenne vaut 5 V × rapport cyclique.

  • Une LED à 490 Hz semble briller à intensité réduite (l’œil moyenne).
  • Un moteur (inertie) tourne à vitesse réduite.
  • Un filtre RC (séance 21) en fait une vraie tension continue.

Broches PWM sur Uno : 3, 5, 6, 9, 10, 11 (marquées ~).

Pourquoi pas une vraie tension variable ?

Produire 2,5 V en continu depuis 5 V, c’est dissiper 2,5 V × I en chaleur dans un transistor (comme un régulateur linéaire). En PWM, le transistor est soit totalement ouvert soit totalement fermé : presque aucune perte. C’est pour cela que tous les variateurs de moteur, les alimentations à découpage et les amplificateurs classe D utilisent la PWM. Le rapport cyclique est la grandeur de commande en robotique.

02 / PWM

LED en fondu et potentiomètre → intensité

const byte LED = 9;      // broche PWM (~)

void setup() { pinMode(LED, OUTPUT); }

void loop() {
  for (int v = 0; v <= 255; v += 5) {   // montée
    analogWrite(LED, v); delay(20);
  }
  for (int v = 255; v >= 0; v -= 5) {   // descente
    analogWrite(LED, v); delay(20);
  }
}
const byte LED = 9, POT = A0;

void loop() {
  int brut = analogRead(POT);          // 0 … 1023
  int pwm = brut / 4;                  // 0 … 255 (ou map)
  analogWrite(LED, pwm);
}

// Perception : l'œil est logarithmique.
// 128 ne paraît pas « moitié » de 255.
// Correction gamma :
//   pwm = (long)brut * brut / 4092 / 1023 * ...
//   ou une table de 256 valeurs.
Un capteur commande un actionneur

Le deuxième programme est un système en boucle ouverte : entrée → traitement → sortie, sans vérifier le résultat. Ajoutez une photorésistance qui mesure la lumière de la LED et ajuste la PWM pour maintenir une luminosité constante : boucle fermée, et vous avez un régulateur (séance 29). Sur Uno, analogWrite a 8 bits (256 niveaux) ; l’ESP32 offre 16 bits.

02 / PWM

Le servomoteur : un angle commandé par une impulsion

#include <Servo.h>         // bibliothèque incluse dans l'IDE

Servo servo;
const byte POT = A0;

void setup() {
  servo.attach(9);          // signal sur D9
}

void loop() {
  int brut = analogRead(POT);
  int angle = map(brut, 0, 1023, 0, 180);
  servo.write(angle);       // 0 … 180 degrés
  delay(15);                // le servo met ~0.1 s / 60°
}
  Servo SG90 (3 fils) :
    marron/noir → GND
    rouge       → 5 V (alim séparée si > 1 servo)
    orange      → signal (D9)

  Signal : impulsion toutes les 20 ms
    1.0 ms →   0°
    1.5 ms →  90°
    2.0 ms → 180°
  (une PWM à 50 Hz dont seule
   la LARGEUR compte)

À l’intérieur : un moteur, un réducteur, un potentiomètre qui mesure l’angle, et un circuit qui compare avec l’impulsion reçue. Un servo est un asservissement complet, en boîte.

Limites

Un SG90 tire jusqu’à 700 mA en pointe : sur le 5 V de l’Arduino, un seul passe ; au-delà, alimentation séparée avec masse commune. Il ne fait que 0–180° (les servos « à rotation continue » sont en fait des moteurs à vitesse commandée). Il n’y a pas de retour de position vers l’Arduino : on suppose qu’il y est arrivé. Plage utile réelle : souvent 10–170°, à calibrer avec writeMicroseconds().

03 / Capteurs

Le capteur ultrason HC-SR04 : mesurer un temps de vol

const byte TRIG = 7, ECHO = 8;

void setup() {
  pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT);
  Serial.begin(115200);
}

float distance_cm() {
  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH); delayMicroseconds(10);   // impulsion de 10 µs
  digitalWrite(TRIG, LOW);
  unsigned long duree = pulseIn(ECHO, HIGH, 30000);  // µs, timeout 30 ms (≈ 5 m)
  if (duree == 0) return -1;                         // rien reçu
  return duree * 0.0343 / 2;                         // son : 343 m/s = 0.0343 cm/µs ; aller-retour
}

void loop() {
  float d = distance_cm();
  if (d < 0) Serial.println("hors portée");
  else Serial.println(d, 1);
  delay(60);                                         // laisser l'écho s'éteindre
}
Précautions

pulseIn bloque jusqu’à 30 ms : dans un robot, on préfère une interruption sur ECHO (diapositive suivante) ou une bibliothèque non bloquante (NewPing). Une surface molle, inclinée ou étroite ne renvoie pas d’écho : d’où les mesures aberrantes, et le filtre médian de la séance 19. Portée pratique : 2 cm à 3 m, cône de 15°. Ne pas mesurer plus de 15 fois par seconde (échos parasites).

03 / Capteurs

Interruptions : réagir en microsecondes

const byte ENCODEUR = 2;               // D2 et D3 acceptent les interruptions externes sur Uno
volatile unsigned long impulsions = 0; // volatile : modifiée dans l'interruption, lue dans loop()

void compter() {                        // l'ISR : Interrupt Service Routine
  impulsions++;                         // court ! pas de Serial, pas de delay, pas de millis
}

void setup() {
  pinMode(ENCODEUR, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENCODEUR), compter, FALLING);   // à chaque front descendant
  Serial.begin(115200);
}

void loop() {
  static unsigned long prochain = 0;
  if (millis() >= prochain) {
    prochain += 1000;
    noInterrupts();                     // lecture atomique d'une variable 32 bits
    unsigned long n = impulsions; impulsions = 0;
    interrupts();
    Serial.print(n); Serial.println(" impulsions/s");   // avec 20 fentes par tour : tours/s = n / 20
  }
}
Ce qu’est une interruption

Un événement matériel (front sur une broche, timer, octet reçu) suspend loop(), exécute une petite fonction, puis reprend là où on en était. On ne peut pas rater une impulsion de 50 µs d’un encodeur de roue, même si loop() est occupé. Règles : l’ISR doit être très courte ; les variables partagées sont volatile ; pour lire une variable de plus d’un octet, on coupe brièvement les interruptions (sinon on peut lire la moitié haute avant et la moitié basse après une modification). C’est votre première rencontre avec la concurrence au niveau matériel — la séance 20 en Python n’en était que le reflet.

03 / Capteurs

Le buzzer et les bibliothèques

const byte BUZZER = 8;

// tone() génère une onde carrée à la fréquence demandée
void setup() {
  tone(BUZZER, 440, 500);   // la 440 Hz pendant 500 ms
  delay(600);
  int gamme[] = {262, 294, 330, 349, 392, 440, 494, 523};
  for (int f : gamme) {     // for « pour chaque » du C++11
    tone(BUZZER, f, 150); delay(200);
  }
  noTone(BUZZER);
}
void loop() {}

Installer une bibliothèque

Croquis → Inclure une bibliothèque → Gérer les bibliothèques. Chercher, installer, puis #include.

BibliothèquePour
Servoservomoteurs (incluse)
NewPingultrason non bloquant
Adafruit MPU6050gyroscope/accéléromètre I2C
LiquidCrystalécran LCD 16×2
FastLEDrubans de LED adressables
Lire une bibliothèque

Une bibliothèque Arduino est un dossier avec un .h (déclarations : ce qu’on peut appeler) et un .cpp (le code). Ouvrez Servo.h dans le dossier des bibliothèques : vous y verrez une classe C++ (séance 16 !) avec attach, write. Lire le code des bibliothèques qu’on utilise est la meilleure façon de progresser en C++. tone() utilise un timer matériel et la broche 3/11 devient indisponible en PWM pendant ce temps : les ressources sont comptées.

04 / Défis

Défi ★ — Thermomètre et veilleuse

Consigne

1. Veilleuse : la LED s’allume d’autant plus que la pièce est sombre (photorésistance → PWM inversée), avec un seuil en dessous duquel elle reste éteinte. Lisser la mesure avec une moyenne glissante sur 8 valeurs (tableau circulaire).
2. Radar : monter le HC-SR04 sur le servo ; balayer de 0 à 180° par pas de 10°, mesurer à chaque pas, envoyer « angle,distance » sur le port série. Sur PC, tracer le résultat en polaire avec matplotlib (séance 19, diapositive lidar).

Correction (moyenne glissante en C)
const byte N = 8;
int tampon[N]; byte indice = 0; long somme = 0;

int lisser(int valeur) {
  somme -= tampon[indice];          // retirer la plus ancienne
  tampon[indice] = valeur;
  somme += valeur;
  indice = (indice + 1) % N;        // tableau circulaire
  return somme / N;
}

void loop() {
  int lumiere = lisser(analogRead(A0));            // 0 sombre … 1023 clair
  int pwm = lumiere > 700 ? 0 : map(lumiere, 0, 700, 255, 0);
  analogWrite(9, constrain(pwm, 0, 255));
  delay(20);
}

Le tableau circulaire est la version C du deque(maxlen=N) : O(1) par mesure, mémoire fixe (16 octets). Notez la somme entretenue plutôt que recalculée.

04 / Défis

Défi ★★ — Régulateur de luminosité en boucle fermée

Consigne

Placer la photorésistance face à la LED (PWM). Objectif : maintenir la lecture de la photorésistance à une consigne fixée par le potentiomètre, quelle que soit la lumière ambiante (mettez la main devant, éclairez avec le téléphone).

Contrôleur proportionnel : pwm += Kp × (consigne − mesure) à chaque cycle de 20 ms, borné 0–255. Trouver un Kp qui converge sans osciller. Envoyer consigne, mesure et pwm sur le traceur série pour voir la régulation. Que se passe-t-il si Kp est trop grand ? Trop petit ?

Correction
const byte LED = 9, LDR = A0, POT = A1;
float pwm = 0, Kp = 0.05;

void loop() {
  static unsigned long prochain = 0;
  if (millis() < prochain) return;
  prochain += 20;
  int consigne = analogRead(POT);
  int mesure = analogRead(LDR);
  pwm += Kp * (consigne - mesure);
  pwm = constrain(pwm, 0, 255);
  analogWrite(LED, (int)pwm);
  Serial.print(consigne); Serial.print(","); Serial.print(mesure); Serial.print(","); Serial.println(pwm);
}

C’est un régulateur intégral pur (la commande est la somme des erreurs) : il annule l’erreur statique mais oscille si Kp est grand, traîne s’il est petit. Vous venez de fermer votre première boucle sur du vrai matériel ; la séance 29 en fait la théorie. Le return en tête de loop() est une façon compacte de faire une cadence fixe.

04 / Défis

Défi ★★★ — Esprit prépa : mesure non bloquante et précision

Consigne

Réécrire la mesure ultrason sans pulseIn : déclencher l’impulsion, puis mesurer la durée de l’écho par une interruption CHANGE sur la broche ECHO avec micros(). loop() ne doit jamais attendre.

Puis étudier la précision : 100 mesures d’une distance fixe (un livre à 30 cm). Calculer moyenne et écart-type sur la carte (en une passe, formule de Welford ou Σx et Σx²). L’écart-type diminue-t-il avec un filtre médian de 5 ? Quelle est l’erreur systématique (moyenne − vraie distance) et d’où vient-elle (température, décalage du capteur) ? Proposer une calibration à deux points.

Correction et ouverture
const byte TRIG = 7, ECHO = 2;                    // ECHO sur une broche d'interruption
volatile unsigned long t_debut = 0, duree = 0;
volatile bool pret = false;

void echo_isr() {
  if (digitalRead(ECHO) == HIGH) t_debut = micros();
  else { duree = micros() - t_debut; pret = true; }
}

void declencher() {
  digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW);
}

void setup() {
  pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT);
  attachInterrupt(digitalPinToInterrupt(ECHO), echo_isr, CHANGE);
  Serial.begin(115200);
}

// Statistiques en une passe (Welford)
long n = 0; float moyenne = 0, m2 = 0;
void ajouter(float x) { n++; float d = x - moyenne; moyenne += d / n; m2 += d * (x - moyenne); }

void loop() {
  static unsigned long prochain = 0;
  if (millis() >= prochain) { prochain += 60; declencher(); }
  if (pret) {
    noInterrupts(); unsigned long d = duree; pret = false; interrupts();
    float cm = d * 0.01715;
    ajouter(cm);
    if (n % 100 == 0) {
      Serial.print("moyenne "); Serial.print(moyenne, 2);
      Serial.print(" cm  ecart-type "); Serial.println(sqrt(m2 / (n - 1)), 3);
    }
  }
  // loop() reste libre pour tout le reste
}

Écart-type typique : 0,1 à 0,3 cm sur une surface dure. Erreur systématique : souvent +0,5 à 1 cm (décalage du capteur, température) → calibration à deux points : mesurer à 10 cm et 50 cm, ajuster cm = a × brut + b. La formule de Welford calcule la variance sans stocker les mesures et sans erreur d’annulation (Σx² − (Σx)² est numériquement catastrophique en float 32 bits) : c’est une question d’analyse numérique de prépa. Ouverture : la fusion de ce capteur avec un infrarouge (Sharp) qui a d’autres défauts — filtre de Kalman.

05 / Vérification

Que fait analogWrite(9, 128) ?

Deux questions supplémentaires

1. Pourquoi une variable modifiée dans une interruption doit-elle être volatile ? Pour que le compilateur la relise en mémoire à chaque accès au lieu de garder une copie dans un registre.

2. Un servo est-il commandé par la tension ? Non, par la largeur d’une impulsion (1 à 2 ms toutes les 20 ms).

Référence

Les mots à retenir

MotDéfinition
analogReadTension → entier 0–1023 (10 bits).
CalibrationRelation mesurée entre valeur brute et grandeur physique.
PWMSignal carré dont la moyenne dépend du rapport cyclique.
analogWritePWM 0–255 sur les broches ~.
ServoActionneur en position, commandé par largeur d’impulsion.
Temps de volDistance = vitesse × durée / 2 (ultrason, lidar).
Interruption (ISR)Fonction courte exécutée immédiatement sur un événement.
volatileVariable partagée avec une ISR.

Pour continuer

Vous lisez et vous agissez

Séance suivante : la même chose en MicroPython sur Raspberry Pi Pico et ESP32 — plus confortable, plus puissant, et connecté.

À faire chez soi

Ressources

Arduino : sortie analogique (PWM) · attachInterrupt

← Séance 22SommaireSéance 24 : MicroPython 1 →