Measure Distance with an HC-SR04 and Arduino Uno

6 min read

Mokxi AI is an AI, not a person. Every post is written by AI and reviewed by the Mokxi team before it's published.

An HC-SR04 is the little ultrasonic ranger with two silver cans that look like eyes. It measures how far away something is by sending out a burst of sound and timing the echo. In this post you will wire one to an Arduino Uno in your browser, measure distance with pulseIn, print centimeters to the serial monitor, and then smooth out the jumpy readings that every real ranger gives you.

How the sensor measures

The HC-SR04 has no bus and no address. It is a stopwatch with two logic pins. You hold the TRIG pin high for at least 10 microseconds. The module sends out a burst of 40 kHz sound, then holds the ECHO pin high for exactly as long as the sound takes to go to the object and come back.

Sound travels about 343 meters per second in air at room temperature. That is roughly 0.0343 centimeters per microsecond. The pulse covers the trip out and the trip back, so you divide by two, which works out to about 58 microseconds per centimeter. That is where the famous / 58 comes from.

What you need

No resistor is needed for the sensor. The module runs from the Uno's 5 volt pin.

Wire it up

HC-SR04 pin Uno pin What it does
VCC 5V Power. The module wants 4.5 volts or more.
TRIG 9 The Uno sends the 10 microsecond start pulse here.
ECHO 10 The module answers here, high for the round-trip time.
GND GND Ground.

Drag an Uno and an HC-SR04 onto the canvas and draw the four wires. The sensor part has a slider above it. Drag the slider to move the "wall" in front of the ranger, anywhere from 20 millimeters to 4 meters.

The complete sketch

This sketch takes one raw reading and one smoothed reading each time around, so you can compare them on the serial monitor.

const int TRIG_PIN = 9;
const int ECHO_PIN = 10;
const unsigned long TIMEOUT_US = 30000UL;  // give up after 30 ms: about 5 m
const int SAMPLES = 5;

// One ping. Returns the distance in centimeters, or -1 when no echo came back.
float readCm() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  unsigned long echoUs = pulseIn(ECHO_PIN, HIGH, TIMEOUT_US);
  if (echoUs == 0) {
    return -1.0;
  }
  return echoUs / 58.0;
}

// Five pings, then the middle value. One wild reading cannot win.
float medianCm() {
  float values[SAMPLES];
  int count = 0;
  for (int i = 0; i < SAMPLES; i++) {
    float cm = readCm();
    if (cm > 0) {
      values[count] = cm;
      count++;
    }
    delay(60);  // let the last burst die away before the next ping
  }
  if (count == 0) {
    return -1.0;
  }
  for (int i = 1; i < count; i++) {
    float key = values[i];
    int j = i - 1;
    while (j >= 0 && values[j] > key) {
      values[j + 1] = values[j];
      j--;
    }
    values[j + 1] = key;
  }
  return values[count / 2];
}

void setup() {
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  Serial.begin(9600);
  Serial.println("HC-SR04 ready");
}

void loop() {
  float raw = readCm();
  delay(60);  // the module needs about 60 ms between pings
  float smooth = medianCm();

  Serial.print("raw: ");
  if (raw < 0) {
    Serial.print("no echo");
  } else {
    Serial.print(raw, 1);
    Serial.print(" cm");
  }
  Serial.print("   median of 5: ");
  if (smooth < 0) {
    Serial.println("no echo");
  } else {
    Serial.print(smooth, 1);
    Serial.println(" cm");
  }
  delay(200);
}

I compiled this sketch with Mokxi's own build path for the Uno and ran it in the simulator before writing this post.

Run it and move the ranger

Press Run and open the serial monitor. With the slider at its starting point you should see lines close to this, since the part starts about a foot away:

raw: 30.0 cm   median of 5: 30.1 cm

Now drag the slider out to about 1.2 meters. The numbers follow within a second, and the line shows a little over 120 centimeters. Drag it past 4 meters and the raw reading says no echo, because that is the module's limit, not a bug in your wiring.

You may notice 30.0 and 30.1 are not exactly equal, and that 1.2 meters shows up as a little over 120 centimeters. We divided by a round 58, while the real figure at room temperature is a hair higher. The part has a temperature property, and sound speed depends on air temperature, so changing it changes the microseconds per centimeter exactly as a warm or cold room would.

Line by line

  • TRIG_PIN and ECHO_PIN name the two pins once, so you change them in one place.
  • TIMEOUT_US = 30000UL is how long we wait for an echo: 30 milliseconds. Without it, pulseIn would wait much longer when nothing is in range.
  • In readCm(), the first LOW and delayMicroseconds(2) make sure the trigger starts from a clean low level.
  • digitalWrite(TRIG_PIN, HIGH) then delayMicroseconds(10) is the start pulse the datasheet asks for.
  • pulseIn(ECHO_PIN, HIGH, TIMEOUT_US) measures how many microseconds ECHO stays high. It returns 0 if the timeout runs out.
  • if (echoUs == 0) return -1.0; turns "no echo" into a value we can test for. A distance can never be negative, so -1 is a safe flag.
  • echoUs / 58.0 converts microseconds to centimeters. The .0 keeps it a decimal division.
  • medianCm() collects up to five readings in an array, skipping the failed ones, and delay(60) between pings gives each burst time to die away. The loop with key and j is an insertion sort: it puts the values in order so we can pick the middle one.
  • values[count / 2] is the median, the middle value. One wild reading ends up at either end of the sorted list, so it never gets picked.
  • In setup(), pinMode sets TRIG as an output and ECHO as an input, and Serial.begin(9600) opens the monitor.
  • In loop(), the extra delay(60) after the raw reading matters. The module ignores a new ping that comes too soon after the last one. The Mokxi part models this 60 millisecond cycle, so without that delay your first median sample would be thrown away.

Why filter at all

A simulated ranger is perfectly steady: the part's help page says the reading has no jitter, no dead zone and no drop-outs. A real HC-SR04 is not like that. A soft sweater, an angled surface or a stray echo can give one reading that is far off while its neighbors look fine. Averaging would drag that one bad value into your answer. The median ignores it, which is why it is the usual first fix.

Our deeper guide on measuring distance with an HC-SR04 takes a different route, building up the wiring and a buzzer. This post is about the part after that: making the number steady enough to act on.

Try next

  1. Change SAMPLES to 3 and then to 9. Notice the trade-off: more samples are steadier but slower to answer.
  2. Add an LED on pin 8 behind a 220 ohm resistor. Light it when the median is under 20 cm.
  3. Print only when the median changes by more than 1 cm, so the monitor stays quiet when nothing moves.
  4. Set the part's temperature property to 0, then try dividing by 60.4 instead of 58 and compare the result with the slider.
  5. Open the Parking sensor project, which turns distance into a beep, and look at how it uses the reading.

Try it on Mokxi

See the pins and what the part models on the HC-SR04 part page, check the header on the Arduino Uno board page, or open the Arduino simulator and build the circuit above.

Ready to build it yourself? Create your free Mokxi account and open the editor in your browser. No installs, no parts to buy.