Arduino: An Ultrasonic Distance Detection Chronical

WARNING: This page contains some degree of caffeine fuelled brain fart

A student I was working with yesterday is developing a “SumoBot”, which as it turns out is a done thing where people build small autonomous microcontroller powered robots that have one goal; to smash all other opponents to oblivion! …or at least out of the ring. It’s basically lightweight Robot Wars. Maybe it would be nice to start with a little video of this:

So the robots need to do a number of things, they need to move forward, backwards and turn, they need to sense if they are safely within the ring, they need to be able to sense objects and also be reactive to these last two things. Perhaps it will be interesting to think about a more complete project in the near future, but what I want to share here ultimatately is a bit of code we worked with yesterday that I thought was functionally cool in my usual nerdy way! Real time ultra-sonic object sensing with minimal interuption of the main code.

I will say this now – whilst this isn’t a particularly complex program, it might give an Arduino beginner a bit of a headache as there is a couple of levels of conditional logic involved, and a slightly more advanced concept than just say, reading from a button. If you are a beginner you should at least familiarise yourself with the basics of Arduino and ideally get comfortable with the “BlinkWithoutDelay” sketch found in File>Examples>Digital.

Ultrasonic sensing seems like a very good option for a SumoBot object sensing system, a ultrasonic module will have a speaker and microphone setup in close proximity, working in conjunction. The module works by sending out a ultrasonic “ping” or pulse and then measure the amplitude (volume) of the responding echo. From this an accurate distance calculation can be made should any physical object disrupt the beam:

Ultrasonic sensor module
Ultrasonic sensor module

You can get modules very cheap these days – for £5 and under, though some stores sell identical looking modules for 10x that price (which is the price they were when I was a student!), I don’t know if there is any particular difference in quality between cheap and expensive modules – but these days I am used to working with the cheapest available and still get great results.

The benefits of ultrasonic sensors include little to no signal interferance, a very narrow projection / detection beam and a very good range of more than 2.5 meters (the one I am playing with right now is happily hitting 3meters plus). They are also pretty darn fast! If you want a more in depth understanding of the technology involved, this website seems to be a pretty good start.

Students that I work with use ultrasonic sensors a lot and in a variety of projects for their cost and stability benefits (and they come in nearly every Arduino Starter Kit). For most applications, time efficiency isn’t too much of a concern so the code they would use is pretty straight-forward and linear. As the ultrasonic modules are pretty generic, I normally get people started with something like this:

/*
 *  Ultrasonic Sensor Module sketch, calculates distance to object in range, 
 *  transmits distance in cm to serial monitor
 *  Aidan Taylor 2016
 */

const int echoPin = 4; // Echo Pin
const int trigPin = 2; // Trigger Pin
const int ledPin = 13; // internal LED for "out of range"

long duration, distance; // memory used to store/calculate distance

void setup() {
 Serial.begin (9600);
 pinMode(trigPin, OUTPUT);
 pinMode(echoPin, INPUT);
 pinMode(ledPin, OUTPUT);
}

void loop() {
 digitalWrite(trigPin, LOW); // reset the trigPin just in case
 delay(2); 

 digitalWrite(trigPin, HIGH); 
 delay(10); 
 digitalWrite(trigPin, LOW); // send a 10ms pulse
 
 duration = pulseIn(echoPin, HIGH); // pulseIn measures time in µseconds
                                    // to get response
                                    

 distance = duration/58.2; // rough conversion to cm 
 
 if (distance < 2 || distance > 400) { // pulseIn will return 0 if no object is in range
 Serial.println("out of range");
 digitalWrite(ledPin, HIGH); 
 }
 else {
 Serial.print("Object: ");
 Serial.print(distance);
 Serial.println("cm");
 digitalWrite(ledPin, LOW); 
 }
 
 //Delay 50ms before next reading.
 delay(50);
}

It’s pretty easy to see what this sketch does, though pulseIn may be a new function for you. Looking at void loop() sequentially, this is what happens:

  • manually send a trigger pulse on a digital pin (the sensor will in response send an ultrasonic ping)
  • get a reading back using pulseIn (more on this below)
  • If there is no object in range, turn on the internal LED and print a serial message
  • If there is an object in range, turn off the LED and print the distance approximation as a serial message
  • Wait for 50ms and repeat

