Tutoriel · Débutant · 20 min de lecture

Lire un capteur ultrasons HC-SR04 : une distance fiable en code

Comment lire un HC-SR04 sur Arduino : pulseIn avec un délai maximal, convertir le temps d'écho en centimètres, et un filtre médian qui tue les pics aléatoires.

Un robot qui évite les obstacles n’est bon que comme la distance sur laquelle il braque. Ce nombre vient d’un capteur ultrasons HC-SR04 : il émet, écoute l’écho, et le temps d’aller-retour dit à quelle distance est la chose la plus proche. Ce guide transforme ce ping brut en une distance en centimètres à laquelle faire confiance — une qui survit aux pertes et échos parasites du capteur au lieu d’envoyer votre robot dans un mur.

Comment le capteur rapporte la distance

Le HC-SR04 parle un langage simple à deux broches. Vous pulsez Trigger au niveau haut 10 microsecondes ; il tire huit salves à 40 kHz et lève Echo jusqu’au retour de la réflexion. Le temps où Echo reste haut est le temps d’aller-retour, et le son voyage à environ 343 m/s, donc :

distance_cm = echo_time_us * 0.0343 / 2

La division par deux est parce que le son est allé et revenu. La page du composant couvre le câblage et la physique ; ici on s’occupe de le lire proprement en code.

Lisez-le avec pulseIn — et réglez toujours un délai maximal

Le délai maximal compte pour une seconde raison au-delà de la justesse : pulseIn bloque, donc un écho manqué cale toute votre boucle aussi longtemps que permis. Voir la temporisation non bloquante pour ce que cela coûte à un robot en mouvement.

pulseIn mesure combien de temps une broche reste haute. Le piège : si l’écho ne revient jamais — rien à portée, ou une surface molle qui disperse le son — pulseIn bloque pendant tout son délai maximal par défaut et votre boucle de commande cale. Passez toujours un délai maximal explicite dimensionné à votre portée maximale :

const int TRIG = 9, ECHO = 10;
const unsigned long TIMEOUT_US = 25000UL;  // ~4 m of round trip

long pingCm() {
  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
  digitalWrite(TRIG, LOW);

  unsigned long us = pulseIn(ECHO, HIGH, TIMEOUT_US);
  if (us == 0) return -1;             // timed out: nothing in range
  return (long)(us * 0.0343 / 2.0);
}

Renvoyer -1 pour un délai — plutôt que 0 — compte. Un 0 se lit comme « un obstacle touche le capteur », ce qui est l’inverse de « la voie est libre ». Gardez « pas d’écho » distinct de « très proche ».

Pourquoi la lecture brute saute

Pointez un HC-SR04 vers un mur et les lectures papillotent quand même, parce que le monde réel est sale :

  • Les cibles molles ou inclinées dispersent l’écho loin du récepteur, donc vous obtenez une valeur énorme occasionnelle ou une perte.
  • Le faisceau d’environ 15° signifie que le capteur rapporte la chose la plus proche dans un cône, donc un pied de table sur le côté peut piquer une lecture courte.
  • Sonder trop vite (moins de ~60 ms d’écart) laisse l’écho du ping précédent fuir dans la lecture suivante.

Injectez ces sauts bruts droit dans une décision arrêt-ou-virage et le robot tressaute. Il faut lisser le flux sans ajouter de retard.

Un filtre médian-sur-5

Le bon outil est une médiane, pas une moyenne. Une moyenne étale un pic sauvage unique sur plusieurs lectures ; une médiane le jette simplement. Prenez cinq pings et utilisez celui du milieu :

Graphique linéaire des mesures de distance HC-SR04 pendant qu'un robot s'approche d'un mur d'environ 58 cm à 15 cm. Le signal brut (gris, avec marqueurs) a un pic jusqu'à environ 85 cm légendé 'écho parasite rejeté' et deux pertes près de zéro légendées 'délai rejeté', tandis que la ligne médiane-sur-5 (cyan) reste lisse à travers les deux.
Un seul passage médian-sur-5 rejette à la fois le lointain écho parasite et les pertes de délai, laissant une distance lisse sur laquelle le contrôleur peut agir. Télécharger le SVG
int cmpLong(const void* a, const void* b) {
  return (*(long*)a) - (*(long*)b);
}

