HC-SR04 and nRFL01+ why is this working?

As part of my plan for world domination, I have a pair of barebones 328s and each has a HC-SR04 and a nRFL01+. Let's call one of them the TX and the other the RX.

The TX transmits a packet to the RX, and then the TX immediately triggers its HC-SR04 (which only has the transmit transducer...I removed the receive transducer). Immediately after receiving the packet, the RX triggers its HC-SR04 (which only has the receive transducer...I removed the TX transducer). The two transducers are pointed at each other.

The RX then reports the distance measured by its HC-SR04. It is remarkably accurate, as shown in the following image:
distances.jpg

I found this result surprising, because I figured that both of the HC-SR04s needed to be triggered almost exactly at the same time in order to report the correct distance. For example, if they were only 1 ms off, there would be a 34 cm error (340 m/s x 0.001s=0.34m). It seemed unlikely that they would be triggered sufficiently simultaneously due to differences in the durations of the radio.write and radio.available code.


Update: as reported in post #6, there is an almost negligible difference (36 micro-seconds) in the durations of those functions, so my concerns were not valid.


I decided to determine the difference in time between when "sonar.ping" was called by the RX and TX. To do that, I connected the uC grounds and pin 4, and added some timing code.

After a half-dozen tests, it was clear that the RX was triggering its HC-SR04 about 3 milliseconds before the TX was triggering its HC-SR04. Here's an example calculation:
times.jpg

So, it seems there ought to be a huge error due to that three millisecond difference, but there isn't. I'm stumped and would appreciate help in understanding why it works.

TX code:

// TX test

#include <SPI.h>
#include "RF24.h"
#include <NewPing.h>

//RF
const boolean radioNumber = 1; // TX=1
const byte enablePin = 7; // CE
const byte selectPin = 8; // CS
const byte addresses[][6] = {
  "1Node", "2Node"};

// HC-SR04
const byte ECHO_PIN = A5;
const byte TRIGGER_PIN = A4;
const unsigned int MAX_DISTANCE = 500; // 5 meters = approx 16 ft

// other
byte timePin = 4;
byte ledPin = 9;
unsigned long startTime,txTime,sendTime;

// objects
RF24 radio(enablePin, selectPin);  //Set up nRF24L01 radio on SPI bus plus CE and CS pins
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.

//***********************************
void setup() {
  Serial.begin(115200);
  Serial.println(F("TX"));

  pinMode(timePin, OUTPUT);
  digitalWrite(timePin, HIGH); // this pulls pin at RX uC high
  startTime=micros();          // record the time here (and at the RX uC)
  sendTime=startTime;          // nRF xmits "sendTime" contents

  pinMode(ledPin, OUTPUT);
  blinkOnce(1500);

  radio.begin();
  radio.setChannel(108);
  
  if (radioNumber) {
    radio.openWritingPipe(addresses[1]);
    radio.openReadingPipe(1, addresses[0]);
  } 
  else {
    radio.openWritingPipe(addresses[0]);
    radio.openReadingPipe(1, addresses[1]);
  }

  radio.enableDynamicAck(); // oddly, this is req'd for "no ACK" writes
  radio.stopListening();
}

//***********************************
void loop() {

  Serial.println(F("Now sending"));

  byte rWrite = radio.write( &sendTime, sizeof(unsigned long), true );  // "true" = no ACK (for future multicast)
  txTime=micros();
  byte dist = sonar.ping_cm();
  blinkOnce(100);
  sendTime=txTime;  // next loop through, send txTime

  delay(4000);

} // Loop

//***********************************
void blinkOnce(int dur) {
  digitalWrite(ledPin, HIGH);
  delay(dur);
  digitalWrite(ledPin, LOW);
  delay(dur);
}

RX code:

//RX test

// libs
#include <SPI.h>
#include "RF24.h"
#include <NewPing.h>

// RF
const boolean radioNumber = 0; // RX=0
const byte enablePin = 7; // CE
const byte selectPin = 8; // CS
const byte addresses[][6] = {
  "1Node", "2Node"};

// HC-SR04 
const byte ECHO_PIN = A5;
const byte TRIGGER_PIN = A4;
const unsigned int MAX_DISTANCE = 500; // 5 meters = approx 16 ft

