1

How can I measure the distance of an HR-S04 ultrasonic sensor with millis and micros?

5
  • 1
    I'd use the Input Capture ability of Timer1, but that's beyond the scope of this site - maybe you can find a library that does that for you.
    – Majenko
    Jul 3, 2021 at 11:18
  • @Majenko, did you see the self-answer?
    – Juraj
    Jul 3, 2021 at 15:13
  • @Juraj I did. Looks fine to me.
    – Majenko
    Jul 3, 2021 at 15:16
  • @Majenko, most ultrasonic code I saw activated trig for some microseconds and then used pulseIn to catch the echo. but the code in answer triggers for 10 milliseconds using states
    – Juraj
    Jul 3, 2021 at 15:30
  • The falling edge is what matters. It still uses pulseIn.
    – Majenko
    Jul 3, 2021 at 15:31

2 Answers 2

3

In response to @Majenko comment on the original question, here is some code that I use which uses the Timer1 input capture function to read an ultrasonic sensor. It also uses the Timer1 PWM generator to create the trigger pulses. The entire code is interrupt driven and nothing blocks the main program.

It's not built as an official style library with all the extra parts, but the .h and .cpp can be put into a folder in the libraries folder as in the manual installation process. It's really just a personal library that I use on some of my robots, but I'm happy to share.

The code as written supports the ATMEGA1284P and ATMEGA328P chips. There are a couple of lines to comment and un-comment to change between the two.

It exposes a singleton instance much like the Serial class, so there's nothing to instantiate. The pins are all set by hardware, you can't change them.

Depending on how much total range you want, you can set the prescaler to a much lower value. If you run with no prescaler then the resolution would be something on the order of nearly 100 times what you can get with micros (62.5ns vs 4us resolution). But that would be shorter distances like 1.2m and less.

https://github.com/delta-G/PingTimer

#include "PingTimer.h"

int distance;

void setup() {
  
  Serial.begin(9600);
  ping.begin();
  ping.sendPing();

}

void loop() {

  //  if the ping sensor has new data save it to distance and print it.
  if(ping.hasNewData()){
    distance = ping.getDistanceMM();
    Serial.print("Current Distance: ");
    Serial.println(distance);
    ping.sendPing();   
  }   

  //  Do other things not blocked by ping sensor

}

PingTimer.h

/*

PingTimer  --  uses Timer1 Input Capture to read a HC-SR04
     Copyright (C) 2019  David C.

     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
     the Free Software Foundation, either version 3 of the License, or
     (at your option) any later version.

     This program is distributed in the hope that it will be useful,
     but WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     GNU General Public License for more details.

     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.

     */

#ifndef PINGTIMER_H_
#define PINGTIMER_H_

#include "Arduino.h"

// Ping pin on OC1A -- PB1 on 328P.  (UNO pin 9)
                    //-- PD5 on 1284P  (pin 13)
#define PING_PIN_MASK (1 << 5)
#define PING_PIN_PORT PORTD

// Echo pin on ICP1 -- PB0 on 328P  (UNO pin 8)
                    //-- PD6 on 1284P  (pin 14)
#define ECHO_PIN_MASK (1 << 6)
#define ECHO_PIN_PORT PIND

class PingTimer {
private:
    volatile uint16_t timerVal;
    volatile boolean overflowed;
    volatile boolean newData;

    void initTimer();

    void startPulse();


public:
    PingTimer();
    void begin();

    void sendPing();
    void echoHandler();
    void overflowHandler();

    boolean hasNewData();
    boolean hasOverflowed();
    uint16_t getTimerVal();

    int16_t getDistanceMM();

};

extern PingTimer ping;

#endif /* PINGTIMER_H_ */

And the PingTimer.cpp

/*

PingTimer  --  uses Timer1 Input Capture to read a HC-SR04
     Copyright (C) 2019  David C.

     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
     the Free Software Foundation, either version 3 of the License, or
     (at your option) any later version.

     This program is distributed in the hope that it will be useful,
     but WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     GNU General Public License for more details.

     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.

     */

