The HC-SR04 is one of the most popular ultrasonic distance sensors in the Arduino ecosystem — affordable, easy to wire, and capable of measuring distances from 2 cm to 400 cm. But anyone who has used it seriously quickly runs into the same problem: the readings can be noisy, inconsistent, or just plain wrong.
Whether you are building a robot that keeps bumping into walls, a parking sensor that triggers at random, or a water level monitor giving erratic readings, poor HC-SR04 accuracy is a frustrating roadblock. The good news is that most accuracy problems are solvable with the right software techniques, hardware practices, and an understanding of how the sensor actually works.
This guide covers everything from the physics of sound-based ranging to advanced filtering algorithms you can implement on any Arduino board today.
How the HC-SR04 Actually Works
Before fixing accuracy issues, it helps to understand the measurement principle. The HC-SR04 works by emitting a burst of 8 ultrasonic pulses at 40 kHz when the TRIG pin receives a 10-microsecond HIGH pulse. The ECHO pin then goes HIGH and stays HIGH until the reflected sound wave returns to the receiver transducer. By measuring how long the ECHO pin stays HIGH, you can calculate distance.
The standard formula is:
distance (cm) = echo_duration_microseconds / 58.2
This 58.2 divisor comes from the speed of sound at approximately 340 m/s at 20°C, divided by 2 (because the pulse travels to the target and back), and converted to the right units. The problem is that this number is only correct at exactly 20°C. At 35°C — a realistic temperature in India — the speed of sound is closer to 352 m/s, introducing a ~3.5% distance error before you even touch the code.
The sensor has a beam angle of roughly 15 degrees. Anything within that cone will reflect sound, including walls, floors, cables, and moving hands. The sensor cannot distinguish between the intended target and an obstacle at the edge of the beam.
Common Accuracy Problems and Their Causes
Understanding why readings go wrong is the first step to fixing them. Here are the most common culprits:
1. Noisy or Bouncing Readings
If your Serial Monitor shows values that jump by 5–30 cm between readings, you are seeing acoustic noise. Hard walls perpendicular to the sensor reflect cleanly, but angled surfaces, soft objects like foam, or round objects scatter the sound, producing multiple reflections. The sensor may pick up secondary echoes.
2. Zero or 0 cm Readings
A reading of 0 usually means the pulseIn() function timed out. The default timeout is 1 second, but if the target is outside the sensor range or the surface absorbs too much sound, no echo returns. Always check for timeout and discard zero readings.
3. Readings That Are Consistently Off by a Fixed Amount
This is almost always a temperature error or a physical offset. If your sensor is mounted 3 cm from the actual reference point, every reading will be off by 3 cm. Measure from the transducer face, not the PCB edge.
4. Ghost Readings (Values Far Too Large)
If the previous echo has not fully dissipated before the next trigger pulse fires, the sensor may measure the time until a ghost reflection. Always wait at least 60 ms between measurements (the datasheet recommends this).
5. Interference from Other Ultrasonic Sensors
Running two HC-SR04 sensors simultaneously without synchronisation causes them to hear each other’s pulses. Stagger your trigger pulses in time, or use sensors with different frequencies.
Temperature Compensation for Speed of Sound
This single improvement can reduce systematic error by 2–4% in typical Indian ambient conditions. The speed of sound in air depends on temperature according to:
speed_of_sound (m/s) = 331.4 + (0.606 * temperature_celsius)
To implement temperature compensation, pair your HC-SR04 with a DHT11 or DS18B20 temperature sensor and feed the live temperature into your distance calculation:
#include <DHT.h>
#define DHTPIN 4
#define DHTTYPE DHT11
#define TRIG_PIN 9
#define ECHO_PIN 10
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(115200);
dht.begin();
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
}
float getDistance(float tempC) {
float speedOfSound = 331.4 + (0.606 * tempC); // m/s
float usPerCm = (10000.0 / speedOfSound); // microseconds per cm one-way
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(4);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH, 25000); // 25ms timeout = ~4m max
if (duration == 0) return -1; // timeout
return duration / (2.0 * usPerCm);
}
void loop() {
float temp = dht.readTemperature();
if (!isnan(temp)) {
float dist = getDistance(temp);
if (dist > 0) {
Serial.print("Distance: ");
Serial.print(dist);
Serial.println(" cm");
}
}
delay(100);
}
By reading temperature every few seconds (temperature changes slowly), you can keep the compensation overhead minimal while gaining significant accuracy.
Software Filtering: Median, Moving Average, and Kalman
Even with temperature compensation, individual readings will still bounce due to acoustic noise. Filtering smooths out these variations.
Median Filter (Best for Outlier Rejection)
Take an odd number of readings (5 or 7) and return the middle value. Spikes and dropouts caused by transient reflections are discarded. This is the most robust simple filter for HC-SR04:
float medianFilter(int samples) {
float readings[samples];
for (int i = 0; i < samples; i++) {
readings[i] = getRawDistance();
delay(15); // wait between pings to avoid echo overlap
}
// Simple bubble sort
for (int i = 0; i < samples - 1; i++)
for (int j = 0; j < samples - i - 1; j++)
if (readings[j] > readings[j + 1]) {
float tmp = readings[j];
readings[j] = readings[j + 1];
readings[j + 1] = tmp;
}
return readings[samples / 2];
}
Moving Average Filter (Best for Slow-Moving Targets)
Maintain a circular buffer of the last N readings and return their average. Slower to respond to sudden changes but very smooth:
#define WINDOW 8
float buf[WINDOW];
int idx = 0;
float sum = 0;
float movingAverage(float newVal) {
sum -= buf[idx];
buf[idx] = newVal;
sum += newVal;
idx = (idx + 1) % WINDOW;
return sum / WINDOW;
}
Exponential Moving Average (Low Memory, Responsive)
A good compromise between lag and smoothness. Alpha controls the balance — lower alpha = smoother but slower:
float ema = 0;
const float alpha = 0.2;
float expMovAvg(float newVal) {
ema = alpha * newVal + (1.0 - alpha) * ema;
return ema;
}
Kalman Filter (Best Overall, Slightly More Complex)
A Kalman filter models both the measurement noise and the expected rate of change of the target. It is optimal for linear systems and produces very clean distance estimates even on fast-moving targets. A simplified 1D version:
float kalmanFilter(float measured) {
static float estimate = 0;
static float errorEstimate = 100;
static float errorMeasure = 4; // measurement noise (tune this)
static float q = 0.1; // process noise
errorEstimate += q;
float K = errorEstimate / (errorEstimate + errorMeasure);
estimate = estimate + K * (measured - estimate);
errorEstimate = (1 - K) * errorEstimate;
return estimate;
}
For most projects, a 5-sample median filter is the best starting point. It is fast, easy to understand, and handles the worst outliers without requiring parameter tuning.
Timing Improvements and Trigger Pulse Optimisation
The standard pulseIn() function in Arduino is blocking — it stops all other code while waiting for the echo. On a slow Arduino Uno, this adds measurable latency, and the function itself has some overhead. Here are timing improvements that help:
Set an Appropriate Timeout
The default pulseIn() timeout of 1 second is far too long. If your maximum range is 100 cm, the maximum echo time is about 5.8 ms. Set the timeout to 25,000 microseconds (25 ms) to cap the wait time and detect out-of-range targets quickly:
long duration = pulseIn(ECHO_PIN, HIGH, 25000);
Clear the Trigger Pin Before Each Pulse
Always drive TRIG LOW for at least 4 µs before the HIGH pulse to guarantee a clean edge:
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(4);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
Use NewPing Library
The NewPing library by Tim Eckel is a drop-in replacement for manual HC-SR04 code. It handles timing correctly, supports timeout, has a built-in ping_median() function, and even supports interrupts for non-blocking operation. Install via the Arduino Library Manager and reduce your code to:
#include <NewPing.h>
#define MAX_DISTANCE 200
NewPing sonar(TRIG_PIN, ECHO_PIN, MAX_DISTANCE);
void loop() {
unsigned int dist = sonar.ping_median(5); // median of 5 readings
Serial.println(sonar.convert_cm(dist));
delay(50);
}
Hardware Tips to Reduce Interference
Software can only do so much. Physical setup matters enormously for HC-SR04 accuracy:
- Mount the sensor away from vibrating motors or fans. Mechanical vibration creates acoustic noise that the receiver picks up directly.
- Keep wires short. Long ECHO wires act as antennas and can pick up electrical noise that causes false short-distance readings.
- Add a 100 µF capacitor between VCC and GND close to the sensor. The HC-SR04 draws a transient current spike when firing, which can cause a voltage dip on the supply rail and affect the microcontroller’s timing.
- Ensure the target surface is perpendicular to the sensor beam. Surfaces at an angle reflect sound away from the receiver, reducing signal strength and introducing multipath errors.
- Mount the sensor at least 3 cm above any flat surface to avoid the sensor picking up the mounting surface as a false echo.
- For outdoor use, shield the sensor from wind. Turbulent airflow changes the local speed of sound and creates noise. A simple foam windbreak around the transducers helps significantly.
Complete Improved Code Example
Here is a production-ready sketch that combines temperature compensation, a 5-sample median filter, timeout handling, and proper trigger timing:
#include <DHT.h>
#define TRIG_PIN 9
#define ECHO_PIN 10
#define DHT_PIN 4
#define DHTTYPE DHT11
#define MAX_DIST_CM 300
#define SAMPLES 5
#define MEAS_DELAY 15 // ms between each ping in median
DHT dht(DHT_PIN, DHTTYPE);
float tempC = 25.0; // fallback temperature
unsigned long lastTempRead = 0;
void setup() {
Serial.begin(115200);
dht.begin();
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
digitalWrite(TRIG_PIN, LOW);
}
float pingCm(float temp) {
float sos = 331.4 + 0.606 * temp; // speed of sound m/s
long timeout_us = (long)((MAX_DIST_CM * 2.0 / 100.0 / sos) * 1e6) + 1000;
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(4);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long dur = pulseIn(ECHO_PIN, HIGH, timeout_us);
if (dur == 0) return -1;
return (dur * sos) / (2.0 * 10000.0);
}
float medianPingCm(int n, float temp) {
float v[n];
for (int i = 0; i < n; i++) {
v[i] = pingCm(temp);
delay(MEAS_DELAY);
}
// insertion sort
for (int i = 1; i < n; i++) {
float key = v[i]; int j = i - 1;
while (j >= 0 && v[j] > key) { v[j+1] = v[j]; j--; }
v[j+1] = key;
}
return v[n / 2];
}
void loop() {
// Update temperature every 5 seconds
if (millis() - lastTempRead > 5000) {
float t = dht.readTemperature();
if (!isnan(t)) tempC = t;
lastTempRead = millis();
}
float dist = medianPingCm(SAMPLES, tempC);
if (dist < 0) {
Serial.println("Out of range");
} else {
Serial.print("Distance: ");
Serial.print(dist, 1);
Serial.print(" cm Temp: ");
Serial.print(tempC, 1);
Serial.println(" C");
}
delay(200);
}
This code achieves typical accuracy of ±1–2 cm across the 5–200 cm range, compared to ±3–5 cm with the default approach.
Frequently Asked Questions
Why does my HC-SR04 give readings of 0 or very large numbers randomly?
Zero readings indicate a pulseIn() timeout — the echo never returned, either because the target was too far, absorbed the sound, or was angled away. Very large readings (above 400 cm) usually mean a delayed secondary echo or electrical noise on the ECHO pin. Add a timeout to pulseIn(), validate the reading is within your expected range, and add a 100 µF decoupling capacitor near the sensor’s VCC pin.
Can I use HC-SR04 indoors and outdoors with the same code?
Yes, but outdoor accuracy is worse because wind and rain affect the sound propagation. Temperature compensation becomes even more important outdoors due to larger ambient temperature swings. Shield the sensor from direct wind and consider using a waterproof ultrasonic sensor (e.g. JSN-SR04T) for outdoor applications.
How close can I reliably measure with the HC-SR04?
The datasheet specifies a minimum range of 2 cm, but in practice, readings below 5 cm are unreliable on most HC-SR04 clones. The ECHO pulse overlaps with the TRIG pulse at very short distances, causing the sensor to miss the return. For precise short-range measurement (under 5 cm), use a Sharp IR distance sensor instead.
My HC-SR04 works fine in testing but fails on my robot — why?
Motor noise is almost certainly the culprit. DC motors and stepper motors generate large voltage spikes on the power supply rails and electromagnetic interference. Use separate power rails for motors and the sensor, add bulk capacitors (100 µF and 0.1 µF in parallel) near the sensor, and route the ECHO wire away from motor wires. Twisted pair for ECHO and GND also helps.
How many HC-SR04 sensors can I run simultaneously on one Arduino?
You can connect as many as you have available digital pins, but you must fire them one at a time, not simultaneously. If two sensors fire at the same time, they will hear each other’s pulses. Use a sequential loop with at least 60 ms between the ECHO of one sensor and the TRIG of the next, or use a multiplexer to share the ECHO pin and trigger sensors individually.
Start Building More Accurate Distance Sensors
Improving HC-SR04 accuracy is not a single tweak — it is a combination of understanding the physics, compensating for temperature, filtering the software output, and careful physical mounting. With the techniques in this guide, you can achieve ±1–2 cm accuracy in real-world conditions, turning a basic sensor into a reliable component for robotics, automation, and measurement projects.
Ready to start? Browse the complete range of Arduino-compatible sensors and boards at Zbotic.in and get everything you need delivered across India.
Add comment