Non-blocking ultrasonic sensor without interrupts and timers?

Hi,

I want to add a ultrasonic sensor to my Quadcopter. Sadly all timers are used for PWM and I want to avoid using interrupts if possible.

So I tried to mimic the behaviour of pulseIn, which is an important function for an ultrasonic sensor because they send an acoustic ping and wait for its return. So my idea was as follows:

  • Trigger ping
  • wait until echo pin is LOW, then start counter
  • when echo pin becomes HIGH (ping has returned), check time on counter and calc distance

Sadly my approach does not work and I don’t know why. Basically this is the functionality of pulseIn, I think. However, it seems I am missing some details. Or maybe the timescale is just so small that my approach does not work?

Any ideas?

My code:

/*********** Ultrasonic Sensor ******************/
#define TRIGGER_PIN  PIN6
#define ECHO_PIN     PIN5

char ultrasonicPhase = 0; // Differentiate between sending ping, waiting for echo becoming low, waiting for echo becoming high.
unsigned long ultrasonicPhaseTime = 0; // Remembers when the phase started.

unsigned long ultrasonicPulseDuration = 0; // The time between triggering the pulse and receiving the echo.
unsigned int ultrasonicDistanceInCentimeters = 0;     // The currently measured distance.
unsigned int lastUltrasonicDistanceInCentimeters = 0; // The distance before the current measured distance.

