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èque | Pour |
|---|---|
| Servo | servomoteurs (incluse) |
| NewPing | ultrason non bloquant |
| Adafruit MPU6050 | gyroscope/accéléromètre I2C |
| LiquidCrystal | écran LCD 16×2 |
| FastLED | rubans 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
| Mot | Définition |
|---|---|
analogRead | Tension → entier 0–1023 (10 bits). |
| Calibration | Relation mesurée entre valeur brute et grandeur physique. |
| PWM | Signal carré dont la moyenne dépend du rapport cyclique. |
analogWrite | PWM 0–255 sur les broches ~. |
| Servo | Actionneur en position, commandé par largeur d’impulsion. |
| Temps de vol | Distance = vitesse × durée / 2 (ultrason, lidar). |
| Interruption (ISR) | Fonction courte exécutée immédiatement sur un événement. |
volatile | Variable 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
- Réaliser le radar (défi ★) et le tracer en polaire sur PC.
- Écrire une classe C++
Tache(période, fonction) pour remplacer lesstatic unsigned long prochain. - Chercher la fiche technique du HC-SR04 et vérifier la formule de conversion.