Introduction: Asynchronously Reading the HC-SR04 Sensor

Hey guys,

This instructable is brought to you by the OpenS lab of Oregon State University.

The speed of sound is pretty slow. In the time it takes for the sonar sensor to send out a ping and get an echo back, the arduino can execute thousands of instructions. You can do all kinds of cool stuff while you're waiting for that echo to come back - yet all the tutorials I've seen online insist on using the delay() function or various memory and execution-time consuming libraries.

In this instructable, I'm going to teach you how to use the millis() function, set up an interrupt routine, and use the PORTC data register.

Step 1: Parts List

For this tutorial we'll need:

-An Arduino Uno

-An HC-SR04 sensor

-Four jumper wires

Connect the sensor and Arduino as shown in the diagram above.

Step 2: Setting Up the Digital Direction Register (DDRC)

On the Arduino, various libraries or functions usually take care of setting up which pins are for input and which are for output. In this instructable, we're going to do that ourselves using the data registers inside the Arduino. Bear with me - this isn't as complicated as it sounds.

Basically, a register is just a set of pins. For instance, registers ending in B control pins 8, 9, 10, 11, 12, and 13, while those ending in C control analog pins 0-5. Registers ending in D control the digital pins 0-7.

Now, chances are you're probably using a few of the digital pins for your project already, and sometimes analog ports 4 and 5 are used for i2c devices, so we're going to use analog pins 2 and 3 (which are controlled by registers ending in C). On the Arduino Uno, analog pins double as digital pins, so we won't run into any issues with that.

There are three main registers we're gonna use for each set of pins. We'll be using the DDRC (Digital Direction Register C, used to set pins to input or output), PORTC (used to set pins high or low), and PINC (used to read the current state of pins).

The HC-SR04 has two pins - TRIG and ECHO. TRIG is the pin used to send out a ping, and ECHO is the pin used to catch the bounceback of the ping. We'll go into that later, but right now all we need to know is that TRIG is going to be connected to our output pin, and ECHO will be connected to our input pin.

To do this, we need to use the Digital Direction Register (the one that controls which pins are for input and which are for output) to set Analog pin 2 to output.

This is basically a long-winded explanation for a very short snippet of code. All we need is this line in our setup():

DDRC |= B00000100; //Set port A2 to output

Step 3: Declaring the Interrupt

Say you have a button connected to a pin on the arduino. When someone presses the button, the button sets the pin to HIGH. When the button isn't actively being pressed, the pin is LOW. An interrupt detects when the state of the button changes from LOW to HIGH, or from HIGH to LOW. So for any given button press, the interrupt is called twice. Once when the button is pushed (LOW to HIGH), and once when the button is released (HIGH to LOW).

This is what we want to do with the ECHO pin of the HC-SR04. The sensor works by sending a HIGH signal on the ECHO pin right after the ping is sent out, and switching to LOW when it hears the echo from the ping come back.

This is the code for setting up an interrupt on Analog pin 3. If you wish to use a different pin, you can refer to the image in the previous step to figure out which PCINT, PCIE, and PCMSK number to use. This goes in our setup() as well.

PCICR |= (1 << PCIE1);
PCMSK1 |= (1 << PCINT11); //PCINT11 corresponds to analog pin 3

Step 4: Setting Up the Asynchronous Timing in Loop()

We're going to need two nested if statements - one that will be true if we are currently sending a pulse out, and one that will be true if we're waiting to hear an echo. These declarations will go before your setup().

boolean waitingForEcho = false;
boolean sendingPulse = false;

If both of these are false, the sonar isn't doing anything, so we'll want to send a pulse out. This is done by setting the TRIG pin to HIGH for at least a few microseconds (one millionth of a second) - I chose to send out a 12 microsecond pulse.

The micros() function keeps track of the microseconds since the Arduino was powered on. We can mark the exact time that we start the pulse, and then if the current micros() time exceeds the pulse start time by 12 microseconds, we want to turn it off. The following code goes at the top of our loop() function.