// other
const byte timePin = 4;
const byte ledPin = 9;
unsigned long startTime,rxTime;

// objects
RF24 radio(enablePin, selectPin); // set up nRF24L01 radio on SPI bus plus CE and CS pins
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.

//***********************************
void setup() {
  Serial.begin(115200);
  Serial.println(F("RX"));

  pinMode(ledPin, OUTPUT);
  blinkOnce(1500);

  // start RX uC first, then
  // wait for TX to pull pin high
  // and record start time at both uC
  while (!digitalRead(timePin)) { // this will be off by one "while" loop
    startTime=micros();   
  }
  Serial.print("rx start time: ");
  Serial.println(startTime);

  radio.begin();
  radio.setChannel(108);

  if (radioNumber) {
    radio.openWritingPipe(addresses[1]);
    radio.openReadingPipe(1, addresses[0]);
  } 
  else {
    radio.openWritingPipe(addresses[0]);
    radio.openReadingPipe(1, addresses[1]);
  }

  radio.startListening();
}

//***********************************
void loop() {

  unsigned long gotTime;

  if (radio.available()) {
    rxTime=micros();              // save time RX starts ping
    byte dist = sonar.ping_cm();
    radio.read( &gotTime, sizeof(unsigned long) );
    blinkOnce(100);

    Serial.print("gotTime: ");
    Serial.print(gotTime);
    Serial.print(" 1st=tx startTime; 2nd=txTime for 1st ping");

    Serial.print(" rxTime: ");
    Serial.println(rxTime);

    Serial.print(" Ping: ");
    Serial.print(dist);
    Serial.println("cm");
  }  

} // Loop

//***********************************
void blinkOnce(int dur) {
  digitalWrite(ledPin, HIGH);
  delay(dur);
  digitalWrite(ledPin, LOW);
  delay(dur);
}

I wonder is this due to the huge difference between the speed of light and the speed of sound?

...R

Please help me understand how that could possibly explain why the RX sonar gives the correct distance even though the RX calls its sonar.ping three milliseconds earlier than the TX calls its sonar.ping.

In case you are unaware of how the HC-SR04 operates:

After the trigger input is raised the uP detects it and after some 10us powers on the MAX232. After 248us, time to ramp up the +/-10V of the charge-pump, the 8 pulses 40KHz train is produced and then the power is switched off. During this time the comparator threshold is also kept low to prevent any spurious signals to be detected in the receiver. The circuit then asserts the echo signal and waits for the echo to return. As soon as the first pulse is detected in the receiver the echo signal is deasserted and one can measure the width of the echo pulse to calculate the obstacle distance.

From this excellent website. Oops - it's actually from this one.

That is, it seems to me that the RX echo pulse will be three milliseconds longer than it should be...and hence there should be a huge error.

Hi,
It looks like the RX has to receive the TX burst, not the TX trigger, to start the distance measuring.

I think you will find that there is a TX burst delay, to enable the RX to get up and measuring.

Tom.... :slight_smile:

Thanks Tom. Yes, there's a short delay between the leading edge of the trigger and the leading edge of the echo pulse (which starts the timing). In my case the RX doesn't know it is waiting for the pulse from the TX. The RX "thinks" it is waiting for the return of its own pulse.

In order for this to work, the TX and the RX need to trigger their sonars at almost exactly the same time (via "sonar.ping"). I thought that the overhead in radio.write and radio.available would be different and result in the TX and RX triggering at substantially different times. But they don't - they trigger at very nearly the same time. See next post.

Well, apparently my original timing code was giving erroneous results.

I tried a different, simpler method (right before the TX triggers its sonar, the TX triggers a RX interrupt, and the ISR notes the time; that is compared to the time taken immediately before the RX triggers its sonar).

By that way of testing, the RX is triggering its sonar only about 36 microseconds before the TX triggers its sonar (not 2900 microseconds, as from the other timing method).

So the average error is only 12 mm, which jives with the results.

