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
Contents

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:
- You raise Trig for 10 µsAnything shorter and the module may ignore it; anything much longer is harmless but wasted time.
- 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.
- Echo goes HIGHIt stays HIGH until the receiver detects a returning burst, or until the module gives up (~38 ms).
- 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
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.
| Board | VCC | Trig | Echo | Level shifting |
|---|---|---|---|---|
| Arduino Uno / Nano / Mega | 5 V | any digital pin | any digital pin | None needed |
| ESP32 / ESP32-C3 / ESP32-S3 | 5 V (VIN / 5 V pin) | any output-capable GPIO | via divider | Required — 1 kΩ + 2 kΩ divider, or a level shifter |
| ESP8266 (NodeMCU, Wemos D1) | 5 V (VIN) | e.g. D5 | via divider, e.g. D6 | Required — same divider |
| Arduino Uno R4 (5 V logic) | 5 V | any digital pin | any digital pin | None 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.
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.

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 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 temperature | Speed of sound | Reading at a true 200 cm | Error |
|---|---|---|---|
| 0 °C | 331.3 m/s | 207.1 cm | +3.5 % |
| 20 °C (formula baseline) | 343.4 m/s | 200.0 cm | 0 % |
| 35 °C (Indian summer indoors) | 352.5 m/s | 194.8 cm | −2.6 % |
| 45 °C (enclosure in the sun) | 358.6 m/s | 191.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.
Flat, hard, square-on
Walls, boxes, water surfaces, a car's number plate. These return most of the energy straight back.
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".
Soft and fuzzy
Curtains, thick jackets, foam, a cat. They absorb 40 kHz sound. Expect dropouts and short readings.
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 need | Use instead | Why |
|---|---|---|
| Millimetre accuracy, narrow beam | VL53L0X / 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, dust | JSN-SR04T (waterproof ultrasonic) | Sealed transducer on a cable; same Trig/Echo interface, so the code above works unchanged |
| Detecting a person, not a distance | PIR (HC-SR501) or mmWave | PIR sees body heat movement and does not care about surface angle or texture |
| Very close range (< 2 cm) | IR proximity or capacitive | The HC-SR04 is blind below ~2 cm: the echo returns while the burst is still being sent |
| Battery-powered, months of life | Laser ToF with a wake interrupt | The HC-SR04 has no sleep mode and no interrupt output — it draws current whenever powered |
Troubleshooting table
| Symptom | Most likely cause | Fix |
|---|---|---|
| Always reads 0 cm | Echo pin not actually connected, or Trig and Echo swapped | Check 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 soft | Move VCC to 5 V; aim at a flat hard surface 30 cm away to verify |
| Readings freeze at one value | No timeout on pulseIn(), or pinging faster than 60 ms | Add the timeout argument; enforce the 60 ms gap |
| Random spikes of 1–3 cm | Echo edge missed under load (Wi-Fi, display refresh) | Interrupt version plus the median-of-5 filter |
| Works on USB, fails on battery | Supply sags below 5 V during the burst | Add a 100 µF electrolytic across the module's VCC/GND |
| ESP32 GPIO stopped responding | 5 V Echo applied directly to a 3.3 V pin | Fit the 1 kΩ / 2 kΩ divider; test the pin with a simple blink before assuming the board survived |
| Two sensors interfere | Both listening to each other's burst | Trigger 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