Issue with large range values from HC-SR04 Ultrasonic sensor on Interrupt

Hi I am using a HC-SR04 Ultrasonic sensor on my toy robot car.
Mostly things work fine but the sensor periodically goes 'off-reservation' and reports ranges of 2800+ cm
Seeing the sensor is only good for about 400cm this is clearly not possible.
The issue has been hanging around for ages but now I would like to get to the bottom of the reason why.
I could just ignore the large readings but the large reading is almost always followed by a very small one eg '3'
It is the small one that causes havoc because it is interpreted as 'crash' which in the real program causes all sorts of other actions to occur.

I have seen other posts on this subject but no real explanation or ways to mitigate.
One such was that pings should be >50ms to avoid residual echos - that didn't help
There is a lot of other talk about the behaviour of interrupt code when using serial at the same time. But, to the best of my reasoning, that isn't an issue here.

The problem is readily apparent with the test code attached. Just set it up pointing to something 1.5 to 2m away then when its steady wave something in front of the sensor at about 60cm.
Something like your hand.

The test code also shows that when it goes bananas it is actually still in the ISR. ie it is waiting for pin to go Low.

Thanks for your wisdom
JC

Using Mega 2560. Ultrasonic sensor HC-SR04

/* 
 Cut down for post
*/

// Ultrasonic
const int pinUSTrig = 52;  //  - used 52 a PMW pin - OK
const int pinUSEcho = 19;  // Must be a physical Interrupt pin.  UNO use 2 or 3

// module data Global stuff
 volatile          bool UltrasonicINT_RangeFound;
 volatile unsigned long UltrasonicINT_Duration;   // 'unsigned long' because that is what micros() returns
 volatile unsigned int  UltrasonicINT_RangeCM;
                   int  UltrasonicINT_TrigPin; 
                   int  UltrasonicINT_EchoPin; 
//********************************************************************************
//****************************************************************************
// This is a REAL interrupt Service Routine (ISR) it has special rules/needs
void UltrasonicINT_EchoPinISR() {
  static unsigned long startTime;

  if (digitalRead(UltrasonicINT_EchoPin)) // Gone HIGH
    startTime = micros();
  else  {// Gone LOW
    UltrasonicINT_RangeFound = true;
    UltrasonicINT_Duration = micros() - startTime;
    UltrasonicINT_RangeCM = (UltrasonicINT_Duration / 58.0);
  }
}
//****************************************************************************
void UltrasonicINT_initialise(int Trig_pin, int Echo_pin)
{
   UltrasonicINT_TrigPin  = Trig_pin;
   UltrasonicINT_EchoPin  = Echo_pin;

   pinMode(UltrasonicINT_TrigPin, OUTPUT);
   pinMode(UltrasonicINT_EchoPin, INPUT);
   attachInterrupt(digitalPinToInterrupt(Echo_pin), UltrasonicINT_EchoPinISR, CHANGE);  // EchoPin interrupt on any change
}

