Tutorial · Beginner · 20 min
Read an HC-SR04 Ultrasonic Sensor: Reliable Distance in Code
How to read an HC-SR04 on Arduino: pulseIn with a timeout, converting echo time to centimetres, and a median filter that kills the random spikes.
Published
A robot that avoids obstacles is only as good as the distance it steers on. That number comes from an HC-SR04 ultrasonic sensor: it chirps, listens for the echo, and the round-trip time tells you how far away the nearest thing is. This guide turns that raw ping into a distance in centimetres you can trust—one that survives the sensor’s dropouts and stray echoes instead of sending your robot into a wall.
How the sensor reports distance
The HC-SR04 speaks a simple two-pin language. You pulse Trigger HIGH for 10 microseconds; it fires eight 40 kHz bursts and raises Echo until the reflection returns. The time Echo stays HIGH is the round-trip time, and sound travels about 343 m/s, so:
distance_cm = echo_time_us * 0.0343 / 2
The division by two is because the sound went out and came back. The component page covers the wiring and the physics; here we care about reading it cleanly in code.
Read it with pulseIn—and always set a timeout
pulseIn measures how long a pin stays HIGH. The trap: if the echo never returns—nothing in range, or a soft surface that scatters the sound—pulseIn blocks for its full default timeout and your control loop stalls. Always pass an explicit timeout sized to your maximum range:
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);
}
Returning -1 for a timeout—rather than 0—matters. A 0 reads like “an obstacle is touching the sensor,” which is the opposite of “the way is clear.” Keep “no echo” distinct from “very close.”
Why the raw reading jumps
Point an HC-SR04 at a wall and the readings still flicker, because the real world is messy:
- Soft or angled targets scatter the echo away from the receiver, so you get an occasional huge value or a dropout.
- The ~15° beam means the sensor reports the nearest thing in a cone, so a table leg to the side can spike a short reading.
- Polling too fast (under ~60 ms apart) lets the previous ping’s echo leak into the next reading.
Feed those raw jumps straight into a stop-or-turn decision and the robot twitches. You need to smooth the stream without adding lag.
A median-of-5 filter
The right tool is a median, not an average. An average smears a single wild spike across several readings; a median simply throws it out. Take five pings and use the middle one:
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
}
Two details make this robust: mapping a timeout to a large “far” value (so a lost echo never reads as an obstacle), and the small delay between pings so echoes don’t cross-talk. For the theory behind filtering without over-smoothing, see sensor noise, bias, and filtering.
Where this goes next
You now have one clean number: the distance straight ahead. Two things build on it. Mount the sensor on a servo and sweep it to see which way is open—servo-scan an ultrasonic sensor—and feed the result into the decision loop in the obstacle-avoidance algorithm. You can try the whole sense-and-steer behaviour first in the obstacle avoidance simulator.
Explore the graph
Part of these builds
Projects and learning paths that include this tutorial.
Further reading