Tutorial · Einsteiger · 20 Min. Lesezeit

HC-SR04-Ultraschallsensor lesen: Distanz in Code

Wie man einen HC-SR04 an Arduino liest: pulseIn mit Timeout, Echozeit in Zentimeter umrechnen und ein Medianfilter, das die zufälligen Spikes killt.

Ein Roboter, der Hindernissen ausweicht, ist nur so gut wie die Distanz, nach der er lenkt. Diese Zahl kommt von einem HC-SR04-Ultraschallsensor: Er zirpt, lauscht aufs Echo, und die Hin-und-Rück-Zeit sagt dir, wie weit das Nächste weg ist. Diese Anleitung macht aus dem rohen Ping eine Distanz in Zentimetern, der du trauen kannst — eine, die Aussetzer und Fremdechos des Sensors überlebt, statt deinen Roboter in eine Wand zu schicken.

Wie der Sensor Distanz meldet

Der HC-SR04 spricht eine simple Zwei-Pin-Sprache. Du pulst Trigger für 10 Mikrosekunden HIGH; er feuert acht 40-kHz-Bursts und hebt Echo an, bis die Reflexion zurückkommt. Die Zeit, die Echo HIGH bleibt, ist die Hin-und-Rück-Zeit, und Schall läuft etwa 343 m/s, also:

distance_cm = echo_time_us * 0.0343 / 2

Die Division durch zwei, weil der Schall hin und zurück lief. Die Bauteilseite behandelt Verdrahtung und Physik; hier geht es ums saubere Lesen in Code.

Lies ihn mit pulseIn — und setze immer ein Timeout

Das Timeout zählt aus zweitem Grund jenseits Korrektheit: pulseIn blockiert, also lässt ein verpasstes Echo deine ganze Schleife so lange stehen, wie du erlaubst. Siehe nicht-blockierendes Timing, was das einen fahrenden Roboter kostet.

pulseIn misst, wie lange ein Pin HIGH bleibt. Die Falle: Kommt das Echo nie zurück — nichts in Reichweite, oder eine weiche Oberfläche, die den Schall streut — blockiert pulseIn sein volles Standard-Timeout, und dein Regelkreis steht. Übergib immer ein explizites Timeout, bemessen auf deine Maximalreichweite:

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);
}

-1 für Timeout zurückzugeben — statt 0 — zählt. Eine 0 liest sich wie „ein Hindernis berührt den Sensor“, was das Gegenteil von „der Weg ist frei“ ist. Halte „kein Echo“ getrennt von „sehr nah“.

Warum die Rohlesung springt

Richte einen HC-SR04 auf eine Wand, und die Messwerte flackern trotzdem, weil die echte Welt unordentlich ist:

  • Weiche oder schräge Ziele streuen das Echo vom Empfänger weg, also bekommst du gelegentlich riesige Werte oder Aussetzer.
  • Der ~15°-Strahl heißt, der Sensor meldet das Nächste in einem Kegel, also kann ein Tischbein seitlich eine kurze Lesung spiken.
  • Zu schnelles Abfragen (unter ~60 ms Abstand) lässt das Echo des vorigen Pings in die nächste Lesung lecken.

Füttere diese rohen Sprünge direkt in eine Stopp-oder-Dreh-Entscheidung, und der Roboter zuckt. Du musst den Strom glätten, ohne Verzug hinzuzufügen.

Ein Median-aus-5-Filter

Das richtige Werkzeug ist ein Median, kein Mittelwert. Ein Mittelwert verschmiert einen einzelnen wilden Spike über mehrere Lesungen; ein Median wirft ihn schlicht raus. Nimm fünf Pings und nutze den mittleren:

Liniendiagramm von HC-SR04-Distanzlesungen, während sich ein Roboter einer Wand von etwa 58 cm bis auf 15 cm nähert. Das Rohsignal (grau, mit Markern) hat einen Spike bis etwa 85 cm, beschriftet als verworfenes Fremdecho, und zwei Aussetzer nahe null, beschriftet als verworfene Timeouts, während die Median-aus-5-Linie (cyan) durch beides glatt bleibt.
Ein einziger Median-aus-5-Durchlauf verwirft sowohl das ferne Fremdecho als auch die Timeout-Aussetzer und hinterlässt eine glatte Distanz, nach der der Regler handeln kann. SVG herunterladen
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
}

Zwei Details machen das robust: Timeout auf einen großen „fern“-Wert abbilden (sodass ein verlorenes Echo nie als Hindernis liest) und das kleine delay zwischen Pings, damit Echos nicht übersprechen. Zur Theorie hinter Filtern ohne Überglätten siehe Sensorrauschen, Bias und Filterung.

Die Schleife nicht mehr blockieren

Alles oben lässt die Schleife weiter stehen. pulseIn wartet, delay(30) wartet, und fünf Pings mit je 30 ms Abstand sind 150 ms, in denen der Roboter keinen anderen Sensor liest und keine Entscheidung trifft. Bei 0,3 m/s sind das 4,5 cm Blindfahrt pro Messung.

Der Fix ist, den Sensor als kleine Zustandsmaschine zu treiben, die die Hauptschleife durchschreitet, ohne je zu warten:

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;
  }
}

Die Schleife ruft nun updatePing() auf und zieht weiter. lastDistance ist immer die jüngste abgeschlossene Messung, höchstens 60 ms alt, und nichts sonst im Roboter hat darauf gewartet.

Das Echo per Interrupt timen

Die Zustandsmaschine muss noch wissen, wann das Echo ankam, und digitalRead in der Hauptschleife zu pollen kostet Auflösung. Ein Interrupt auf beide Flanken des Echo-Pins kostet nichts:

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);
}