#include "PingTimer.h"

PingTimer ping;


PingTimer::PingTimer(){
    newData = false;
    overflowed = false;
    timerVal = 0;
}

void PingTimer::begin() {
    initTimer();
    // ATMEGA328P
//  pinMode(8, INPUT);
//  pinMode(9, OUTPUT);
    // ATMEGA1284P
    pinMode(14, INPUT);
    pinMode(13, OUTPUT);
}


void PingTimer::initTimer(){
    cli();
    TCCR1A = 0;  // no PWM
    // Noise Cancel On, RISING Edge , No prescaler, 4ms range.
    TCCR1B |= ((1 << ICNC1) | (1 << ICES1) | (1 << CS11) | (0 << CS10));
    // make sure analog comparator isn't selected as capture input
    ACSR &= ~(1 << ACIC);
    //  Turn off Timer interrupts
    TIMSK1 = 0;
    // setup COMPA for 10us to time the trigger pulse
    // and COMPB for 12ms for out of range signal (a little over 4 meters)
    OCR1AH = 0;
    OCR1AL = 20;  // With prescaler at 8 each tick is 0.5 us.
    OCR1BH = 0xB5;
    OCR1BL = 0xB0; // 0xB5B0 is 46512.  That's a little over 4 meters with prescaler at 8.
    sei();
}

//  This is a public facing function to call the startPulse so it can stay private
//  since it twiddles registers
void PingTimer::sendPing() {
    startPulse();
}

void PingTimer::startPulse(){
    // set pin high and setup PWM unit to
    // turn the pin off 10us later
    cli();
    TCNT1H = 0x00;
    TCNT1L = 0x00;
    //Set up to get one pulse of PWM
    // Clear OC1A on compare match.  10bit fast PWM
    TCCR1A = ((1 << COM1A1) | (1 << WGM11) | (1 << WGM10));
    // Setup input capture interrupt to catch the rising
    // of the echo pin and start the timer
    // Noise Cancel On, RISING Edge ,prescaler at 8 gives a 32ms timer = 5.5 meter range
    TCCR1B = ((1 << ICNC1) | (1 << ICES1) | (1 << CS11) | (0 << CS10) | (1 << WGM12));
    //  Turn on interrupt Input Capture
    TIFR1 = ((1 << ICF1) | (1 << TOV1));
    TIMSK1 = ((1 << ICIE1));
    overflowed = false;
    newData = false;
    sei();
}



// Called from ISR
void PingTimer::echoHandler() {
    if (ECHO_PIN_PORT & ECHO_PIN_MASK) {  //Rising Pin
        // Kill the OC1A PWM unit and return to normal mode
        TCCR1A = 0;
        // Noise Cancel On, FALLING Edge ,prescaler at 8 gives a 32ms timer = 5.5 meter range
        TCCR1B = (1 << ICNC1) | (0 << ICES1) | (1 << CS11) | (0 << CS10);
        // Reset Timer1
        TCNT1H = 0;
        TCNT1L = 0;
        //  Reset Input Capture and Timer Overflow Interrupts
        TIFR1 = (1 << ICF1) | (1 << TOV1);
        //  Turn on interrupts Input Capture and Timer Overflow
        TIMSK1 = (1 << ICIE1) | (1 << TOIE1);
    } else {  // Falling pin
        // Read the ICR1 Register
        uint8_t low = ICR1L;
        uint8_t high = ICR1H;
        timerVal = ((high << 8) | low) - 4; // Noise canceler adds 4 clock cycles

        //  turn off the timer interrupts
        TIMSK1 = 0;
        newData = true;
    }
}

// Called from ISR  Timer 1 OVF
void PingTimer::overflowHandler(){
    //turn off timer interrupts
    TIMSK1 = 0;
    newData = true;
    overflowed = true;
}



boolean PingTimer::hasNewData(){
    cli();
    boolean retval = newData;
    newData = false;
    sei();

    return retval;
}

boolean PingTimer::hasOverflowed(){
    cli();
    boolean retval = overflowed;
    overflowed = false;
    sei();

    return retval;
}

