HC-SR04 Ultrasonic Sensor: Wiring, Code and Real Accuracy

Cover image for the complete HC-SR04 ultrasonic sensor guide from eARgle Innovation Labs.

The HC-SR04 is the first sensor almost every embedded project uses, and the first one almost every project gets wrong. It is not a rangefinder — it is a stopwatch attached to a loudspeaker. Once you treat it that way, the accuracy problems, the stuck readings and the phantom 0 cm values all have obvious fixes.

This guide covers the working wiring for both 5 V and 3.3 V boards, the timing maths behind pulseIn(), temperature compensation, three levels of code (blocking, non-blocking, filtered), the beam geometry that decides what the sensor can and cannot see, and a troubleshooting table built from the failures we see most often in the lab.

  • Range 2–400 cm
  • Accuracy ±3 mm (ideal)
  • Beam ~15° cone
  • Supply 5 V, ~15 mA
  • Interface Trig / Echo
  • Rate ≤ 25 Hz practical
HC-SR04 ultrasonic sensor module showing the 40 kHz transmitter and receiver transducers, the control PCB and the four-pin header.
The two cans are the 40 kHz transmitter (T) and receiver (R); everything on the PCB between them exists to time the gap between them. Photo: Suyash Dwivedi, CC BY-SA 4.0, via Wikimedia Commons.

How the HC-SR04 actually measures distance

The module holds a 40 kHz transmitter (marked T) and a receiver (marked R). The sequence never changes:

  1. You raise Trig for 10 µsAnything shorter and the module may ignore it; anything much longer is harmless but wasted time.
  2. The module fires an eight-cycle 40 kHz burstThat burst leaves the transmitter as a cone of sound, not a laser line — this is the single most important physical fact about the sensor.
  3. Echo goes HIGHIt stays HIGH until the receiver detects a returning burst, or until the module gives up (~38 ms).
  4. You measure how long Echo stayed HIGHThat pulse width, in microseconds, is the round-trip flight time of the sound.

So the distance is not measured — it is calculated from time. At 20 °C, sound travels at roughly 343 m/s, or 0.0343 cm/µs. The pulse covers the distance twice (out and back), which gives the formula every Arduino tutorial repeats without explaining:

distance_cm = pulse_us × 0.0343 / 2
            = pulse_us / 58.3          // the "magic" 58 in most sketches
Why this matters

Because the sensor reports time, every error source is a timing error: a busy CPU that misses the edge, a burst that reflects off a closer object than the one you meant to measure, or air that is 15 °C warmer than the constant baked into your formula. Debug timing, not “the sensor”.

Wiring to Arduino, ESP32 and ESP8266

The HC-SR04 is a 5 V part. It needs 5 V on VCC to fire a burst with usable energy, and its Echo pin drives out at 5 V logic. That is fine on an Arduino Uno and not fine on an ESP32 or ESP8266, whose GPIO are 3.3 V.

BoardVCCTrigEchoLevel shifting
Arduino Uno / Nano / Mega5 Vany digital pinany digital pinNone needed
ESP32 / ESP32-C3 / ESP32-S35 V (VIN / 5 V pin)any output-capable GPIOvia dividerRequired — 1 kΩ + 2 kΩ divider, or a level shifter
ESP8266 (NodeMCU, Wemos D1)5 V (VIN)e.g. D5via divider, e.g. D6Required — same divider
Arduino Uno R4 (5 V logic)5 Vany digital pinany digital pinNone needed

The divider on Echo is two resistors: 1 kΩ from Echo to the GPIO, and 2 kΩ from that GPIO node to GND. That drops 5 V to about 3.3 V. It costs two components and saves the board.

The mistake that kills ESP boards

Powering the HC-SR04 from the ESP32’s 3V3 pin “because it seemed to work” is the second most common failure. At 3.3 V the burst is weak, maximum range collapses to well under a metre, and readings become distance-dependent nonsense. Power it from 5 V and divide the Echo line.

An HC-SR04 ultrasonic sensor wired to an Arduino Uno on a breadboard with jumper wires to the Trig and Echo pins.
On a 5 V Arduino this wiring is complete as shown. On an ESP32 or ESP8266 the Echo line needs the divider described above. Photo: Pradana Aumars, CC BY-SA 4.0, via Wikimedia Commons.

Code: blocking, non-blocking and filtered

Level 1 — the standard blocking read

Correct, readable, and fine for a first project. Note the timeout: without it, pulseIn() blocks for a full second when no echo returns.

Arduino / ESP32 · blocking

const uint8_t TRIG = 5;
const uint8_t ECHO = 18;
const unsigned long TIMEOUT_US = 30000UL;   // ~5 m of round trip

void setup() {
  Serial.begin(115200);
  pinMode(TRIG, OUTPUT);
  pinMode(ECHO, INPUT);
  digitalWrite(TRIG, LOW);
}

