Reading the HC-SR04 with Timer1 and no interrupts

Attached is my "device checkout code" for the HC-SR04 Ultrasonic sensor. This code only triggers and reads the sensor echo.
It uses Timer1 in the capture mode (capture Rising edge and capture falling edge).

The code (sketch) is kind of a bare bones approach used only as a starting point for other programs utilizing the HC-SR04.
Writing this code I learned (from the forum community):

  1. Interrupts are possible in the setup portion of a sketch.
  2. To clear the Timer1 capture flag (ICF1) one must set it to 1 !
// Device Test Code
// HC-SR04 Ultrasonic distance sensor
// 2022-03-11  fully functional.
//
// This program is like example code for using Timer1 w/o interrupts
// to measure the sensor echo.
// Captured by Timer1 are:
//    1) Time when echo goes high
//    2) Time when echo goes low.
// It is thought using Timer1 is more accurate than the
//  built in PulseLN() function.  This has not been proven.
//
// Connect echo pulse to Arduino ICP1 pin.
//
// see end of file for additional information.

#define bit(b) (1UL << (b)) // use this instead of _BV(b)

const byte TriggerPin = 9;
const byte EchoPin    = 8;      // ICP1 input pin
const byte LedPin     = 13;

// TCCR1B values to start Timer1,  CSS11 = 1, sets prescale = 8 (1 MHz count rate)
// Input Capture Edge Select, ICSE1 (1=Rising, 0=Falling)
const uint8_t TCCR1BCaptureRisingEdge  = bit(CS11) | bit(ICES1);
const uint8_t TCCR1BCaptureFallingEdge = bit(CS11);

void setup()
{
  Serial.begin(9600);
  delay(50);
  while (!Serial);
  Serial.println(" Starting ....");

  digitalWrite(TriggerPin, LOW);
  pinMode(TriggerPin, OUTPUT);
  pinMode(EchoPin, INPUT);
  pinMode(LedPin, OUTPUT);

  digitalWrite(LedPin, LOW);

  noInterrupts ();  // protected code
  // reset Timer 1
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  TIMSK1 = 0;

  TIFR1 = _BV(ICF1); // clear Input Capture Flag
  TCCR1B = TCCR1BCaptureRisingEdge;
  interrupts ();
}

void loop(){
  TCNT1 = 0;
  TIFR1 = bit(ICF1); // clear Input Capture Flag
  TCCR1B = TCCR1BCaptureRisingEdge;
    
  //Trigger:
  digitalWrite(TriggerPin, LOW);  // just to be sure trig is low
  delay(1);
  digitalWrite(TriggerPin, HIGH);
  delayMicroseconds(8);
  digitalWrite(TriggerPin, LOW);
  delayMicroseconds(8);             // may not be required.

  while ((TIFR1 & bit(ICF1)) == 0); // Is the Input Capture Flag set?
  noInterrupts ();
    TIFR1 = bit(ICF1); // Clear the Input Capture Flag (Yes, by setting it to 1)
  Interrupts();
  uint16_t riseTime = ICR1;
  digitalWrite(LedPin, HIGH);       //  for troubleshooting.

  // Now look for a falling edge
noInterrupts();
  TIFR1 = _BV(ICF1); // clear Input Capture Flag
  TCCR1B = TCCR1BCaptureFallingEdge;
Interrupts();

  while ((TIFR1 & bit(ICF1)) == 0);   // Is the Input Capture Flag set?
  uint16_t fallTime = ICR1;
  Serial.print (riseTime);
  Serial.print ("    ");
  Serial.println (fallTime);
  delay(2000);
}

