Using 3 modified hc-sr04 to triangulate the position of one or more objects

I have done some tests using three modified hc-sr04 in order to check if it is possible synchronize the receivers accurately enough to determine if an object is to the left, near center, or to the right using time differences.

Using only one hc-sr04 as active transmitter/receiver and two acting as receivers, it should be possible to calculate the position of one or more objects by triangulation.
The two receivers are placed 16 cm apart and angled toward the center with a common focus about 20 - 25 centimeters in front. The T/R is mounted vertically to have both the transmitter and receiver at the center.

For processing the signal out of the amplifier, the onboard mcu is disabled when acting as receiver only.
To replace the onboard mcu, at2313a/at4323a are used with the Attinycore as core.
At2313a requires use of a non buffered simple hardware serial out to be able to print an int.

All the hc-sr04 are synchronized within a few microseconds, using the rising edge of the T/R echo pin to start the timer1 of the two receivers as well as the T/R using pinchangeinterrupt.
Even using a 16Mhz x-talclock divided by 8 takes more than 32 ms before the first overflow, sufficient for the distances in question.

On the two receivers, the onbord mcu has been disabled, unsoldering pin 9, threshold, and pin 10, signal in, order to avoid any interference from the mcu.
The at2313a/at4323a of the T/R is connected to the threshold and signal of the onboard mcu.

On the versions of the hc-sr04 used(versions with 2 transistors), the design flaw of the band pass filter, peak at around 12 Khz, has been modified to a peak closer to 40 Khz, resulting in a much better sensitivity.

The analog comparator, pin AIN0, is connected directly to the signal output of the hc-sr04 (was pin 10 of the onboard mcu).
Every time an echo pulse train starts, the analog comparator interrupt function checks if the interrupt time stamp should be saved, at the moment up to 4 timestamps.
The resolution of the timestamps is 1 usec when sent to the controller.

The controller, at the moment an arduino uno is used, only needs to send a trigger signal to the T/R, then wait around 30ms before collecting the data by sending a signal to the at2313a/at4323a, one at a time, in order to receive the timestamps using the software serial library.

The controller then processes the data to establish the position of one or more objects within the width of the transmitter beam.
A simple median filter is used to smooth up to 8 pings before processing the timestamps.

At the moment, only very simple tests are done.

Let us call the receiver to the right A, center B and left C.
At the moment triangulation is used in three ways, A-B, B-C, A-C.
(Arduino Playground - IntersectionOfCircles)

Before the triangulation, a test is made to determine if the distance A is less than C or B.
If that is the case, the object is to the right of the center.
Next test is to see if distance C is less than A or B, in that case the object is to the left of the center.
X-offset is used to move the resulting position in the x direction when calculating B-C.

Here are some test results, only for the first echo shown.

First with a wall/board in front:
A 1472usec:    25.82cm 
B 1492usec:    26.18cm 
C 1487usec:    26.09cm 
Echo: 0
Point A (0.00x,0.00y), B (8.00x,0.00y), Obj (2.86x,25.67y)
Distance AB is 8.00, X-offset 0.00
Distance A to object is 25.82
Distance B to object is 26.18
Echo: 0
Point B (8.00x,0.00y), C (8.00x,0.00y), Obj (12.29x,25.82y)
Distance BC is 8.00, X-offset 8.00
Distance B to object is 26.18
Distance C to object is 26.09
Echo: 0
Point A (0.00x,0.00y), C (16.00x,0.00y), Obj (7.57x,24.69y)
Distance AC is 16.00, X-offset 0.00
Distance A to object is 25.82
Distance C to object is 26.09

A can, 5cm in diameter, is used for the next tests.

Placed near the center:
A 1114usec:    19.54cm 
B 1111usec:    19.49cm 
C 1133usec:    19.88cm 
Echo: 0
Point A (0.00x,0.00y), B (8.00x,0.00y), Obj (4.13x,19.10y)
Distance AB is 8.00, X-offset 0.00
Distance A to object is 19.54
Distance B to object is 19.49
Echo: 0
Point A (0.00x,0.00y), C (16.00x,0.00y), Obj (7.59x,18.01y)
Distance AC is 16.00, X-offset 0.00
Distance A to object is 19.54
Distance C to object is 19.88