if(!waitingForEcho){   
 if(!sendingPulse){
      pulseStartTime = micros(); // record the start time of the pulse.
      PORTC |= B00000100; // use the PORTC register to set analog pin A2 HIGH.
      sendingPulse = true; // we don't want to execute this block of code twice in a row, so let's set this to true.
    }else if(pulseStartTime + 12 < micros()){ // if the start time + 12 microseconds is greater than the current micros() time
      PORTC &= B00000000; // use the PORTC register to set any currently HIGH ports to LOW.
      sendingPulse = false; // we're done sending the pulse, so we can set this to false.
      waitingForEcho = true; // now we want to wait for the echo to come back.
    }

Step 5: Setting Up the Interrupt Subroutine

We need a variable to store the start time of the echo wait period, a variable to store the time in between the ping being sent out and the echo being received, and a boolean to check if pin 3 was HIGH or LOW last time the interrupt was called.

These declarations will go before your setup():

unsigned long echoStartTime;
unsigned long durationOfEcho;
boolean previousInterruptWasHigh = false;

This is the actual interrupt function, which will be run when analog pin 3 changes states. This should go at the bottom of your sketch, outside of your setup() and your loop():

ISR(PCINT1_vect){  
  timeOfLastInterrupt = millis();
  
  if(PINC & B00001000 ){                                     // Is analog pin 3 high?
    if(!previousInterruptWasHigh){                         // If the last time this interrupt was run, the state was not HIGH
      echoStartTime = micros();                             // Mark the time this interrupt happened.
      previousInterruptWasHigh = true;                  // Make sure this block of code doesn't get run again next time
    }
  }else{ // Since the last if statement didn't get executed, analog pin 3 must've switched to LOW.
    durationOfEcho = micros() - echoStartTime; // Determine the duration that pin 3 was HIGH.
    waitingForEcho = false; // This is the boolean we used in loop(). We're no longer waiting for the echo.
    previousInterruptWasHigh = false;
  }
}

Step 6: Complete Sketch

/* 
   Jonah Siekmann
   OpenS Lab, Oregon State University
   4/14/2017
*/

//You may get an error when compiling that reads something like 'stray \342'
//This is an issue with copy+pasting out of instructable's website (they add invisible characters to text).
//I tried to remove them all and paste the code back in, but if the issue persists you may just need to retype the code by hand.

unsigned long pulseStartTime, echoStartTime, durationOfEcho, timeOfLastInterrupt;
double distance;
boolean previousInterruptWasHigh = false;
boolean waitingForEcho = false;
boolean sendingPulse = false;
void setup(){
  DDRC |= B00000100; //Set port A2 to output
  PCICR |= (1 << PCIE1);
  PCMSK1 |= (1 << PCINT11); //PCINT11 corresponds to analog pin 3
}
void loop(){
  if(!waitingForEcho){   
   if(!sendingPulse){
        pulseStartTime = micros(); // record the start time of the pulse.
        PORTC |= B00000100; // use the PORTC register to set analog pin A2 HIGH.
        sendingPulse = true; // we don't want to execute this block of code twice in a row, so let's set this to true.
      }else if(pulseStartTime + 12 < micros()){ // if the start time + 12 microseconds is greater than the current micros() time
        PORTC &= B00000000; // use the PORTC register to set any currently HIGH ports to LOW.
        sendingPulse = false; // we're done sending the pulse, so we can set this to false.
        waitingForEcho = true; // now we want to wait for the echo to come back.
      }
  }

  distance = ((double)durationOfEcho/2)/29.1;
  if(millis()-timeOfLastInterrupt > 20){
  	distance = -1;
  }
}
ISR(PCINT1_vect){  
  timeOfLastInterrupt = millis();
  if(PINC & B00001000 ){                                     // Is analog pin 3 high?
    if(!previousInterruptWasHigh){                         // If the last time this interrupt was run, the state was not HIGH
      echoStartTime = micros();                             // Mark the time this interrupt happened.
      previousInterruptWasHigh = true;                  // Make sure this block of code doesn't get run again next time
    }
  }else{ // Since the last if statement didn't get executed, analog pin 3 must've switched to LOW.
    durationOfEcho = micros() - echoStartTime; // Determine the duration that pin 3 was HIGH.
    waitingForEcho = false; // This is the boolean we used in loop(). We're no longer waiting for the echo.
    previousInterruptWasHigh = false;
  }
}