Let’s take a quick look at the pulseIn() function. It is a little bit like digitalRead() except it has a different purpose. pulseIn() will pause the sketch and wait for a high or low pulse on the pin designated in its argument. When the pin receives a pulse change, pulseIn will start to measure the time it takes for the pin to revert again and will then write a value in to the designated memory block. This value is the time in microseconds (a second is made up of 1,000,000 of these!) – as a quick side note here, this number can be very big so you will probably want to use an unsigned long to store this number in memory. As a second side note, Arduino Uno can only calculate to an accuracy within 4 microseconds.

I measured the “echo” pin of the ultrasonic module on the oscilloscope and it makes it clear how this works well with pulseIn, when the module receives a pulse going from high to low on the “trig” pin, it will send a single responding pulse on the “echo” pin. The length of this pulse should be a fairly accurate calculation of the distance measurement (from the ultrasonic signal attenuation as discussed above) represented as the echo/reflection time in microseconds. Don’t think that the module is sending an actual echo of the ultrasonic pulse, it is sending a more useable bit of data for the Arduino after making the calculation with its own on-board microcontroller. I did wonder if the echo pin actually returned a PWM style pulse constantly, but this doesn’t seem to be the case, a single trigger gets a single response.

So anyway, back to SumoBot!

In my opinion, to make the ultimate bot-smashing-bulldozer the robot will need a seemless realtime sketch to run on, so it can multitask and react in err… 4 microseconds 🙂 Otherwise we would be looking at a program where the robot can only do one thing at a time, and it may take way too much time to do it!! Think of a situation where the robot is trying to back away a safe distance from the edge of the arena but an opponent is closing in, it can’t even scan for threats because of the time it takes to run its void reverse(); routine!

Rule N0.1 for realtime programming is absolutely no delays. A delay is literally a pause in the sketch where practically nothing happens at all. There are always tiny delays to some extent as the Arduino’s microcontroller can only process things so fast, but the processing speed is so fast that we don’t need to consider that – and there is little we can do about it anyway (though notably, some commands take longer than others). So the way around using delays is to use what I would describe as a “time based conditional”. A good example of this is provided in the Examples Sketchbook under File > Examples > Digital > BlinkWithoutDelay. The idea behind it goes like this:

  • Sample and store the current time
  • Check if x amount of time has passed by comparing against another time sample.

Psuedo code could look like:

thisTime;
lastTime;

thisTime = actualTimeMS();

if(thisTime - lastTime > 1000) {
  lastTime = thisTime;
}

This non-program constantly updates “thisTime” with a real time in say, milliseconds. Let’s say that “lastTime” is initialised as “0”, the if() function would check if thisTime minus 0 is a value greater than 1000, if a second has past then indeed this will be “true” and so lastTime will be updated with thisTime, otherwise nothing happens and the code keeps looping – now check what happens when lastTime is updated and the code loops around again to the if(), remember that thisTime is constantly updated.

Arduino has an internal clock that starts counting from 0 when void loop() begins. I’m not sure how it counts exactly, but you can access this internal timer with two different functions, millis() and micros(). As the names suggest, millis() will return a value in milliseconds and micros() returns a value in microseconds (accurate to 4 microseconds).

So a delay can be replaced in effect with a conditional argument that checks “has enough time past in order to start this action?”. If enough time hasn’t past, then the associated code will be skipped over and the program can continue with other actions. When working this way, a sketch can quickly start to look very complicated. You might need numerous blocks of memory assigned in order to store time samples and tests for a variety of actions, it takes longer to design and more human cognitive power to get it right than a linear sketch – but in the right applications the results are well worth it. So below is my code for realtime feedback from the ultrasonic sensor module:

/* 
 *  Realtime Ultrasonic object detection and distance measurement sketch
 *  Aidan Taylor 2017
 */

const int echoPin = 4;
const int trigPin = 2;