// returns distance in cm, or -1.0 on timeout
float readDistanceCm() {
  digitalWrite(TRIG, LOW);
  delayMicroseconds(4);
  digitalWrite(TRIG, HIGH);
  delayMicroseconds(10);            // the datasheet's 10 us trigger
  digitalWrite(TRIG, LOW);

  unsigned long us = pulseIn(ECHO, HIGH, TIMEOUT_US);
  if (us == 0) return -1.0;         // nothing came back
  return us / 58.3;                 // cm at ~20 C
}

void loop() {
  float d = readDistanceCm();
  if (d < 0)        Serial.println("out of range");
  else              Serial.printf("%.1f cm\n", d);
  delay(60);                        // >= 60 ms between pings: let echoes die
}
The 60 ms rule

The datasheet asks for at least 60 ms between measurements. Ping faster and the previous burst is still bouncing around the room when you start listening — you get readings that are stable, plausible and wrong. 60 ms caps you at about 16 pings per second, which is plenty for a robot or a parking sensor.

Level 2 — non-blocking with interrupts

pulseIn() stalls your whole program for up to the timeout. On an ESP32 running Wi-Fi, or any robot that must keep its motors alive, measure the echo with an interrupt instead.

ESP32 · interrupt-driven, no blocking

const uint8_t TRIG = 5;
const uint8_t ECHO = 18;

volatile unsigned long riseUs = 0;
volatile unsigned long echoUs = 0;
volatile bool          haveReading = false;

void IRAM_ATTR onEcho() {
  if (digitalRead(ECHO)) {
    riseUs = micros();                      // burst left
  } else {
    echoUs = micros() - riseUs;             // burst returned
    haveReading = true;
  }
}

unsigned long lastPing = 0;

void setup() {
  Serial.begin(115200);
  pinMode(TRIG, OUTPUT);
  pinMode(ECHO, INPUT);
  attachInterrupt(digitalPinToInterrupt(ECHO), onEcho, CHANGE);
}

void loop() {
  // fire a ping every 60 ms without ever blocking
  if (millis() - lastPing >= 60) {
    lastPing = millis();
    digitalWrite(TRIG, HIGH);
    delayMicroseconds(10);
    digitalWrite(TRIG, LOW);
  }

  if (haveReading) {
    haveReading = false;
    unsigned long us = echoUs;
    if (us > 200 && us < 30000) {           // ignore junk edges
      Serial.printf("%.1f cm\n", us / 58.3);
    }
  }

  // ... motors, Wi-Fi, display all keep running here
}

Level 3 — the filter that makes it usable

A raw HC-SR04 stream contains occasional wild outliers: a burst that found a chair leg, or an edge missed under interrupt load. A median of the last five readings removes them almost completely, and unlike an average it does not smear a real, sudden change in distance.

Any board · median-of-5 filter

float buf[5];
uint8_t idx = 0;
bool filled = false;

float medianDistance(float fresh) {
  if (fresh < 0) return -1.0;
  buf[idx] = fresh;
  idx = (idx + 1) % 5;
  if (idx == 0) filled = true;
  uint8_t n = filled ? 5 : idx;

  float t[5];
  for (uint8_t i = 0; i < n; i++) t[i] = buf[i];
  for (uint8_t i = 1; i < n; i++) {          // insertion sort, n = 5
    float k = t[i]; int8_t j = i - 1;
    while (j >= 0 && t[j] > k) { t[j+1] = t[j]; j--; }
    t[j+1] = k;
  }
  return t[n / 2];
}

Accuracy, temperature and the speed of sound

The ±3 mm figure on the datasheet is a laboratory number: a flat, hard target, square to the sensor, at room temperature, at about 30 cm. In a real project the dominant error is not the module — it is the constant in your formula. The speed of sound changes with air temperature by roughly 0.6 m/s per °C:

speed_m_s = 331.3 + 0.606 × T_celsius
Air temperatureSpeed of soundReading at a true 200 cmError
0 °C331.3 m/s207.1 cm+3.5 %
20 °C (formula baseline)343.4 m/s200.0 cm0 %
35 °C (Indian summer indoors)352.5 m/s194.8 cm−2.6 %
45 °C (enclosure in the sun)358.6 m/s191.5 cm−4.3 %

Five centimetres of error at two metres, purely from heat. If your project needs better than a few percent — a tank level gauge, a height measurement, a parking stop line — pair the HC-SR04 with a temperature sensor you already have in the kit and compute the speed at runtime:

// with a DHT22 / DS18B20 already reading tempC
float speed  = 331.3 + 0.606 * tempC;        // m/s
float cmPerUs = speed * 100.0 / 1000000.0;   // cm per microsecond
float dist_cm = (pulse_us * cmPerUs) / 2.0;

What the beam can and cannot see

The burst spreads out in a cone of roughly 15°. At 1 m that cone is about 26 cm wide; at 3 m it is nearly 80 cm wide. The module reports the first strong echo inside that cone — not the object you are pointing at.

Good

Flat, hard, square-on

Walls, boxes, water surfaces, a car's number plate. These return most of the energy straight back.

Poor

Angled surfaces