long medianCm() {
  long s[5];
  for (int i = 0; i < 5; i++) {
    long v = pingCm();
    s[i] = (v < 0) ? 400 : v;   // treat timeout as "far", not "touching"
    delay(30);                  // let the previous echo die away
  }
  qsort(s, 5, sizeof(long), cmpLong);
  return s[2];                  // the middle value
}

Deux détails rendent cela robuste : projeter un délai vers une grande valeur « loin » (pour qu’un écho perdu ne se lise jamais comme un obstacle), et le petit delay entre pings pour que les échos ne se mélangent pas. Pour la théorie du filtrage sans surlissage, voir bruit, biais et filtrage des capteurs.

Cessez de bloquer la boucle

Tout ce qui précède cale encore la boucle. pulseIn attend, delay(30) attend, et cinq pings à 30 ms d’écart font 150 ms pendant lesquelles le robot ne lit aucun autre capteur et ne prend aucune décision. À 0,3 m/s cela fait 4,5 cm de trajet aveugle par mesure.

Le correctif est de piloter le capteur comme une petite machine à états que la boucle principale fait avancer sans jamais attendre :

enum PingState { IDLE, TRIGGERED, WAITING };

PingState state = IDLE;
unsigned long stateSince = 0;
long lastDistance = -1;

const unsigned long PING_PERIOD_MS = 60;   // let the previous echo die away
const unsigned long ECHO_TIMEOUT_MS = 30;  // beyond the sensor's range

void updatePing() {
  const unsigned long now = millis();

  switch (state) {
    case IDLE:
      if (now - stateSince < PING_PERIOD_MS) return;
      digitalWrite(TRIG, HIGH);
      stateSince = micros();          // note: microseconds for this short step
      state = TRIGGERED;
      break;

    case TRIGGERED:
      if (micros() - stateSince < 10) return;
      digitalWrite(TRIG, LOW);
      stateSince = now;
      state = WAITING;
      break;

    case WAITING:
      if (echoComplete) {             // set by the interrupt below
        lastDistance = echoMicros / 58.2;
        echoComplete = false;
        stateSince = now;
        state = IDLE;
      } else if (now - stateSince > ECHO_TIMEOUT_MS) {
        lastDistance = -1;            // no echo — far, or invisible
        stateSince = now;
        state = IDLE;
      }
      break;
  }
}

La boucle appelle désormais updatePing() et passe. lastDistance est toujours la mesure terminée la plus récente, vieille d’au plus 60 ms, et rien d’autre dans le robot ne l’a attendue.

Temporiser l’écho avec une interruption

La machine à états doit encore savoir quand l’écho est arrivé, et scruter digitalRead dans la boucle principale coûte en résolution. Une interruption sur les deux fronts de la broche Echo ne coûte rien :

volatile unsigned long echoStart = 0;
volatile unsigned long echoMicros = 0;
volatile bool echoComplete = false;

void echoISR() {
  if (digitalRead(ECHO) == HIGH) {
    echoStart = micros();
  } else if (echoStart != 0) {
    echoMicros = micros() - echoStart;
    echoStart = 0;
    echoComplete = true;
  }
}

void setup() {
  pinMode(TRIG, OUTPUT);
  pinMode(ECHO, INPUT);
  attachInterrupt(digitalPinToInterrupt(ECHO), echoISR, CHANGE);
}

Sur un Arduino Uno cela doit être la broche 2 ou 3 — les deux seules broches d’interruption externe — et si votre robot a des encodeurs de roues, ils veulent les deux. Ce conflit est réel, et c’est l’une des raisons les plus courantes pour passer aux interruptions sur changement d’état d’un Nano, à un Mega, ou à un ESP32.

Notez la garde echoStart != 0. Sans elle, un front descendant arrivant sans front montant précédent — ce qui arrive sur la première interruption après un délai — calcule une durée absurde depuis un horodatage périmé.

Choisir un filtre

La médiane est le bon défaut, mais il vaut de savoir ce que les alternatives font vraiment, parce qu’elles échouent différemment :

