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:
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