"Problem" solved (except I don't know
why my original timing code didn't work...)


The reason for the inconsistent distances is that I was moving things around...

New TX test code, FWIW:

// TX test

#include <SPI.h>
#include "RF24.h"
#include <NewPing.h>

//RF
const boolean radioNumber = 1; // TX=1
const byte enablePin = 7; // CE
const byte selectPin = 8; // CS
const byte addresses[][6] = {
  "1Node", "2Node"};

// HC-SR04
const byte ECHO_PIN = A5;
const byte TRIGGER_PIN = A4;
const unsigned int MAX_DISTANCE = 500; // 5 meters = approx 16 ft

// other
byte timePin = 2;
byte ledPin = 9;
unsigned long sendTime;

// objects
RF24 radio(enablePin, selectPin);  //Set up nRF24L01 radio on SPI bus plus CE and CS pins
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.

//***********************************
void setup() {
  Serial.begin(115200);
  Serial.println(F("TX"));

  pinMode(timePin, OUTPUT);

  pinMode(ledPin, OUTPUT);
  blinkOnce(1500);

  radio.begin();
  radio.setChannel(108);
  
  if (radioNumber) {
    radio.openWritingPipe(addresses[1]);
    radio.openReadingPipe(1, addresses[0]);
  } 
  else {
    radio.openWritingPipe(addresses[0]);
    radio.openReadingPipe(1, addresses[1]);
  }

  radio.enableDynamicAck(); // oddly, this is req'd for "no ACK" writes
  radio.stopListening();
}

//***********************************
void loop() {

  Serial.println(F("Now sending"));

  byte rWrite = radio.write( &sendTime, sizeof(unsigned long), true );  // "true" = no ACK (for future multicast)
  digitalWrite(timePin, HIGH); // pulls interrupt pin at RX uC high
  byte dist = sonar.ping_cm();
  blinkOnce(100);
  digitalWrite(timePin, LOW);

  delay(4000);

} // Loop

//***********************************
void blinkOnce(int dur) {
  digitalWrite(ledPin, HIGH);
  delay(dur);
  digitalWrite(ledPin, LOW);
  delay(dur);
}

New RX test code:

//RX test

// libs
#include <SPI.h>
#include "RF24.h"
#include <NewPing.h>

// RF
const boolean radioNumber = 0; // RX=0
const byte enablePin = 7; // CE
const byte selectPin = 8; // CS
const byte addresses[][6] = {
  "1Node", "2Node"};

// HC-SR04 
const byte ECHO_PIN = A5;
const byte TRIGGER_PIN = A4;
const unsigned int MAX_DISTANCE = 500; // 5 meters = approx 16 ft

// other
const byte ledPin = 9;
volatile unsigned long txTime;
unsigned long rxTime;

// objects
RF24 radio(enablePin, selectPin); // set up nRF24L01 radio on SPI bus plus CE and CS pins
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.

//***********************************
void setup() {
  Serial.begin(115200);
  Serial.println(F("RX"));

  pinMode(ledPin, OUTPUT);
  blinkOnce(1500);

  attachInterrupt(0, getTime, RISING);  // digital pin 2

  radio.begin();
  radio.setChannel(108);

  if (radioNumber) {
    radio.openWritingPipe(addresses[1]);
    radio.openReadingPipe(1, addresses[0]);
  } 
  else {
    radio.openWritingPipe(addresses[0]);
    radio.openReadingPipe(1, addresses[1]);
  }

  radio.startListening();
}

//***********************************
void loop() {

  unsigned long gotTime;

  if (radio.available()) {
    rxTime=micros();              // save time RX starts ping
    byte dist = sonar.ping_cm();
    radio.read( &gotTime, sizeof(unsigned long) );
    blinkOnce(100);

    Serial.print("rxTime: ");
    Serial.print(rxTime);

    Serial.print(" txTime: ");
    Serial.print(txTime);

    Serial.print(" Ping: ");
    Serial.print(dist);
    Serial.println("cm");
  }  

} // Loop

//***********************************
void blinkOnce(int dur) {
  digitalWrite(ledPin, HIGH);
  delay(dur);
  digitalWrite(ledPin, LOW);
  delay(dur);
}

//***********************************
void getTime() {
    txTime=micros();       
}

DaveEvans:
By that way of testing, the RX is triggering its sonar only about 36 microseconds before the TX triggers its sonar (not 2900 microseconds, as from the other timing method).

This was the general concept I had in mind when writing Reply #1 :slight_smile:

...R

The physics were never in question.