Auf einem Arduino Uno müssen das Pin 2 oder 3 sein — die einzigen zwei externen Interrupt-Pins — und wenn dein Roboter Radencoder hat, wollen die beide. Dieser Konflikt ist echt, und er ist einer der häufigeren Gründe, warum ein Projekt zu Pin-Change-Interrupts eines Nano, einem Mega oder einem ESP32 wechselt.

Beachte die echoStart != 0-Wache. Ohne sie berechnet eine fallende Flanke ohne vorherige steigende — was beim ersten Interrupt nach einem Timeout passiert — eine Unsinnsdauer aus einem alten Zeitstempel.

Einen Filter wählen

Median ist die richtige Voreinstellung, aber es lohnt sich zu wissen, was die Alternativen wirklich tun, denn sie versagen unterschiedlich:

Filter Weist einen einzelnen Spike ab? Verzug Kosten Wann nutzen
Keiner Nein 0 — Nie, an einem fahrenden Roboter
Mittel aus 5 Nein — er verschmiert den Spike über 5 Abtastungen 2 Abtastungen Niedrig Das Rauschen ist gaußförmig, nicht spikig
Median aus 5 Vollständig 2 Abtastungen Sortieren, trivial bei n=5 Ultraschall. Das ist die richtige Antwort
Median aus 3 Ja, ein Spike 1 Abtastung Niedrigste Du brauchst die extra Reaktionsfreude
Exponentiell (EMA) Nein Hängt von α ab Niedrigste Glätten eines schon sauberen Signals
Median dann EMA Ja, plus Glättung 2 Abtastungen + α Niedrig Schnelle Schleifen, die beides brauchen

Ultraschallrauschen ist nicht gaußförmig — es sind gelegentliche wilde Ausreißer gegen ein sonst stabiles Signal, genau der Fall, den ein Mittelwert schlecht und ein Median perfekt behandelt. Ein 400-cm-Spike in fünf 30-cm-Lesungen verschiebt den Mittelwert um 74 cm und den Median um gar nichts.

Temperatur, wenn du die Genauigkeit brauchst

Der 58,2-Teiler nimmt 343 m/s an, das ist die Schallgeschwindigkeit bei 20 °C. Sie ist nicht konstant:

speed (m/s) = 331.3 + 0.606 x temperature_celsius
Temperatur Fehler gegen 20-°C-Annahme Bei 2 m
0 °C Liest 3,5 % zu weit 7 cm
10 °C 1,8 % zu weit 3,5 cm
30 °C 1,7 % zu nah 3,4 cm
40 °C 3,5 % zu nah 7 cm

Für Hindernisvermeidung ignoriere das völlig — 7 cm bei 2 m ändern keine Entscheidung. Für einen Roboter, der eine Lücke vermisst, oder einen, der im Winter in der Garage arbeitet, kompensiere:

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

Wenn der Roboter schon einen MPU-6050 trägt — oder fast jeden I²C-Sensor — hast du eine Temperaturlesung gratis dabei.

Mehrere Sensoren betreiben

Zwei HC-SR04, die gleichzeitig pingen, hören je das Zirpen des anderen und produzieren beidseitig wilde Messwerte. Sie müssen sich abwechseln:

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);
  }
}

Der Preis ist Aktualisierungsrate: Drei Sensoren mit je 60 ms heißt, jeder einzelne Sensor aktualisiert alle 180 ms. Ist das zu langsam — und an einem fahrenden Roboter ist es das oft — lautet die Antwort entweder weniger Ultraschallsensoren oder ein VL53L0X dazu, der Licht nutzt und daher ein Sonar gar nicht stören kann.

Fehlersuche

Symptom Wahrscheinliche Ursache Fix
Roboter friert zufällig ein pulseIn ohne Timeout Timeout übergeben oder nicht-blockierende Zustandsmaschine nutzen
Notstopps im leeren Raum Timeout als 0 cm behandelt Ein Timeout heißt fern. Sentinel zurückgeben und behandeln
Messwerte wechseln wild Schneller als ~60 ms abgefragt Das vorige Echo aussterben lassen
Stabil, aber ein paar Prozent falsch Temperatur Mit obiger Formel kompensieren
Wilde Messwerte mit zwei Sensoren Sie hören einander Reihum, nie gleichzeitig
Auf dem Tisch brav, am Roboter verrauscht Motorlärm auf der 5-V-Schiene Sensorspeisung entkoppeln; Leitungen von Motorleitungen fernhalten
Gelegentlicher Riesenwert überlebt den Filter Mittel statt Median Median nutzen — ein Mittel verschmiert einen Spike, weist ihn nicht ab
ESP32-Pin antwortet nicht mehr 5-V-Echo in einen 3,3-V-GPIO Teiler aufs Echo; prüfen, ob der Pin noch lebt

Wohin das als Nächstes geht

Du hast nun eine saubere Zahl: die Distanz geradeaus. Zwei Dinge bauen darauf. Montiere den Sensor auf ein Servo und schwenke ihn, um zu sehen, welcher Weg offen ist — Ultraschallsensor per Servo scannen — und füttere das Ergebnis in die Entscheidungsschleife in den Hindernisvermeidungs-Algorithmus. Das ganze Fühl-und-Lenk-Verhalten kannst du zuerst im Hindernisvermeidungs-Simulator ausprobieren.

Den Graphen erkunden

Teil dieser Bauprojekte

Projekte und Lernpfade, die dieses Tutorial enthalten.

Weiterführende Literatur

Quellen