boolean trigStat = 0;
long duration, distance, lastDistance;

unsigned long timeMicros, trigMicros; 

void setup() {
  Serial.begin(9600); 
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
}

void loop() {
  timeMicros = micros();

  if((timeMicros-trigMicros) > 2000 && trigStat == 0) {
    trigStat = 1;
    digitalWrite(trigPin, HIGH);
  }
  else if((timeMicros-trigMicros) > 10000 && trigStat == 1) {
    trigStat = 0;
    trigMicros = timeMicros;
    digitalWrite(trigPin, LOW);
    duration = pulseIn(echoPin, HIGH);
    distance = duration/58.2;
  }

  if(distance != lastDistance ) {
    lastDistance = distance;
    Serial.print("Object: ");
    Serial.print(distance);
    Serial.println("cm");
  }
}

Ok so this code looks kinda different than the first – but it actually does the same thing only much faster, and with the added possibility of doing other things simultaneously. Let’s break down void loop() as a list of commands:

  • Sample the current time in microseconds, store it in memory (timeMicros)
  • If 2000 microseconds have passed and trigStat = 0 do this:
    • make trigStat 1
    • make the trigger go high
  • otherwise:
  • If 10000 microseconds have past and trigStat = 1 do this:
    • make trigStat 0
    • copy the time stored in timeMicros into trigMicros
    • make the trigger low
    • store the echo pulse length
    • convert the pulse length to cm
  • finally, if the distance recorded is not the same as the last distance do this:
    • copy new distance to last distance
    • print the current distance in cm

So, how do we actually check time has past? For each iteration of void loop(), the current Arduino counter readout is sampled and stored in memory block “timeMicros”, as there is very little to delay the code, timeMicros gets updated pretty constantly. “trigMicros” is our test criteria to use against timeMicros, it initialises with the value 0. After 2000µs (2ms or 0.002 seconds), timeMicros – trigMicros is definitely greater than 2000 (2001 – 0 > 2000 = True). This is the test criteria in the first if() function (remember if the test turns out false then the code continues without being hindered). The next if() checks if 10000µs have past with the same method, only if this turns out to be “true” then trigMicros has its value replaced with a copy of timeMicros.

So if you run through the code again, take into account that trigMicros now equals at least 10000 and timeMicros must equal at least 10001 once the “timeMicros = micros();” command has run. Let’s take this as a test case, (10001 – 10000 > 2000)? = False. Now another 2000µs need to pass before the conditional test can pass as True again.

In this sketch, the first if() starts the trigger pulse and the second if() ends that same trigger and also includes the pulseIn command to process the ultrasonic sensor, note again that the trigMicros test condition only gets reset here as well to prevent the spamming of trigger pulses.

In my tests, I found that you can’t set the time interval check to be too small, otherwise you start getting odd readings from the sensor, this is because the ultrasonic module echo pulse might end up being longer than the time out of pulseIn(). The trigStat value is a boolean which is put in place just to prevent the trigger from misfiring, the code constantly spamming useless commands or otherwise things happening out of sequence.

Try running both of the sketch examples on this page with your own sensor and note the difference in the speed the messages play back in the Serial Monitor – remember that in the realtime sketch messages only update if the distance value changes.

Some notes on speed and performance: the pulseIn() function is not unlike delay() in the effect it has on the program as the sketch will pause until pulseIn() registers a complete pulse or otherwise times out. You can improve things to some extent as a safeguard as pulseIn takes a third argument as the time out period, however you must take care with this as described above because you can end up with pulseIn() timing out and retriggering before the ultrasonic module echo pulse finishes in some cases – however we are talking about a really tiny amount of time here! Another thing I observed recently when working with a DAC that Serial.print() and Serial.println() commands make quite an impact on the fluidity of the code (at least at 9600 baud). This was observable with a trianglewave as sending Serial reports would turn the smooth triangle DAC output into a steppy ladder! I think that when you want the maximum performance from your microcontroller, you want to keep Serial comms to an absolute minimum and implement them only when absolutely necessary.

Wow did you read the whole thing? Well done! 😉