Beyond about 30–45° off square, the burst reflects away like light off a mirror and never returns. Readings jump to "out of range".

Poor

Soft and fuzzy

Curtains, thick jackets, foam, a cat. They absorb 40 kHz sound. Expect dropouts and short readings.

Bad

Thin or round

A broom handle, a chair leg, a wire. Too little cross-section to return a detectable burst — until the cone widens enough to catch the floor behind it.

Two practical consequences. First, mount the sensor at least 20–30 cm above the floor and tilt it slightly up, or the cone will find the ground and report the floor distance forever. Second, if you need to know which object is where, one HC-SR04 cannot tell you — use two or three with staggered pings, and give each its own 60 ms slot so they do not hear each other.

When to stop using an HC-SR04

If you needUse insteadWhy
Millimetre accuracy, narrow beamVL53L0X / VL53L1X (laser ToF, I²C)±3 mm at 3.3 V, ~25° or 27° FoV, no acoustic reflections, much faster sample rate
Outdoor use, rain, dustJSN-SR04T (waterproof ultrasonic)Sealed transducer on a cable; same Trig/Echo interface, so the code above works unchanged
Detecting a person, not a distancePIR (HC-SR501) or mmWavePIR sees body heat movement and does not care about surface angle or texture
Very close range (< 2 cm)IR proximity or capacitiveThe HC-SR04 is blind below ~2 cm: the echo returns while the burst is still being sent
Battery-powered, months of lifeLaser ToF with a wake interruptThe HC-SR04 has no sleep mode and no interrupt output — it draws current whenever powered

Troubleshooting table

SymptomMost likely causeFix
Always reads 0 cmEcho pin not actually connected, or Trig and Echo swappedCheck continuity; confirm Trig is an OUTPUT and Echo an INPUT in code
Always reads maximum / "out of range"3.3 V on VCC, or the target is angled or softMove VCC to 5 V; aim at a flat hard surface 30 cm away to verify
Readings freeze at one valueNo timeout on pulseIn(), or pinging faster than 60 msAdd the timeout argument; enforce the 60 ms gap
Random spikes of 1–3 cmEcho edge missed under load (Wi-Fi, display refresh)Interrupt version plus the median-of-5 filter
Works on USB, fails on batterySupply sags below 5 V during the burstAdd a 100 µF electrolytic across the module's VCC/GND
ESP32 GPIO stopped responding5 V Echo applied directly to a 3.3 V pinFit the 1 kΩ / 2 kΩ divider; test the pin with a simple blink before assuming the board survived
Two sensors interfereBoth listening to each other's burstTrigger them in sequence, 60 ms apart, never simultaneously

Frequently asked questions

Can the HC-SR04 run at 3.3 V?

Some modules will respond at 3.3 V, but usable range drops dramatically — often under a metre — and accuracy becomes distance-dependent. The transducer needs the full 5 V drive to emit a burst with enough energy to return from a distant target. Power it at 5 V and level-shift the Echo output instead. Variants sold as "HC-SR04P" or "RCWL-1601" are explicitly rated for 3.0–5.5 V and are the right part if a 5 V rail genuinely is not available.

Why do I need a resistor divider on the Echo pin for ESP32?

Echo idles low and drives to VCC (5 V) when active. ESP32 and ESP8266 GPIO are specified to roughly 3.6 V absolute maximum. Every 5 V pulse stresses the input protection diode; boards often survive for weeks and then fail on one pin. A 1 kΩ series resistor with a 2 kΩ resistor to ground brings the pulse to about 3.3 V and costs almost nothing.

What is the real maximum range?

400 cm is the datasheet figure against an ideal flat target. In practice, plan for a reliable 250–300 cm indoors against walls and boxes, and much less against soft or angled surfaces. Beyond 3 m the returning burst is usually too weak for the comparator to register, and you get intermittent timeouts rather than gradually degrading numbers.

How fast can I take readings?

The hard limit is the 60 ms inter-measurement gap the datasheet asks for, giving about 16 readings per second. You can push to 25 Hz in a small, cluttered space where echoes die quickly, but any faster and previous bursts contaminate the measurement. If you need hundreds of samples per second, ultrasonic is the wrong technology — use a laser time-of-flight sensor.

Does the NewPing library fix these problems?

Partly. NewPing gives you a timeout by default, a built-in median function, and a timer-interrupt mode that avoids blocking — worth using on AVR boards. It does not fix physics: beam spread, soft targets, temperature drift and the 60 ms rule apply exactly the same. On ESP32 the interrupt approach shown above is usually simpler than porting library timer code.

Get the sensor, not just the article

The ultrasonic distance pack ships the HC-SR04, the divider resistors and the mounting bracket — the exact parts used in this guide.

Ultrasonic distance pack Browse all components
AR
Arun Roshan

Founder of eARgle Innovation Labs and eARgle Technologies, and a PhD scholar at VIT Vellore working on IoT–AI assistive technology. Ultrasonic sensing is part of his research on assistive navigation devices, where a missed echo is not an inconvenience but a safety failure.