DIY Ultrasonic Rangefinder

While browsing through an online surplus catalog I spotted a rather cheap ($1.25) ultrasonic transducer.  This got me to thinking about designing a simple circuit that would perform ultrasonic ranging in a fashion similar to what I'd done previously using the Parallax "Ping" sensor.  I've been wondering for some time just how hard it would be to design my own equivalent circuit, so I ordered 4 of the transducers and set about researching the idea.  The end result, which you can learn more about by reading this page, is a something you can build for a total component cost of less than $5 and which, while not as polished as the Parallax Ping, seems to work rather well, IMHO.

The Transmit Side: To keeps things as simple and as inexpensive as possible, I decided to leverage the compute power of the attached Arduino micro controller as much as possible.  So, rather than have a dedicated pulse generation and drive circuit for the sending transducer, I simply connected it to two of the Arduino's digital I/O pins.  Connecting to two pins let me use differential drive by swinging one side high, with the other low for one half cycle, then reversing this for the other half cycle.  I was concerned that this might not provide a strong enough ping pulse to give much range but, as you can see in the photo below, the transmit pulse (lower trace on scope) is easily able to reflect off the ceiling (the top scope trace), which is 5 feet and 6 inches away, as measured from the transducers.


The ultrasonic transducers used in rangefinders like the Parallax Ping typically operate at about 40 kHz, but the surplus transducers I purchased have a lower resonant frequency of 24.50khz +/- 0.25khz (the seller has no specs on the part, but several other purchasers offered up this information in the "comments" section and I was able to confirm this experimentally.)  Using a 16 MHz Arduino "Duemilanove" I was able to approximate this frequency and generate a 4 cycle transmit pulse using the following Arduino "sketch":

#define  CYCLES  4
#define  DELAY   20
#define  PHASE1  0x08    // D3 High, D2 Low
#define  PHASE2  0x04    // D3 Low,  D2 High
#define  MASK    (PHASE1 | PHASE2)

void setup () {
}

void loop() {
  DDRD = MASK;    // PHASE1 & PHASE2 pins are outputs
  for (int ii = 0; ii < CYCLES; ii++) {
    PORTD = PHASE1;
    delayMicroseconds(DELAY);
    PORTD = PHASE2;
    delayMicroseconds(DELAY);
  }
  delay(30);
}

The code uses direct port manipulation to set the D2 & D3 data pins rather than the more familiar pinMode() and digitalWrite() calls.  Direct port mannipulation takes less overhead, which means the loop can be timed more precisely.  Since my Rigol digital scope (which I love, BTW!) can measure the frequency of a short pulse, I was able to easily tweak the loop to get very close to the needed 24.5 kHz frequency.  However, I was also able to confirm this using a $30 Extech MN36 multimeter (which has a built in frequency counter function) by simply making the loop repeat endlessly rather than sending out a short pulse.  So, you can try this method if you don't happen to have fancy scope like I do.

The Receive Side: Since the transducers are very frequency selective, the receiver circuit merely needs to provide sufficient amplification of the signal from the reflected pulse and then run the amplified signal through a simple detector circuit.  Borrowing liberally from an app note I found here, I came up with the following circuit:


As suggested in the app note, the first op amp stage (1C1A) provides about 100x gain.  The second stage (IC1B) adds another 10x of gain and then drives the diode-based detector circuit to rectify the amplified AC signal.  This builds a charge across C5 which, when the signal strength is great enough, will provide enough base-emitter current in T1 to cause it to pull the output signal low.  The two R5 and R6 resistors serve as a voltage divider to provide a synthetic ground signal to the op amps so that the whole circuit can work off a single, 5V supply from the Arduino.  The choice of the Op Amp is important because of the high, 100x gain used in the first stage.  In particular, the circuit needs an Op Amp with a gain bandwidth product of least 2.5 MHz on order to function as designed.