// Start the measurement cycle by triggering the sonar pulse
// the return is handled by the ISR ( UltrasonicINT_EchoPinISR() )
// see the HC-SR04 data sheet for the meaning of these values
void UltrasonicINT_TriggerPing()
{
  UltrasonicINT_RangeFound = 0; // false;
  UltrasonicINT_Duration   = 0;
  UltrasonicINT_RangeCM    = 0;
  digitalWrite(UltrasonicINT_TrigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(UltrasonicINT_TrigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(UltrasonicINT_TrigPin, LOW);
}

//********************************************************************************
void setup() {
 Serial.begin(9600);
 Serial.println ("I am 'Test_module_Ultrasonic_INT2.ino'"); 
 delay(1000);
// Start the Ultrasonic
 UltrasonicINT_initialise(pinUSTrig,pinUSEcho); 

}

void loop()
{
  unsigned long starttime, endtime, waitcount;
  waitcount = 0;
  starttime = millis();

  UltrasonicINT_TriggerPing();

  while (UltrasonicINT_RangeFound == false  ) {
    waitcount++;
    endtime = millis();
  }
  Serial.print ("  waited for count of .. "); 
  Serial.print (waitcount); 
  Serial.print ("   elapsed is (ms) = "); 
  Serial.print (endtime - starttime); 
  Serial.print ("  Range (in cm) is ="); 
  Serial.println (UltrasonicINT_RangeCM); 

  delay(200);
}

Sample of output

  waited for count of .. 8519   elapsed is (ms) = 12  Range (in cm) is =195
  waited for count of .. 8520   elapsed is (ms) = 11  Range (in cm) is =195
  waited for count of .. 120923   elapsed is (ms) = 167  Range (in cm) is =2876
  waited for count of .. 479   elapsed is (ms) = 1  Range (in cm) is =3
  waited for count of .. 8572   elapsed is (ms) = 12  Range (in cm) is =196
  waited for count of .. 8477   elapsed is (ms) = 12  Range (in cm) is =194
  waited for count of .. 8497   elapsed is (ms) = 13  Range (in cm) is =195

  waited for count of .. 8513   elapsed is (ms) = 13  Range (in cm) is =195
  waited for count of .. 5505   elapsed is (ms) = 8  Range (in cm) is =123
  waited for count of .. 5416   elapsed is (ms) = 8  Range (in cm) is =121
  waited for count of .. 122477   elapsed is (ms) = 170  Range (in cm) is =2913
  waited for count of .. 484   elapsed is (ms) = 0  Range (in cm) is =3
  waited for count of .. 8514   elapsed is (ms) = 11  Range (in cm) is =195
  waited for count of .. 1346   elapsed is (ms) = 2  Range (in cm) is =24
  waited for count of .. 122525   elapsed is (ms) = 170  Range (in cm) is =2914
  waited for count of .. 484   elapsed is (ms) = 0  Range (in cm) is =3
  waited for count of .. 8535   elapsed is (ms) = 11  Range (in cm) is =195
  waited for count of .. 8517   elapsed is (ms) = 13  Range (in cm) is =195

should be volatile too

One way to mitigate is to use the median of some number of readings. The NewPing library makes that easy with its median method. Five may be a good place to start.

Another way to mitigate outliers is to ignore any measurement that is larger or smaller than the previous measurement by a some amount or ratio.

Or, ignore any measurement that is significantly larger or smaller than an extrapolated/predicted/expected value that you calculate based on a line or curve fit through the last few measurements.

Thanks Lads/Lasses; Looking some more this behaviour can be seen as a timeout issue in the sensor HC-SR04

especially post 11 and 17 to 21.
Also David Pillings wiki at HC-SR04 | David Pilling
lists several different versions of HC-SR04, all branded the same but with different chips and different behaviour.

@DaveEvans good suggestions but seeing I am using physical interrupts its going to be hard to add another layer to ping multiple times.
I was already thinking of using a weighted average to address outliners but its only going to be useful if the timeout values are removed 1st.
In its real form (not just the test code) 'get-range' is a command to the Arduino from a controlling computer running much more quickly than the Arduino.
Even the timeout values of 160 or 170ms are enough to cause problems (ie being blind) if we are moving over 1m/sec

I would really like to code If ((echo-interrupt-pin has gone-low) || (sychronised-timer has triggered))
But I have no idea how to get Interrupt Service Routines to play together.
In the mean time I think I will just introduce a 'result_valid' flag
JC

// Ultrasonic
const int pinUSTrig = 52;  //  - used 52 a PMW pin - OK
const int pinUSEcho = 19;  // Must be a physical Interrupt pin.  UNO use 2 or 3

// module data Global stuff
 volatile          bool UltrasonicINT_RangeFound;
 volatile          bool UltrasonicINT_RangeValid;
 volatile unsigned long UltrasonicINT_Duration;   // 'unsigned long' because that is what micros() returns
 volatile unsigned int  UltrasonicINT_RangeCM;
                   int  UltrasonicINT_TrigPin; 
                   int  UltrasonicINT_EchoPin; 
//********************************************************************************
//****************************************************************************
// This is a REAL interrupt Service Routine (ISR) it has special rules/needs
void UltrasonicINT_EchoPinISR() {
  volatile static unsigned long startTime;

  if (digitalRead(UltrasonicINT_EchoPin)) // Gone HIGH
    startTime = micros();
  else  {// Gone LOW
    UltrasonicINT_RangeFound = true;
    UltrasonicINT_Duration = micros() - startTime;
    UltrasonicINT_RangeCM = (UltrasonicINT_Duration / 58.0);
    if (UltrasonicINT_RangeCM > 450 || UltrasonicINT_RangeCM < 6) {
        UltrasonicINT_RangeCM = 0;
        UltrasonicINT_RangeValid = false;
    }
    else
        UltrasonicINT_RangeValid = true;
  }
}
//****************************************************************************
void UltrasonicINT_initialise(int Trig_pin, int Echo_pin)
{
   UltrasonicINT_TrigPin  = Trig_pin;
   UltrasonicINT_EchoPin  = Echo_pin;

   pinMode(UltrasonicINT_TrigPin, OUTPUT);
   pinMode(UltrasonicINT_EchoPin, INPUT);
   attachInterrupt(digitalPinToInterrupt(Echo_pin), UltrasonicINT_EchoPinISR, CHANGE);  // EchoPin interrupt on any change
}

// Start the measurement cycle by triggering the sonar pulse
// the return is handled by the ISR ( UltrasonicINT_EchoPinISR() )
// see the HC-SR04 data sheet for the meaning of these values
void UltrasonicINT_TriggerPing()
{
  UltrasonicINT_RangeFound = false;
  UltrasonicINT_RangeValid = false;
  UltrasonicINT_Duration   = 0;
  UltrasonicINT_RangeCM    = 0;
  digitalWrite(UltrasonicINT_TrigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(UltrasonicINT_TrigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(UltrasonicINT_TrigPin, LOW);
}

//********************************************************************************
void setup() {
 Serial.begin(9600);
 Serial.println ("I am 'Test_module_Ultrasonic_INT2.ino'"); 
 delay(1000);
// Start the Ultrasonic
 UltrasonicINT_initialise(pinUSTrig,pinUSEcho); 

}

void loop()
{
  unsigned long starttime, endtime, waitcount;
  waitcount = 0;
  starttime = millis();

  UltrasonicINT_TriggerPing();

  while (UltrasonicINT_RangeFound == false  ) {
    waitcount++;
    endtime = millis();
  }
  Serial.print ("  waited for count of .. "); 
  Serial.print (waitcount); 
  Serial.print ("   elapsed is (ms) = "); 
  Serial.print (endtime - starttime); 
  if (UltrasonicINT_RangeValid) {
     Serial.print ("  Range (in cm) is ="); 
     Serial.println (UltrasonicINT_RangeCM); 
  }
  else {
     Serial.println ("  Range is invalid - fire again"); 
  }
  delay(200);
}

Why do you want to use interrupts?

I agree. Interrupts are not really appropriate here. If is much simpler to poll the echo pin in the loop.
Also having a while() loop within the main loop() should be avoided but, if you must have one, it should have a timeout to prevent it getting stuck.

while (UltrasonicINT_RangeFound == false  ) {  // add a timeout condition
    waitcount++;
    endtime = millis();
  }

Probably a very simple finite state machine approach would be better, with just the following states:
start: sends the trigger, sets the start time and advances to the next state.
pending_echo: waits up to X ms for the echo. Prints the results or timeout message and returns to the state start.

Well.... long story. Basically the Arduino part of my toy is responsible for the physical things. eg All wheel movement including turns as well as 'safety' things like 'emergency stop'
So the program design is all command and event driven. For example the command 'TurnLeft 60 degrees' is broken down into 1. setting target direction, starting the wheels 2. waiting for the target heading to be reached 3. publishing the answer.
But it never actually waits. Things like 'range' are using physical interrupts but others are using millisDelay or a general timer along with a series of routines called names like
things_to_do_every_010ms(void)
a complete short one is

 void things_to_do_every_1sec(void) {
   noInterrupts();
   event_1unit      = 0;
   interrupts();
   if (G_wait_for_heartbeat_ans == true) { // then the request was sent but has not been answered o declare us 'disconnected'
       DEBUG_PRINTLN(F("Marking serial as 'dis-connected' "));
       G_is_connected = false;
   }   
}

eg in the example above 'waiting for the target heading to be reached' is coded in the things_to_do_every_010ms() and it gets current heading from IMU and compares to target.
So everything that can get handed off to hardware to handle or pseudo interrupts is handed off.
When last measured a few months ago loop() was averaging 287us to 433us - and that is under some load 'doing things'

Or to put the whole story together - because that what its currently doing and I dont want to spend the time on this module when a bandaid will fix it. BTW The main module is 2300 lines :slight_smile:

all well and good but it cant go back to the start until the unit times out. So you might have captured the timeout early - but you still have to wait it out.

If you want a separate wait for the timout, it could be something like this (untested).
It uses a finite state machine model and does not block any activities in the loop().
But I guess your problem will be integrating it with the rest of your code especially if any of it blocks for any significant time which may cause the trigger to be missed.
Anyway, you might find the approach interesting.

enum State { start, pending_echo, timeout_wait, pending_next_repeat } ;
State state ;

const int UltrasonicINT_TrigPin = 52;  //  - used 52 a PMW pin - OK
const int UltrasonicINT_EchoPin = 19;  // Must be a physical Interrupt pin.  UNO use 2 or 3

uint32_t currentStateEnteredAtMs ;
uint32_t triggeredAtUs ;

void advanceState( State newState ) {
  state = newState ;
  currentStateEnteredAtMs = millis() ;
  // add debug information here if required
}


void setup() {
  pinMode(UltrasonicINT_TrigPin, OUTPUT);
  pinMode(UltrasonicINT_EchoPin, INPUT);
  advanceState( State::start ) ;
}



void loop() {
  
  switch ( state ) {
    
    case State::start : {
        // activate trigger here
        triggeredAtUs = micros() ;
        advanceState( State::pending_echo ) ;
        break ;
      }

    case State::pending_echo : {
        // wait for echo or time out
        if ( micros() - triggeredAtUs > 1000 ) {
          // print timeout message
          advanceState( State::timeout_wait ) ;
        }
        else {
          if ( digitalRead(UltrasonicINT_EchoPin)) {
            // test echo pin and if HIGH, calculate and print distance
            advanceState( State::pending_next_repeat ) ;
          }
        }
        break ;
      }

    case State::timeout_wait : {
        if ( millis() - currentStateEnteredAtMs > 10 ) {  // wait for timeout
          advanceState( State::start ) ;
        }
        break ;
      }

    case State::pending_next_repeat : {
        if ( millis() - currentStateEnteredAtMs > 2000 ) {  // repeat interval
          advanceState( State::start ) ;
        }
        break ;
      }

  } // switch

} // loop()

The NewPing lib has a timer/interrupt method for event-driven sketches. Understand the reluctance to change horses midstream, but it may be worth a look.

Thanks, that is interesting. It could fit in with the overall multi dimensional state approach of the rest of the module.

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