Near right edge:
A 1055usec:    18.51cm
B 1097usec:    19.25cm
C 1114usec:    19.54cm
Echo: 0
Point A (0.00x,0.00y), B (8.00x,0.00y), Obj (2.26x,18.37y)
Distance AB is 8.00, X-offset 0.00
Distance A to object is 18.51
Distance B to object is 19.25
Echo: 0
Point A (0.00x,0.00y), C (16.00x,0.00y), Obj (6.77x,17.23y)
Distance AC is 16.00, X-offset 0.00
Distance A to object is 18.51
Distance C to object is 19.54

Near left edge:
A 1112usec:    19.51cm
B 1092usec:    19.16cm
C 1023usec:    17.95cm
Echo: 0
Point B (8.00x,0.00y), C (8.00x,0.00y), Obj (14.81x,17.91y)
Distance BC is 8.00, X-offset 8.00
Distance B to object is 19.16
Distance C to object is 17.95
Echo: 0
Point A (0.00x,0.00y), C (16.00x,0.00y), Obj (9.83x,16.85y)
Distance AC is 16.00, X-offset 0.00
Distance A to object is 19.51
Distance C to object is 17.95

Finally, 2 cans placed left and right with the right one a little closer than the left one:
A  862usec:    15.12cm
B 1000usec:    17.54cm
C  974usec:    17.09cm
Echo: 0
Point A (0.00x,0.00y), B (8.00x,0.00y), Obj (-0.94x,15.09y)
Distance AB is 8.00, X-offset 0.00
Distance A to object is 15.12
Distance B to object is 17.54
Echo: 0
Point B (8.00x,0.00y), C (8.00x,0.00y), Obj (12.99x,16.82y)
Distance BC is 8.00, X-offset 8.00
Distance B to object is 17.54
Distance C to object is 17.09
Echo: 0
Point A (0.00x,0.00y), C (16.00x,0.00y), Obj (6.02x,13.87y)
Distance AC is 16.00, X-offset 0.00
Distance A to object is 15.12
Distance C to object is 17.09

Almost usable to avoid objects in the path.
I have searched for ways to do more accurate calculations, but so far without success.
Any suggestions?

I would not remove the processor on HC-SR04's and let them do the echo timing. I would remove the transmitter from one HC-SR04 and trigger them both with one Arduino pin. I would use Timer1 and an overflow interrupt to get a 16 MHz clock. Use CHANGE interrupts to measure the pulses on the Echo pins.

Here is a 16 MHz timer I wrote for such an occasion:

// Fast Timer
// Written by John Wasser
//
// Returns the current time in 16ths of a microsecond.
// Overflows every 268.435456 seconds.

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



void StartFastTimer()
{
  noInterrupts ();  // protected code
  // Reset Timer 1 to WGM 0, no PWM, and no clock
  TCCR1A = 0;
  TCCR1B = 0;

  TCNT1 = 0;  // Reset the counter
  TIMSK1 = 0; // Turn off all Timer1 interrupts

  // Clear the Timer1 Overflow Flag (yes, by writing 1 to it)
  // so we don't get an immediate interrupt when we enable it.
  TIFR1 = _BV(TOV1);

  TCCR1B = _BV(CS10); // start Timer 1, no prescale
  // Note: For longer intervals you could use a prescale of 8
  // to get 8 times the duration at 1/8th the resolution (1/2
  // microsecond intervals).  Set '_BV(CS11)' instead.

  TIMSK1 = _BV(TOIE1); // Timer 1 Overflow Interrupt Enable
  interrupts ();
}

volatile uint16_t Overflows = 0;

ISR(TIMER1_OVF_vect)
{
  Overflows++;
}

unsigned long FastTimer()
{
  unsigned long currentTime;
  uint16_t overflows;

  noInterrupts();
  overflows = Overflows;  // Make a local copy

  // If an overflow happened but has not been handled yet
  // and the timer count was close to zero, count the
  // overflow as part of this time.
  if ((TIFR1 & _BV(TOV1)) && (TCNT1 < 1024))
    overflows++;

  currentTime = overflows; // Upper 16 bits
  currentTime = (currentTime << 16) | TCNT1;
  interrupts();

  return currentTime;
}

// Demonstration of use
#if 1
void setup()
{
  Serial.begin(115200);
  while (!Serial);
  StartFastTimer();
}

void loop()
{
  static unsigned long previousTime = 0;
  unsigned long currentTime = FastTimer();

  Serial.println(currentTime - previousTime);
  previousTime = currentTime;

  delay(100);
}
#endif

As I recall, the hc-sr04 produces a series of 5 pulses and then begins to listen for an echo. Are you able to tell which pulse you are receiving the echo from?
Paul