Filtre Rejette un pic unique ? Retard Coût Quand l’utiliser
Aucun Non 0 — Jamais, sur un robot en mouvement
Moyenne sur 5 Non — elle étale le pic sur 5 échantillons 2 échantillons Bas Le bruit est gaussien, pas à pics
Médiane sur 5 Complètement 2 échantillons Tri, trivial à n=5 Ultrasons. C’est la bonne réponse
Médiane sur 3 Oui, un pic 1 échantillon Le plus bas Quand il faut la réactivité en plus
Exponentiel (EMA) Non Dépend de α Le plus bas Lisser un signal déjà propre
Médiane puis EMA Oui, plus lissage 2 échantillons + α Bas Boucles rapides qui veulent les deux

Le bruit ultrasonore n’est pas gaussien — ce sont des valeurs aberrantes sauvages occasionnelles contre un signal sinon stable, précisément le cas qu’une moyenne traite mal et une médiane parfaitement. Un pic à 400 cm dans un jeu de lectures à 30 cm déplace la moyenne de 74 cm et la médiane de rien du tout.

La température, s’il faut la précision

Le diviseur 58,2 suppose 343 m/s, la vitesse du son à 20 °C. Elle n’est pas constante :

speed (m/s) = 331.3 + 0.606 x temperature_celsius
Température Erreur contre hypothèse 20 °C À 2 m
0 °C Lit 3,5 % trop loin 7 cm
10 °C 1,8 % trop loin 3.5 cm
30 °C 1,7 % trop près 3.4 cm
40 °C 3,5 % trop près 7 cm

Pour l’évitement d’obstacles, ignorez cela entièrement — 7 cm à 2 m ne change aucune décision. Pour un robot qui mesure un passage, ou qui travaille dans un garage en hiver, compensez :

float cmPerMicrosecond(float tempC) {
  return (331.3f + 0.606f * tempC) / 20000.0f;   // /1e6 for us, /2 for round trip
}

Si le robot porte déjà un MPU-6050 — ou presque tout capteur I²C — vous avez une mesure de température disponible gratuitement.

Faire tourner plusieurs capteurs

Deux HC-SR04 qui pingent simultanément entendront chacun le chirp de l’autre, produisant des lectures folles des deux côtés. Ils doivent jouer à tour de rôle :

const int SENSORS = 3;
int current = 0;
long distance[SENSORS];

void updateAll() {
  if (pingComplete(current)) {
    distance[current] = lastDistance;
    current = (current + 1) % SENSORS;    // round-robin
    startPing(current);
  }
}

Le coût est le taux de rafraîchissement : trois capteurs à 60 ms chacun signifient que chaque capteur s’actualise toutes les 180 ms. Si c’est trop lent — et sur un robot en mouvement cela l’est souvent — la réponse est soit moins de capteurs ultrasons, soit un VL53L0X en renfort, qui utilise la lumière et ne peut donc pas interférer avec un sonar du tout.

Dépannage

Symptôme Cause probable Correctif
Le robot se fige au hasard pulseIn sans délai maximal Passez un délai, ou utilisez la machine à états non bloquante
Arrêts d’urgence dans une pièce vide Traiter un délai comme 0 cm Un délai signifie loin. Renvoyez une sentinelle et traitez-la
Les lectures alternent follement Sondage plus vite que ~60 ms Laissez mourir l’écho précédent
Stables mais fausses de quelques pour cent Température Compensez avec la formule ci-dessus
Lectures folles avec deux capteurs Ils s’entendent Tour à tour, jamais simultanés
Bien sur l’établi, bruyant sur le robot Bruit des moteurs sur le rail 5 V Découplez l’alimentation du capteur ; éloignez ses fils des fils moteur
Une énorme valeur occasionnelle survit au filtre Moyenne au lieu de médiane Utilisez la médiane — une moyenne étale un pic, elle ne le rejette pas
Une broche ESP32 ne répond plus Echo 5 V dans un GPIO 3,3 V Diviseur sur Echo ; vérifiez que la broche marche encore

Où cela va ensuite

Vous avez désormais un nombre propre : la distance droit devant. Deux choses se bâtissent dessus. Montez le capteur sur un servo et balayez-le pour voir quel côté est libre — balayez un capteur ultrasons au servo — et injectez le résultat dans la boucle de décision dans l’algorithme d’évitement d’obstacles. Vous pouvez essayer tout le comportement détecter-et-braquer d’abord dans le simulateur d’évitement d’obstacles.

Explorer le graphe

Fait partie de ces réalisations

Les projets et parcours d'apprentissage qui incluent ce tutoriel.

Pour aller plus loin

Références