void setup() {
  Serial.begin(115200);
  Serial.println("connected");
  pinMode(TRIGGER_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
}

void loop() {
  // Update distance value
  if(ultrasonicPhase == 0) {
    // Trigger ping
    ultrasonicPhase = 1;
    digitalWrite(TRIGGER_PIN, LOW);
    delayMicroseconds(10);
    digitalWrite(TRIGGER_PIN, HIGH);
    delayMicroseconds(20);
    digitalWrite(TRIGGER_PIN, LOW);
    
    ultrasonicPhaseTime = micros();
  } else if(ultrasonicPhase == 1) {
    // Wait for echo becoming low
    if(ultrasonicPhaseTime + 10000 < micros()) {
      // Exit if echo does not become low in a given time
      Serial.println("Error1:ECHO not LOW");
      ultrasonicPhase = 0;
    } else if(digitalRead(ECHO_PIN) == LOW) {
      ultrasonicPhase = 2;
      ultrasonicPhaseTime = micros();
    }
  } else if(ultrasonicPhase == 2) {
    // Wait for echo becoming high
    if(ultrasonicPhaseTime + 500000 < micros()) {
      // Exit if echo does not become high in a given time
      Serial.println("Error2:ECHO not HIGH");
      ultrasonicPhase = 0;
    } else {
      if(digitalRead(ECHO_PIN) == HIGH) {
        ultrasonicPulseDuration = (micros() - ultrasonicPhaseTime);
        ultrasonicDistanceInCentimeters = ultrasonicPulseDuration / 58.2;
        
        ultrasonicPhase = 0;
      }
    }
  }

  // Do stuff with distance value
  if(ultrasonicDistanceInCentimeters != lastUltrasonicDistanceInCentimeters) {
    lastUltrasonicDistanceInCentimeters = ultrasonicDistanceInCentimeters;
    Serial.print(ultrasonicDistanceInCentimeters);
    Serial.println(" cm");
  }
}

What surprises me, is that the echo pin does not becoming low sometimes (error1).
That the echo pin is not becoming high (error2) is the normal case, if the ping is not reflected by a surface or the surface is to far away.

I want to avoid using interrupts if possible.

Why? The NewPing library uses interrupts, and handles them for you.

Perhaps you can use a Pin Change interrupt on whatever pin connects to Echo. An interrupt wil be the best way to get fairly accurate timing.

Ping sensor blocks for very short times and is only good up to about 4 meters?

How about use millis() to only read that a couple hundred times a second?

Have you learned the BlinkWithoutDelay sketch? You can time as many events as you can spare RAM for.

I want to avoid using interrupts if possible.

Why? The NewPing library uses interrupts, and handles them for you.

The NewPing lib is great. However, it uses the timer2 for its interrupts, which is already in use by the flight controller to do PWM for two of the motors.

@GoForSmoke: I don't get what you want to tell me? My code above is non-blocking.

I try to rephrase my question: Is there a conceptual mistake in my attempt to replace pulseIn? If not, why is the code I provided in the first post not working as pulseIn? Particularly I wonder why the error1 can occur. The pin shoud go LOW eventually because the sensor sets it to low when it triggers a ping and I trigger it in phase 0.

Yes what you do is the non-block method. My apology, good work!

Some tips:

    if(ultrasonicPhaseTime + 10000 < micros()) {
    if(ultrasonicPhaseTime + 500000 < micros()) {

Those work up to rollover that you won’t see for about 70 minutes (71.58…) and then bug.

What you want is to use rollover-proof unsigned subtraction.

    if(micros() - ultrasonicPhaseTime < 10000UL) {

The fastest loop() I run that does very light processing is still 10us to 20us.

Integers don’t have fractions. This 58.2 forces the compiler to convert to floating point (100x slower than 32-bit integer math), calculate and convert back to unsigned int. But at least it’s only once at the end of a good read so not critical.

        ultrasonicDistanceInCentimeters = ultrasonicPulseDuration / 58.2;

Here is the same result with purely integer math. Top and bottom are * 10, no decimal fraction.

        ultrasonicDistanceInCentimeters = ultrasonicPulseDuration * 10UL / 582UL;

The UL are my way of making sure the compiler uses unsigned long constants, probably don’t need but the don’t hurt and I am always suspicious of the compiler!

The digitalRead() and digitalWrire() are about 5x slower than direct port read and write. Once you have pinMode() set you can read and write port PINx and PORTx registers directly in a fraction of a microsecond.

Lastly, in the Arduino Reference page look up the switch-case statement. It should be a little faster than the

  if(ultrasonicPhase == 0) {
  } else if(ultrasonicPhase == 1) {
  } else if(ultrasonicPhase == 2) {
  }

because switch(ultrasonicPhase) only gets evaluated once.

astange: Sadly my approach does not work and I don't know why. Basically this is the functionality of pulseIn, I think. However, it seems I am missing some details. Or maybe the timescale is just so small that my approach does not work?

Any ideas?

What surprises me, is that the echo pin does not becoming low sometimes (error1). That the echo pin is not becoming high (error2) is the normal case, if the ping is not reflected by a surface or the surface is to far away.

I was trying to see if you missed a signal so I dragged out barely modified example code that worked for me years ago and maybe come up with differences.

Does this work, however blocky, with your sensor? You will have to change pin numbers or jumper holes. Why I ask is that the procedure is not quite the same as in your code.

// this code is to test LC Tech Ultrasonic Range module with Arduino
// wiring module to Arduino: (I used a Teensy++ 2.0 but a pin is a pin.)
// GND to GND, VCC to 5V, ECHO to pin 10, TRIG to pin 11

#define ECHO 10
#define TRIGGER 11

// DZ is to slow down the rate of serial prints, it is the difference
// in usecs that must be from one read to the next before print is made
#define DZ 33

unsigned long mikros, lastEcho = 0UL; // lastEcho is needed for DZ to work
unsigned long echo; // I just like data when I test, compare to micros

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

  digitalWrite( ECHO, LOW );
  pinMode( ECHO, INPUT );
  digitalWrite( TRIGGER, LOW );
  pinMode( TRIGGER, OUTPUT );
}

void loop( void )
{
  // Operation of the ultrasonic module:
  // First, trigger must be held HIGH at least 10 usecs then made LOW
  // For a short period, ECHO pin will read LOW. This must be passed.
  // I begin counting usecs after the ECHO pin changes to read HIGH.
  // Then ECHO pin will read HIGH until the echo is done, but which
  // echo pulse? There are 8 pulses sent out.

  // Also the temperature here is colder that Atmosphere Standard.
  // Beware with speed of sound based formula, the speed changes 
  // with temperature.
  
  // Under ideal conditions with ideal objects you may get 2mm accurate
  // but just point around a room or at people gives much less.
   
  digitalWrite( TRIGGER, HIGH );
  delayMicroseconds( 20 );
  digitalWrite( TRIGGER, LOW );

  while ( !( digitalRead( ECHO )));

  mikros = micros();

  //  Test distance = (high level time * sound velocity (340M/S) / 2)
  echo = 0UL;
  while ( ( digitalRead( ECHO )))
  {
    echo++;
    if ( echo > 1000000UL )
    {
      echo = 0UL;
      break;
    }
  } 

  mikros = micros() - mikros; 

  if ( echo )
  {
    if (( mikros - lastEcho > DZ ) && ( lastEcho - mikros > DZ ))
    {
      Serial.print( "loops = " );
      Serial.print( echo );
      Serial.print( " -- range = " );
      Serial.print( mikros * 170 / 1000 );
      Serial.println( "mm" );
    }
    lastEcho = mikros;
  }
  else
  {
    Serial.println( "No echo." );
    lastEcho = 0UL;
  }
  delay( 10 ); // this seems to help settle readings
}

I think that that blocky wait

while ( !( digitalRead( ECHO )));

may be extremely short. Short enough that your sketch misses it? It is the time for the module to send the 8 pulse ping after which the ECHO pin is HIGH until the ping return.

Note the ridiculous 1 second timeout.

I should dig out my ping module and speed this code up then have a shot at no-blocking it.

This might be helpfull:
This triggers 1 to 8 units maybe more haven’t tested further
Can use any pin for input or trigger!
Triggers all units at once with Trigger Pin
This sends data by Readable serial or i2c as a series of
Receives data using interrupts
Just add a second arduino

#include <Wire.h>
#define SLAVE_ADDRESS 0x2A // define slave address 0x2A = 42 
#define TriggerPin  2          // output pin all trigger pins are linked to.
#define SamplesToAverage 5     // number of ping samples to average together for use
#define DisplayTimeDelay 50     // wait this long before sending serial output
#define DelayBetweenPings 5    // delay after ping data is completely recieved all have reported in and the next trigger pulse in miliseconds (5 seems to be great)

unsigned long Timer, zTimer, xTimer, PingInterval, DisplayTimer; // Delay timers
volatile int PinArray[20];     // List of pins that could be used as ping inputs:

unsigned long PingTime[20] ; // interval [1000;2000]  Channels 9,10,11,A0,A1
volatile int ToCompleteCtr;  // when interoupted before math we need to c
volatile  unsigned long PingTimeX[20]; // interval [0;65535]  Channels 9,10,11,A0,A1
volatile  uint16_t edgeTime[20];       // time when the pin changed states to high start of timing
volatile  uint8_t PCintLast[3];        // the last state that the input pins were in
volatile  uint8_t mask[3];             //looking fo pin changes using bianary
int PinMask[3];                        // used to make sure all pings are recieved before sending another pulse.
int Measurements[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
byte *MeasurementsB;

int SerialEvent = 0;
int I2CsendDataGo = 0;
uint8_t dataB[32] ; // Max Wire library buffer size
unsigned long  i2cLive = 0;
int c, Pin;
int x = 0;
float inches, cm;
int feet;
int DiscardedSampleCtr;
// port change Interrupt
ISR(PCINT0_vect) { //this ISR pins 8~13
  static uint8_t pin;
  static unsigned long cTime;
  cTime = micros();         // micros() return a uint32_t
  pin = PINB; //  get the state of all pins bit 0 = pin 8, bit 1 = pin 9 dtc.
  ToCompleteCtr++;
  sei();                    // re enable other interrupts 
  CheckTimers(8, 13,  1, pin, cTime);
}

ISR(PCINT1_vect) { //this ISR s A0~A5
  static uint8_t pin;
  static unsigned long cTime;
  cTime = micros();         // micros() return a uint32_t
  pin = PINC; //  get the state of all pins bit 0 = pin 8, bit 1 = pin 9 dtc.
  ToCompleteCtr++;
  sei();                    // re enable other interrupts 
  CheckTimers(14, 19,  2, pin, cTime);
}

ISR(PCINT2_vect) { //this ISR pins 0-7
  static uint8_t pin;
  static unsigned long cTime;
  cTime = micros();         // micros() return a uint32_t
  pin = PIND; //  get the state of all pins bit 0 = pin 8, bit 1 = pin 9 dtc.
  ToCompleteCtr++;
  sei();                    // re enable other interrupts
  CheckTimers(0, 7,  0, pin, cTime);
}

void CheckTimers(uint8_t StartPin, uint8_t EndPin, uint8_t group, uint8_t  pin, unsigned long cTime )
{
  volatile uint16_t dTime;
  mask[group] = pin ^ PCintLast[group];   
  PCintLast[group] = pin;          // we memorize the current state of all PINs [D8-D13]
  for (uint8_t ii = 0; ii <= (EndPin - StartPin); ii++) {
    if (mask[group] >> ii & 1) { // pin has changed
      if ((pin >> ii & 1))edgeTime[(ii + StartPin)] = cTime; //Pulse went HIGH store the start time
      else { // Pulse Went low calculate the duratoin
        dTime = cTime - edgeTime[(ii + StartPin)]; // Calculate the change in time
        PingTimeX[(ii + StartPin)] = PingTimeX[(ii + StartPin)] + dTime; // Lets Store any duration up to 65535 micro seconds
      }
    }
  }
  ToCompleteCtr--;  //when all interupts are complete this will return to zero
}

// Install Pin change interrupt for a pin, can be called multiple times
void pciSetup(byte pin)
{
  if (pin <= 7)PinMask[0] =  bitWrite(PinMask[0], pin, 1); // PIND for pins 0~7
  else if (pin > 13) PinMask[2] =  bitWrite(PinMask[2] , pin - 14, 1); // PINC for A0~A5 Starts on Pin 14
  else PinMask[1] =  bitWrite(PinMask[1] , pin - 8, 1); // PINB for pins 8~13
  pinMode(pin, INPUT);// enable interrupt for pin...
  *digitalPinToPCMSK(pin) |= bit (digitalPinToPCMSKbit(pin));  // enable pin
  PCIFR  |= bit (digitalPinToPCICRbit(pin)); // clear any outstanding interrupt
  PCICR  |= bit (digitalPinToPCICRbit(pin)); // enable interrupt for the group
  PinArray[pin] = 1;
}
void serialEvent() {
  SerialEvent++;
}
void setup() {
  // initialize i2c as slave
  Wire.begin(SLAVE_ADDRESS);
  Wire.onReceive(receiveData);
  Wire.onRequest(sendData);
  Serial.begin(115200);
  Serial.println("Hello");
  int i;

  pinMode(2, OUTPUT);
  pinMode(13, OUTPUT);
  // enable interrupt for pin...
  // Select the pins you want to use for input
  // pciSetup(0); //Caution Serial Communication
  // pciSetup(1); //Caution Serial Communication
  // pciSetup(2);//This is my trigger pin
  pciSetup(3);
  pciSetup(4);
  pciSetup(5);
  pciSetup(6);
  pciSetup(7);
  pciSetup(8);
  pciSetup(9);
  pciSetup(10);
  //pciSetup(11);
  // pciSetup(12);
  // pciSetup(13);
  //pciSetup(14); // A0
  //pciSetup(15); // A1
  // pciSetup(16); // A2
  // pciSetup(17); // A3
  // pciSetup(18); // A4 // note: i2c comm
  // pciSetup(19); // A5 // note: i2c comm
  DisplayTimer = millis();
}

void loop() {
  Timer = millis(); // timing events!
  PingIt(); // Manage ping data
  if (SerialEvent) SendSerialData();
  if (I2CsendDataGo)I2CsendData();
  if ((DisplayTimer <= Timer) && (i2cLive <= Timer)) {
    DisplayTimer = Timer + DisplayTimeDelay;
    DiscardedSampleCtr = 0;
  }
}

void RadarOut( int Degrees) {
  static int x = 0;
  static bool xz = true;
  static byte SensorCount = 8;
  static byte ii = 0;
  int deg = Degrees / SensorCount;
  ii = 0;
  while (ii < 8) {

    Serial.print((ii * deg) );
    Serial.print(",");
    Serial.print(Measurements[ii]);
    Serial.print(".");
    ii++;
  }
}
void PingTrigger(int Pin) {
  digitalWrite(Pin, LOW);
  delayMicroseconds(1);
  digitalWrite(Pin, HIGH); // Trigger another pulse
  delayMicroseconds(5);
  digitalWrite(Pin, LOW);
}

bool AllClear() {
  return (!(PinMask[0] & PIND) && !(PinMask[1] & PINB) && !(PinMask[2] & PINC) && !ToCompleteCtr); //  all the input pins are LOW
}

void PingIt() {

  if ( AllClear()) { // Wait 

    if (x == 0) {
      xTimer = Timer + DelayBetweenPings; 
      x = 1; 
    }

    if (xTimer <= Timer ) { 
      if (c >= SamplesToAverage) {
        byte ii = 0;
        for (int i = 0; i <= 20; i++) if (PinArray[i]) {
            PingTime[i] =  (unsigned long) (PingTimeX[i] / c); // average 
            PingTimeX[i] = 0;
            Measurements[ii] = (int) (microsecondsToCentimeters(PingTime[i]));
            ii++;
          }
        c = 0;
        DiscardedSampleCtr++;
      }
      x = 0;
      c++;
      PingInterval = max((Timer - zTimer), PingInterval) ;
      PingInterval = min((Timer - zTimer), PingInterval) ;
      PingTrigger(TriggerPin); // Send another ping 
      zTimer = Timer;
    }
  }
}

float microsecondsToInches(long microseconds)
{
  return (float) microseconds / 74 / 2;
}

float microsecondsToCentimeters(long microseconds)
{
  return (float)microseconds / 29 / 2;
}

// callback for received data
void receiveData(int byteCount)
{
  String requestCommand = "";
  while (Wire.available())
  {
    requestCommand = requestCommand + (char)Wire.read();
  }
}
void sendData()
{
  I2CsendDataGo ++;
}
// callback for sending data
void I2CsendData()
{
  I2CsendDataGo = 0;
  i2cLive = Timer + 3000;// Stop Serial output after last i2c Request
  DataMessage();
  Wire.write(dataB, 30); // respond with message
  DiscardedSampleCtr = 0;
  digitalWrite(13, !digitalRead(13));
}
uint8_t DataMessage() {
  int x = 0;
  for (int i = 0; i < 15; i++ ) {
    dataB[x] = (uint8_t)(Measurements[i] >> 8);
    x++;
    dataB[x] = (uint8_t)Measurements[i];
    x++;
  }
}
void SendSerialData()
{
  SerialEvent = 0;
  int val = Serial.read() - '0';
  while (Serial.available())Serial.read();
  if (val == 0) {
    DataMessage();
    Serial.write(dataB, 30); // respond with message
  } else  if (val == 1) {
    RadarOut( 360);
    Serial.println();
  }
}

hears the Master i2c reciever code for my above post (Post Size limit reached)

// Wire Master Reader
// by Nicholas Zambetti <http://www.zambetti.com>

// Demonstrates use of the Wire library
// Reads data from an I2C/TWI slave device
// Refer to the "Wire Slave Sender" example for use with this

// Created 29 March 2006

// This example code is in the public domain.


#include <Wire.h>
void setup()
{
  Wire.begin();        // join i2c bus (address optional for master)
  Serial.begin(115200);  // start serial for output
}
void loop()
{
  int count = 0;
  uint16_t data[16];
  Wire.requestFrom(0x2A, 30);    // request 6 bytes from slave device #2
  bool msb = true; // starts with MSB, then LSB
  for (; Wire.available() ;) {
    if (msb) {
      data[count] = Wire.read() << 8; // first byte is bits 15-8 (MSb=15)
    }
    else {
      data[count++] |= Wire.read(); // second byte is bits 7-0 (LSb=0)
    }
    msb = !msb;
  }
  for (int i = 0; i < 15; i++) if(data[i]){
    Serial.print(data[i]);
    Serial.print("\t");
  }
  Serial.println();
  delay(10);
}

After reading GoForSmokes code I realized that I made a wrong assumption about the US sensor: I thought that the echo pin is LOW until an echo is received. But it is actually HIGH until an echo is received... So after switching these conditions in the if-statements, my initial version already works quite fine.

However, here is a more cleaned up and working version of the non-blocking ultrasonic sensor sketch:

#define TRIGGER_PIN  PIN6
#define ECHO_PIN     PIN5

enum us_sensor_phase {
  trigger, // Phase, in which the US sensor is triggered to send pings.
  sending, // Phase, in which we wait until all pings have been sent by the US sensor.
  receive  // Phase, in which we await the echo of the US sensor pings.
};

enum us_sensor_phase ultrasonicPhase = trigger; // The current phase of the sensor
unsigned long ultrasonicPhaseTime = 0;          // Remembers when the phase started.

unsigned long ultrasonicPulseDuration = 0;            // The time between triggering the pulse and receiving the echo.
unsigned int ultrasonicDistanceInCentimeters = 0;     // The currently measured distance.
unsigned int lastUltrasonicDistanceInCentimeters = 0; // The distance before the currently measured distance.


void setup() {
  Serial.begin(115200);
  Serial.println("connected");
  pinMode(TRIGGER_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
}

void loop() {
  // Update distance value
  switch(ultrasonicPhase) {
  case trigger:
    // Trigger ping
    ultrasonicPhase = sending;
    digitalWrite(TRIGGER_PIN, LOW);
    delayMicroseconds(10);
    digitalWrite(TRIGGER_PIN, HIGH);
    delayMicroseconds(20);
    digitalWrite(TRIGGER_PIN, LOW);
    ultrasonicPhaseTime = micros();
    break;
  case sending:
    // Wait until all pings have been sent by the ultrasonic sensor.
    if((micros() - ultrasonicPhaseTime) > 500UL ) {
      // Exit if pings are not sent fast enough
      Serial.println("Error1: Ping not sent");
      ultrasonicPhase = trigger;
    } else if(digitalRead(ECHO_PIN) == HIGH) {
      ultrasonicPhase = receive;
      ultrasonicPhaseTime = micros();
    }
    break;
  case receive:
    // Await the echo of a ping.
    if((micros() - ultrasonicPhaseTime) > 20000UL) {
      // Exit if no echo is received (e.g. because too far away)
      Serial.println("Error2: No echo received");
      ultrasonicPhase = trigger;
    } else if(digitalRead(ECHO_PIN) == LOW) {
      ultrasonicPulseDuration = (micros() - ultrasonicPhaseTime);
      ultrasonicDistanceInCentimeters = ultrasonicPulseDuration / 58.2;
      ultrasonicPhase = trigger;
    }
    break;
  }

  // Do stuff with distance value
  if(ultrasonicDistanceInCentimeters != lastUltrasonicDistanceInCentimeters) {
    lastUltrasonicDistanceInCentimeters = ultrasonicDistanceInCentimeters;
    Serial.print(ultrasonicDistanceInCentimeters);
    Serial.println(" cm");
  }
}

Thanks for the help :)