The SR04 transducer is tuned to 40Khz, the receiver of the module is tuned to 38Khz. For better accuracy the sr04 can be modified to receive on 40Khz; research required on your part at this point.

Anyways fixing the sr04 receiver will increase sensitivity.

The sr04 has a terrible higher voltage regulator for the transducer. Rebuilding the transducer regulator will be of great benefit.

Only long after submitting the question and looking at the data, I realised that the triangulation works as expected.
There are still issues like delays which have not been considered for the current test setup.

I use the at2313/at4313 in order to measure the time for one or more echoes.
As the timer starts from zero when the echo pin of the transmitter/receiver rises, all three timers start
from zero at the same time, within a few clocks. As no other interrupt than the analog comparator is active, it is easy to get accurate timestamps.
Hence no need to check for overflow as it takes 32K usec using a 2 Mhz timer clock.
If an 8-bit timer is used, however, the overflow would need to be handled as you do in the FastTimer example.
Here is the sketch used as is.
It compiles for the at4313, but an additional non-buffered serial library is needed for the at2313 due to the smaller memory.

/***
ATMEL (ATTINY2313a)/4313a / ARDUINO
Pin 1 (PA2) analog input Vcc = 1, Vcc -1.2v = 0. Above 2.2v for 5v vcc to avoid reset!
                             +-\/-+
            RESET      PA2  1|    |20 Vcc 5v      
            RXD        PD0  2|    |19 PB7 SCL/SCK/PCINT17
 Data out<- TXD        PD1  3|    |18 PB6 MISO/DO/PCINT16  
            XT2/PCINT9 PA1  4|    |17 PB5 MOSI/DI/PCINT5
            XT1/PCINT8 PA0  5|    |16 PB4 OC1B/PCINT4
            INT0/PCI13 PD2  6|    |15 PB3 OC1A/PCINT3    Echo from T/X HC, start timer pos. edge
            PCI14/INT1 PD3  7|    |14 PB2 OC0A/PCINT2    Pulse in, start tx data negative edge
            PCI15/T0   PD4  8|    |13 PB1 AIN1/PCINT1    Reference voltage 10K pot + 0.1uF center tap(0.5v).
            PCI16/T1   PD5  9|    |12 PB0 AIN0/PCINT0    Signal in from HC detector output, positive 
                       GND 10|    |11 PD6 ICIPI/PCINT17
                             +----+

PB0 = In, signal from HC-sr04, positive pulses 40 Khz
PB1 = Reference voltage, adjustable.
PB2 = In, pulse in, send data via TXD.    Trig on negative edge comp. 
PB3 = In, Start timer1                    Trig on positive edge from echo positive start timer
PD1 = Tx, send data

Uses the analog comparator to save the timer count when an echo is detected.

8 mhz cpu, timer clock 1 Mhz, 16Mhz timer clock 2 Mhz
On a standard Arduino UNO execution of the user code within the ISR starts after 3,1 microsecond, 16Mhz,
8 Mhz 6.2 usec
***/

#include <PinChangeInterrupt.h>

#if defined( __AVR_ATtiny2313__ ) || defined( __AVR_ATtiny2313A__ ) 
#include <Tx313SerOut.h>      //Custom no buffer serial out only
#endif

//16Mhz clock/8 = 2 Mhz, 0.5 usec per tick. Max 32768 usec = 574.8 cm - enough
#if (F_CPU == 16000000)
#define USE_16MHZXTAL
#warning "Using 16Mhz clock"
#endif

#if defined( __AVR_ATtiny4313__ ) || defined( __AVR_ATtiny4313A__ ) 
#define SerialOut Serial
#else
Tx313SerOut SerialOut;          //at2313 has not sufficient ram for default serial print
#endif

#define ROUNDTRIP_CM 57 
#define MAX_ECHOS   4           //Adjust if memory allows.
#define TXTXT       PIN_PB2     //<- Send value(s) 
#define STARTPIN    PIN_PB3     //<- Start timer, echo out hc04 

#ifdef USE_16MHZXTAL
#define MIN_ECHO_DISTANCE ROUNDTRIP_CM * 2
#else
#define MIN_ECHO_DISTANCE ROUNDTRIP_CM     //Ignore  pulse train from transmitter
#endif

volatile uint16_t distance[MAX_ECHOS];    //Store up to 4 echos
volatile int8_t distcount;
volatile uint16_t old_clock;
volatile byte estate;       // 0-> 1 start timer, 1 = store echo time, 3 = send result(s)