/*
I thank numerous Arduino forum posters from which I've gleaned bits and pieces.  Special thanks to
John Wasser and westfw for questions regarding interrupts and using Timer1 ICF1.
This code triggers and reads the echo pulse width of a HC-SR04 ultrasonic transducer.
This version uses no interrupts, however the program is "blocked" by the "while" waits
for the echo pulse edges.

THis program targets the Arduino Pro Mini running at 8Mhz.  Timer1 has a prescale of 8 resulting
in 1µs/count.

interrupts in Setup .. see:  https://forum.arduino.cc/t/is-there-a-risk-of-interrupts-in-setup/968030/2

Notes:
1) The "riseTime" represents the time between the trigger and the echo pin going high.
2) With the above settings for Timer1 can count to about 65ms. Far longer than required for the HC-SR04
   if the prescaler is set to 1 instead of 8 the max count will be 9ms Shorter that the max range of the
   sensor (put with higher resolution).  However it seems the HC-SR04 is not consistent enough to benefit
   from the added resolution at the shorter ranges.

Typical output at 65CM:
1256 4658
1261 4663
1260 4662
1256 4658
1268 4668
1261 4661
1256 4658
1256 4659

*/
//  -- eof --

You may be interested in this deep dive into the HC-SR04. His goal was an accuracy of 1 mm. One of the first steps was to use the capture register, but that wasn't enough....

https://www.davidpilling.com/wiki/index.php/HCSR04

He reports achieving his goal.

1 Like

If you don't mind using the Input Capture interrupt:

// Measure the HC-SR04 echo pulse on Arduino UNO Pin 8 (ICP1 pin) and calculate distance

const byte TriggerPin = 9;
const byte EchoPin = 8; // MUST be 8 (ICP1)

// Note: Since this uses Timer1, Pin 9 and Pin 10 can't be used for
// analogWrite().

// Speed of sound:
// 343 meters per second 
// == 0.002915452 seconds per meter
// == 2915.452 microseconds per meter
const float MicrosecondsPerCM = 29.15452;
const float MicrosecondsPerRoundTripCM = MicrosecondsPerCM * 2;  // ~58.3
const float HalfMicrosecondsPerRoundTripCM = MicrosecondsPerRoundTripCM * 2; // ~116.6

// TCCR1B values to start Timer 1, prescale = 8
// Input Capture Edge Select (1=Rising, 0=Falling)
const uint8_t TCCR1BCaptureRisingEdge  = _BV(CS11) |  _BV(ICES1);
const uint8_t TCCR1BCaptureFallingEdge = _BV(CS11);

uint16_t RisingEdgeTime = 0;
unsigned long TimeOfLastPing = 0;

void setup()
{
  Serial.begin(115200);
  while (!Serial);

  Serial.println("Ready");

  pinMode(TriggerPin, OUTPUT);

  noInterrupts ();  // protected code
  // reset Timer 1
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  TIMSK1 = 0;

  TIFR1 = _BV(ICF1); // clear Input Capture Flag
  TCCR1B = TCCR1BCaptureRisingEdge;
  interrupts ();
}

void loop()
{
  // Is the Input Capture Register set?
  if (TIFR1 & _BV(ICF1))
  {
    uint16_t edgeTime = ICR1;
    TIFR1 = _BV(ICF1); // Clear the flag

    if (RisingEdgeTime)
    {
      // This must be the falling edge
      uint16_t elapsed = edgeTime - RisingEdgeTime;
      // Look for the next rising edge
      RisingEdgeTime = 0;
      TCCR1B = TCCR1BCaptureRisingEdge;

      // Now calculate distance:
      float cm = elapsed / HalfMicrosecondsPerRoundTripCM;
      Serial.println(cm, 3);
      tone(4, cm*10);
    }
    else
    {
      // This is the rising edge
      RisingEdgeTime = edgeTime;
      TCCR1B = TCCR1BCaptureFallingEdge;
    }
  }

  // Be sure to leave 20 to 30 milliseconds between pings so 
  // the sensor doesn't catch old echoes from the prevous ping.
  if (millis() - TimeOfLastPing >= 30)
  {
    TimeOfLastPing = millis();
    digitalWrite(TriggerPin, HIGH);
    digitalWrite(TriggerPin, LOW);
  }
}
1 Like

@johnwasser
Thank you. BTW it was your first post of similar code that drove into my skull the concept of clearing the ICF1 flag by writing a 1 . Although it stated that in the datasheet, I was hung up on the flag must to to zero when triggered.

In my current case I plan on using the sensor to measure the top down distance in my heating oil tank. Time is NOT of the essence here :slight_smile:

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.