Introduction: Non-blocking Ultrasonic Sensor for Arduino

Picture of Non-blocking Ultrasonic Sensor for Arduino

The HC-SR04 ultrasonic ranging module is a fantastic addition to any robot project. Its a very simple and cheap way to track a robots progress (assuming we have fixed reference points) over a reported range of 2-400cm with accuracies of 3mm possible. It's also a great way to add object avoidance to stop your precious creation crashing into the scenery.

The modules operation is very simple. When triggered with a 10us pulse on the trigger pin the module sends out eight 40kHz ultrasonic pulses. The module then takes the echo pin high and holds it there until it receives an echo back to the module. The length of time the echo pin is high is directly proportional to the distance to an object. In fact, this time is the time it takes the pulses to travel to the object and back and, because we know the speed of sound at 20°C in dry air at sea level in 343.2m/s, we can use the following formula to work out the distance from the module to the object:

distance (m) = (time(s) * speed (m/s)) / 2

As we are going to measuring time in microseconds its easy for us to work out how far sound travels per microsecond and simply multiply our measured time by this number.

343.2m/s = 0.0343.2cm/us

So now we can say:

distance (cm) = (time(us) * speed(cm/us)) / 2 = (time * 0.0343.2)/2

As multiplying and dividing are expensive steps (in terms of time) on a microcontroller we may as well divide our speed constant by 2 now and save repeating the effort later, thus:

distance (cm) = time(us) * 0.01716

Now, we are going to measuring an integer (whole) number of microseconds and we know that floating point mathematics is less efficient on a microcontroller than the integer equivalent. So, rather than multiplying our measured amount of time by a floating point number we could divide it by an integer because 0.01716 is approximately the same as 1/58, thus:

distance (cm) = time(us) / 58(cm/us)

By using the same method we can determine that the distance to an object in inches is as follows:

distance (in) = time(us) / 148(in/us)

So, now we know how the sensor works, how to use it and how to get a useful value from its output. The final problem, for me anyway, is that waiting for the echo pin to go low will block our programs execution. This means that whilst waiting for the module to take a reading we can't do anything else. Given the modules range of 400cm we could be waiting for as much as 23ms. Now, this may not seem like much, but for a robot which has an iterative control loop and has lots of other things to be doing (controlling motors, gathering other sensor data) this is a significant delay.

Thankfully, there is a way that the microcontroller can tell us when the echo pin changes rather than us having to watch and wait. This mechanism is called an 'interrupt' because the microcontroller literally interrupts the execution of the program when an interrupt condition occurs and call a function of our choosing. And this is the method I've used for this example. When the echo pin goes high at the beginning of the ranging our interrupt service routine (ISR, out function which deals with the interrupt) is called and we save the current time as our start time. When the echo pin goes low at the end of the ranging our ISR is called again and we save the current time as our end time. The difference between the start and end times is the time taken for the ranging. Simple! There are lots of resources out there that describe how interrupts far better than I could and I've already written far to much here! Also, I've wrapped the code up in a little library, so if you want to use it and don't care how interrupts work, you don't need to.

Step 1: Hardware

The hardware setup for this example is very simple. You need an Arduino, an HC-SR04 module (eBay) and four patch wires.

To hook the module up to the Arduino, do the following:

  1. Attach the Vcc and Gnd pins on the module to there compatriots on the Arduino
  2. If you are using an Arduino with an ATmega328 (Uno, Ethernet) or an a Mega then connect the echo pin to Arduino pin 2. If you're using a Leonardo, like me, then connect it to pin 3.
  3. Connect the trigger pin to a digital pin of your choice! I recommend pin 3 for the Uno users and pin 2 for the Leonardo users.


Step 2: Software

The necessary software for this example is attached. It takes the form of a small library (HC_SR04.h and HC_SR04.cpp) and a sketch that uses this library (Ultrasonic.ino), the content of which is as follows:

#include "HC_SR04.h"

#define TRIG_PIN 2
#define ECHO_PIN 3
#define ECHO_INT 0


void setup(){  
  while(!Serial) continue;

void loop(){
    // Do something with the range...
  // Do other things...

We simply define the pins and the interrupt we want to use for our sensor (for the pins specified in the previous step, ECHO_INT should be 0 irrespective of which Arduino you are using) and create an instance of the HC_SR04 class. We then call begin() during setup() to sort out the interrupts. Our sensor is now ready to use. To get a range value we call start() and then wait for isFinished() to return true. We don't have to sit and do nothing while we are waiting for this as we would otherwise, we can just check back periodically whilst do other things. In this case, we have nothing better to do! We can now get the range to an object by calling getRange(). This will return the range in centimetres by default but calling getRange(INCH) will get the value in inches.

Step 3: Conclusion

I hope this little example is useful to someone out there. I know there are many, many other examples of using these modules out there, but I just thought I would share my experience/wisdom(!).

The code for this and a few other bits and pieces is also available via my github page.

Thanks for reading :)


Juan IgnacioO (author)2017-05-30

thank's for sharing, small, simple, non blocking, just what i was looking for.

I have to make a smal change in the .cpp file

attachInterrupt(digitalPinToInterrupt(_echo), _echo_isr, CHANGE);

instead of

attachInterrupt(_int, _echo_isr, CHANGE);

didn't work before this change, now is working proprely


AlbertS99 made it! (author)2017-04-28

Thank you jazzycamel. This works well. Better than any of my attempts till now.

There is still a lot of jitter/dither probably caused by latency in responding to the interrupt. Using the getRangeRaw() that KakhaO proposed halves the jitter because the processor is not doing division.

I think the 'blocking' that russ_hensel mentioned is in start() with the delay(10) that enforced a 10us wait every cycle in loop(). I do not know of a way to get around this without messing up the timers.

drepster (author)2016-10-23

awesome! just what I was looking for. thanks.

KakhaO (author)2016-05-15

Thanks for shearing, just one idea to improve the library, I've added a function

unsigned int HC_SR04::getRangeRaw(){

return (_end-_start);


In main code I can use impulse raw data like that

#define CM2MKS(CM) CM*58


if (rng<CM2MKS(20))


russ_hensel (author)2015-01-26

In computer jargon non-blocking often indicates that a particurlal process will not stop or block other processes. This does not seem to be the case here. What do you mean non-blocking?

jazzycamel (author)russ_hensel2015-01-27

That is correct and it indeed is the case here. As it says in the text, because I'm using interrupts to inform me of when the ranging has completed I don't have to poll the echo pin and block the loop() function, I can just check to see if its finished: if yes then do something with the range, if no then carry on doing other things, thus: non-blocking. I appreciate my example sketch was a probably a little confusing on this point so I have updated it a little to reflect this.

Thanks for your comments :)