//Timer runs at clock(8Mhz)/8, 1 us between ticks. up to 65 ms, 16Mhz 0.5 usec gives 32ms
ISR(TIMER1_OVF_vect)        //Timer overflow vector 
{
  //Stop timer! just in case.
  TCCR1B = 0;
}

//Start timer on positive edge from hc-sr04 echo pin
void timerStart(void) {
  distcount = 0;
  old_clock = ROUNDTRIP_CM;   //Ignore echo first few centimeters , avoid end of pulse train   
  estate = 1;                 //Timer started
  cli();
  TCNT1 = 0;      
  // Using 8Mhz, this sets timer to 1 Mhz
  // For 16mhz, 2Mhz
  TCCR1B = _BV(CS11);        
  sei(); 
}

void txoutStart(void) {
  estate = 3;                 //For send tx result
}

//Interrupt code of the comparator
// NB : you need to stabilize the power rail with a capacitor (otherwise you'll have ripples and misreading). 
ISR(ANA_COMP_vect) {
  
  //Always catch timer count?
  uint16_t clocks = TCNT1;    //16 bits, 1 mhz 60 ms = 60000 us, 2mhz 32ms no overflow
  ACSR &=  ~(1 << ACIE);
  //Ignore pulses until timer trigger has arrived or when array filled.
  if ( estate == 1 && distcount < MAX_ECHOS) {
    //Prevent too close echoes. Needed for decoding 40khz pulse first rise.
    //Pulses within delay are ignored and sets new delay
    if ( clocks > old_clock ) {  //xxx ticks, 8 pulses, min. diff. distance, approx 4 cm
      distance[distcount++] = clocks;      //Ticks from timer start

    }
    old_clock = clocks + MIN_ECHO_DISTANCE;  //Avoid saving within a pulse train.
  }
  ACSR |=  (1 << ACIE);
}

void setup() {

  pinMode(STARTPIN, INPUT_PULLUP);
  pinMode(TXTXT, INPUT_PULLUP);

  //NOTE: At2313, do not use hardwareserial and built-in Print. Buffers too big!
  SerialOut.begin( 38400 );

  //Latency 6 cycles for  PinChangeInterrupt - 0.75 us at 8 Mhz (plus enter isr ~6 us)
  //From hc-sr04 echo goes heigh when echo timing starts.
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(STARTPIN), timerStart, RISING);  
  attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(TXTXT), txoutStart, FALLING);

  cli();
  TCCR1B = 0;            //Timer stopped.
 
  OCR1B = 0x00;
  TIMSK = _BV(TOIE1);    // Enable overflow interrupt - just to stop after ~60 ms/ ~30 ms.

  PORTB &= ~(1 << PB0);  // no Pull-up on PB0
  DIDR = (1 << AIN0D | 1 << AIN1D );

  ACSR  |=
    (0 << ACD)   |       // Comparator ON
    (0 << ACBG)  |       // Disconnect 1.23V reference from AIN0 (use AIN0 and AIN1 pins)
    (0 << ACIC)  |       // input capture disabled
    (1 << ACIS1) |       // set interrupt bit on rising edge (AIN0)
    (1 << ACIS0);        // (ACIS1 and ACIS0 == 11)
  // Enable the interrupt
  ACSR |= (1 << ACIE);
  sei();                 // enable global interrupts
}

void loop() {
  if ( estate == 3 ) {
    TCCR1B = 0;          //Stop timer before transmit of data.

    for (int i = 0; i < distcount; i++ ) {
#ifdef USE_16MHZXTAL  
      SerialOut.println(distance[i]/2);      //16Mhz, 0,5 usec make it 1 usec count
#else
      SerialOut.println(distance[i]);        //8 mhz, 1us.
#endif
    }
    
    SerialOut.print(F("44444\n"));    //End marker, 44444 to avoid receiver time-out(parseInt).
    estate = 0;                       //Wait for echo timer start
  }
}

Looking at the output of the receiver using an oscilloscope, there many more than 5 pulses.
Probably due to the transmitting transducer oscillating even after the pulse train has finished.
For fairly close echoes, 20 - 30 cm, the peak voltage is around 4.8v and the pulse train lasts 1 ms or more.
The very first pulse might only have a peak of less than 1 volt, even less for fainter echoes.
At the moment, the reference voltage for the analog comparator is 0.5v, adjustable using a 10K pot.
The very first pulse might then be missed in some cases.
Experiments shall show how low the reference can be set without picking up noise.

eight pulses... HC-SR04 | David Pilling

Not to mention the numerous pulses bouncing around the room... also if the receiver has a high Q factor it will resonate itself.