uint16_t PingTimer::getTimerVal() {
    uint16_t retval = 0;
    cli();
    if (overflowed) {
        retval = -1;
    } else {
        uint16_t copy = timerVal;
        retval = copy;
    }
    sei();
    return retval;
}

int16_t PingTimer::getDistanceMM() {

    int16_t retval = 0;
    uint16_t copy = getTimerVal();
    if (copy == (uint16_t) -1) {
        retval = -1;
    } else {
        // 16Mhz is 62.5 ns/clock
        // We've got the prescaler dividing by 8
        uint32_t nanoSecs = copy * 62.5 * 8;
        //speed of sound in mm/ns
        uint16_t mm = nanoSecs * 0.000344;
        retval = mm / 2;  // half a round trip
    }
    return retval;
}

ISR(TIMER1_CAPT_vect){
    ping.echoHandler();
}

ISR(TIMER1_OVF_vect){
    ping.overflowHandler();
}

ISR(TIMER1_COMPB_vect){
    ping.overflowHandler();
}
2
  • Many years ago, it was recommended to access 16-bit I/O registers one byte at a time, reading LSB-first and writing MSB-first. This is ancient history. As gcc has since learned how to access those registers, you can now simply write OCR1B = 46512 and timerVal = ICR1. Jul 24, 2023 at 14:01
  • I think I sorta knew that and wasn't sure I remembered right so I just went with what the datasheet said to do. If I update this later I'll add that change.
    – Delta_G
    Jul 24, 2023 at 16:32
1

I have noticed that many people on the net have problems using millis with the sensor to take measurements etc. so I created this sketch to help anyone.

You are free to ask for any clarification about it!

/*
 *  HC-SR04 sensor measuring
 *
 *  Non-blocking HC-SR04 sensor measuring
 *
 *  The circuit:
 *  - Echo pin connected to pin 2
 *  - Trigger pin connected to pin 3
 *  - VCC pin connected to VCC (5v)
 *  - GND gger pin connected to GND
 *
 *  created 3 Jul 2021
 *  by alessandromrc
 */

#define trigPin 3
#define echoPin 2

unsigned long timerStart = 0;
unsigned long StartTime = micros();
bool timer = false;
const unsigned long HIGH_TRIGGER = 10;
const unsigned long LOW_TRIGGER = 2;

float timeDuration, distance;

enum SensorStatus {
  TRIG_LOW,
  TRIG_HIGH,
  ECHO_HIGH
};

SensorStatus sensorStatus = TRIG_LOW;

bool isTimerReady(const unsigned long Sec) {
  return (micros() - timerStart) < Sec;
}

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

void loop(void) {
  switch (sensorStatus) {
  case TRIG_LOW: {
    digitalWrite(trigPin, LOW);
    timerStart = micros();
    if (isTimerReady(LOW_TRIGGER)) {
      sensorStatus = TRIG_HIGH;
    }
  }
  break;

  case TRIG_HIGH: {
    digitalWrite(trigPin, HIGH);
    timerStart = micros();
    if (isTimerReady(HIGH_TRIGGER)) {
      sensorStatus = ECHO_HIGH;
    }
  }
  break;

  case ECHO_HIGH: {
    if (!timer) {
      Serial.print("Microseconds Passed in initialization: ");
      Serial.println(micros() - StartTime);
      timer = !timer;
      Serial.println("Starting to Measure");
    }
    digitalWrite(trigPin, LOW);
    timeDuration = pulseIn(echoPin, HIGH);
    Serial.print("Measured: ");
    Serial.print(timeDuration * 0.034 / 2);
    Serial.println(" cm");
    sensorStatus = TRIG_LOW;
  }
  break;
  }
}

Github Repository

2
  • it is enough to trigger for 5 microseconds. why do you use a separate state to trigger for 10 milliseconds?
    – Juraj
    Jul 3, 2021 at 15:35
  • 1
    @Juraj I didn't knew about this, thanks for letting me know! Jul 3, 2021 at 19:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.