The circuit is fairly easy to put together on a a small protoboard mounted to an Arduino protoshield.  It's best to try and separate the two transducers a bit to reduce the local cross coupling from the transmit to receive side.  By plugging the transducers into the opposite ends of the small protoboard, I was able to get about 3/4 of an inch separation between them, but 1 inch is better if you have the room.  I also found that even small tweaks to the angular position of each transducer could significantly change how much of he transmit signal bled through to the receive side, so it is very useful to have a scope handy to watch for this.  In practice, though, you'll need to assume that some amount of cross coupling will still occur.  This means that the ranging software will need to define a timing dead zone so it can ignore these local reflections (more on this later.)

The complete parts list is shown below (approximate cost $2-3, minus cost of transducers) and all of the components are available from Mouser:

 Description
 Mouser Part #
 Quantity
 10K 1/8 watt resistor
 299-10K-RC4
 100K 1/8 watt resistor 299-100K-RC2
 1 Meg 1/8 watt resistor 299-1M-RC1
 .001 μF ceramic capacitor
 594-K102K15X7RH53L25
 .01 μF ceramic capacitor (Note 1)
 594-K103K15X7RF5TL21
 2N3904 NPN Transistor
 512-2N3904TAR1
 1N914A Fast Recovery Diode
 512-1N914A2
 MCP602-I/P Dual Op Amp (Note 2)
 MCP602-I/P
1

Note 1: the .01 μF capacitor is used for power decoupling and is not shown on the schematic.
Note 2: alternately, you can use a TI TLV272IP Dual Op Amp.

Putting all the Pieces Together: To put this circuit to use a rangefinder, we have to add a bit more code to the pulse generating test code I displayed above.  The basic steps needed can be described by the following pseudo code:
  1. Send a short pulse of ultrasound through the transmit transducer
  2. Wait a short amount of time for the local echo to die down so we don't get a false reading
  3. Loop at some defined rate until the output from T1 goes low while using the system timer to measure elapsed time
  4. When the input goes LOW, the measured elapsed time is proportional to the round trip range from the sensor to the source of the echo
  5. Optionally, calculate the distance using the speed of sound in air as a reference
Here's a simple Arduino sketch that uses the Arduino's micros() function to measures the time it takes to bounce back an echo from the emitted pulse and then print the result to the Ardiuno's serial port:

#define  CYCLES   4
#define  DELAY    20
#define  TIMEOUT  8000
#define  PHASE1   0x08    // D3 High, D2 Low
#define  PHASE2   0x04    // D3 Low,  D2 High
#define  MASK     (PHASE1 | PHASE2)
#define  T1OUT    0x10    // D4 is input from T1 OUT

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

void loop() {
  unsigned long start = micros();
  DDRD = MASK;    // PHASE1 & PHASE2 pins are outputs
  for (int ii = 0; ii < CYCLES; ii++) {
    PORTD = PHASE1;
    delayMicroseconds(DELAY);
    PORTD = PHASE2;
    delayMicroseconds(DELAY);
  }
  // delay for dead time to avoid local echo (bleed through)
  delayMicroseconds(200);
  // Measure time to receive reflected pulse
  int timeout = TIMEOUT;
  while ((PIND & T1OUT) != 0  &&  timeout > 0) {
    timeout--;
    delayMicroseconds(1);
  }
  unsigned long end = micros();
  unsigned long time;
  // Check for timer wraparound
  if (end > start)
    time = end - start;
  else
    time = (end + ~start) - (start + ~start);
  Serial.println(time, DEC);
  delay(200);
}

When I run the code, the sketch prints out a value of ~9980 which represents a round trip time of .009980 seconds.  Since sound travels at 1118 ft/sec, .009980 * 1118 equals 11.15764 feet, which is fairly close to the actual round trip distance of 11 feet.  Actually, since the speed of sound varies with air pressure, temperature and humidity, this is a pretty good measurement.

Easy Assembly with a Custom PCB: Once I had the circuit refined, I decided to create a PC board to simplify the construction of my final circuit design.  I created a two sided design so I could use ground planes for both Vcc and Gnd.  The layout of the top and bottom layers look like this: