Brody's Builds
← All projects
Ultrasonic Distance Meter
ArduinoMedium1 hour

Ultrasonic Distance Meter

Point it at a wall and a screen shows how far away it is in feet and inches, from 1 inch up to about 13 feet.

Board: Arduino UNO (Elegoo UNO R3)

What you need

What it is

A pocket tape measure that uses sound. Aim the sensor at a wall and the screen shows the distance two ways, about three times a second:

12 ft  4.5 in
148.5 inches

If nothing is in range it says Out of range, and if something is right up against it, Too close.

How it works

  • The sensor's two "eyes" are a tiny speaker and a tiny microphone. It sends a click of sound too high to hear and times the echo.
  • Sound takes about 148 microseconds to travel one inch out and back at room temperature, so the time divided by 148 is the distance in inches.
  • Steady numbers: it takes 5 readings and uses the middle one, so one odd echo doesn't make the number jump.
  • The screen is wired so it doesn't need the Arduino's 5V or GND pins: pin A0 powers it and A1 and A2 act as ground. That leaves 5V and GND free for the sensor, and every screen pin gets its own wire.

Build it

1Upload the code

Upload the code below first (see Getting started).

2Put the screen on the breadboard

Lay the screen face up with its 16 pins in column a, rows 1 to 16 (screen pin 1, marked VSS, in row 1). The screen hangs off the edge of the breadboard. Each wire then goes from column e of a row to the Arduino.

The wiring on a full-size breadboard
The wiring on a full-size breadboard
Screen pinNameArduino
1VSSGND
2VDDA0
3V0 (contrast)9
4RS12
5RWA1
6E11
7 to 10nothing
11D45
12D54
13D63
14D72
15A (backlight +)10
16K (backlight −)A2

3Connect the sensor

Four female-to-male wires: Vcc → 5V, Trig → 7, Echo → 8, Gnd → GND.

Tip: Trig and Echo can go in 7 and 8 either way round. The Echo pin rests LOW while the sensor has power, so the program looks for that and works out which is which.

4Measure

Aim it straight at a flat wall. The sound spreads out in a cone, so anything in the way (a chair, your other hand) can answer first.

Commands

TypeWhat it does
c 40Screen contrast, 0 to 255 (lower = darker letters)
b 160Backlight brightness, 0 to 255

Both are saved, so they stay the same on battery.

If it doesn't work

  • It always says "Out of range": when I first built mine, Trig and Echo were swapped. The program now fixes that by itself, so check that all four sensor wires are firmly on and the sensor has power.
  • Numbers jump around: soft things like curtains and couches soak up sound. Aim at something hard and flat.
  • Blank screen or solid blocks: change the contrast with c 20 or c 60.

The code

Copy it into the Arduino app, or download the file. New to this? Start with Getting started.

DistanceMeter.ino · 287 lines Download
// Distance meter: point the ultrasonic sensor at a wall and the screen shows// how far away it is, in feet and inches, about 3 times a second.//// Ultrasonic sensor (HC-SR04), 4 female-to-male wires://   Vcc -> 5V     Trig -> pin 7     Echo -> pin 8     Gnd -> GND// Trig and Echo can go in pins 7 and 8 either way round: the Echo pin rests// LOW while the sensor has power, so the program looks for that and uses// whichever pin it finds as Echo.//// Screen (LCD1602), pin by pin. Pin 1 is the one marked VSS://    1 VSS -> GND         2 VDD -> A0          3 V0 -> pin 9       4 RS -> pin 12//    5 RW  -> A1          6 E   -> pin 11      7 to 10 -> nothing//   11 D4  -> pin 5      12 D5  -> pin 4      13 D6 -> pin 3      14 D7 -> pin 2//   15 A   -> pin 10     16 K   -> A2// A0 powers the screen and A1/A2 act as ground, so every screen pin gets its// own wire and the Arduino's 5V and GND pins are free for the sensor.//// The sensor works from about 1 inch to about 13 feet. Aim it straight at a// flat wall: the sound spreads out in a cone, so furniture in the way can// answer first.//// From the computer://   "c 40"   sets the screen contrast, 0 to 255 (lower = darker letters)//   "b 160"  sets the backlight brightness, 0 to 255// Both are saved, so they stay the same on battery.#include <EEPROM.h>// ---------- Pins ----------const byte SENSOR_PINS[2] = {7, 8};byte trigPin = 7;byte echoPin = 8;const byte LCD_POWER = A0;const byte LCD_RW = A1;const byte LCD_GROUND = A2;  // the backlight's K pinconst byte LCD_RS = 12;const byte LCD_E = 11;const byte LCD_DATA[4] = {5, 4, 3, 2};  // the screen's D4, D5, D6, D7const byte LCD_CONTRAST = 9;const byte LCD_LIGHT = 10;// ---------- Measuring ----------// Sound takes about 148 microseconds to go 1 inch out and back at room temperature.const float MICROS_PER_INCH = 148.0;const unsigned long NO_ECHO = 0xFFFFFFFF;const byte READINGS = 5;  // take 5 and keep the middle one, to ignore stray echoes// ---------- Saved settings ----------struct Settings {  byte check;  byte contrast;  byte backlight;};const byte SETTINGS_CHECK = 0xD5;Settings settings;void setup() {  Serial.begin(9600);  Serial.println("Distance meter starting");  EEPROM.get(0, settings);  if (settings.check != SETTINGS_CHECK) {    settings = {SETTINGS_CHECK, 40, 160};    EEPROM.put(0, settings);  }  // Pins 9 and 10 flicker on and off very fast to set contrast and brightness.  // Making them flicker faster (31,000 times a second) keeps the screen steady.  TCCR1B = (TCCR1B & 0b11111000) | 0b001;  pinMode(LCD_CONTRAST, OUTPUT);  pinMode(LCD_LIGHT, OUTPUT);  analogWrite(LCD_CONTRAST, settings.contrast);  analogWrite(LCD_LIGHT, settings.backlight);  // Ground pins first, then switch the screen's power on.  pinMode(LCD_RW, OUTPUT);  digitalWrite(LCD_RW, LOW);  pinMode(LCD_GROUND, OUTPUT);  digitalWrite(LCD_GROUND, LOW);  pinMode(LCD_POWER, OUTPUT);  digitalWrite(LCD_POWER, HIGH);  delay(100);  lcdBegin();  lcdPrintAt(0, 0, " Distance meter ");  lcdPrintAt(0, 1, "  Aim at a wall ");  findSensorPins();  delay(1000);}void loop() {  readComputer();  unsigned long echoTime = measureMicros();  if (echoTime == NO_ECHO) findSensorPins();  // in case the wires were swapped or plugged in late  // Repeat the screen's start-up routine before every update, so a screen  // that had a glitch gets back in step.  lcdWake();  if (echoTime == NO_ECHO) {    showLines("  Out of range  ", " (max ~13 feet) ");    Serial.println("Out of range");  } else if (echoTime < MICROS_PER_INCH) {    showLines("   Too close    ", "  (min 1 inch)  ");    Serial.println("Too close");  } else {    showDistance(echoTime / MICROS_PER_INCH);  }}// ---------- Measuring ----------// Works out which of pins 7 and 8 is Echo: with a pull-up on, the Echo pin// still reads LOW because the sensor holds it there; the Trig pin reads HIGH.// If it can't tell (sensor unplugged, or busy), it keeps the last choice.void findSensorPins() {  pinMode(SENSOR_PINS[0], INPUT_PULLUP);  pinMode(SENSOR_PINS[1], INPUT_PULLUP);  delay(2);  bool low0 = digitalRead(SENSOR_PINS[0]) == LOW;  bool low1 = digitalRead(SENSOR_PINS[1]) == LOW;  if (low0 != low1) {    echoPin = low0 ? SENSOR_PINS[0] : SENSOR_PINS[1];    trigPin = low0 ? SENSOR_PINS[1] : SENSOR_PINS[0];  }  pinMode(echoPin, INPUT);  pinMode(trigPin, OUTPUT);  digitalWrite(trigPin, LOW);}// Measures 5 times and returns the middle echo time, in microseconds.unsigned long measureMicros() {  unsigned long times[READINGS];  for (byte i = 0; i < READINGS; i++) {    times[i] = echoMicros();    delay(60);  // the sensor needs a short rest between measurements  }  // Sort them, smallest first.  for (byte i = 1; i < READINGS; i++) {    for (byte j = i; j > 0 && times[j] < times[j - 1]; j--) {      unsigned long swap = times[j];      times[j] = times[j - 1];      times[j - 1] = swap;    }  }  return times[READINGS / 2];}// Sends out a click of sound and times how long the echo takes to come back.// Returns NO_ECHO if nothing came back within about 16 feet.unsigned long echoMicros() {  digitalWrite(trigPin, LOW);  delayMicroseconds(2);  digitalWrite(trigPin, HIGH);  delayMicroseconds(10);  digitalWrite(trigPin, LOW);  unsigned long microseconds = pulseIn(echoPin, HIGH, 30000);  return microseconds == 0 ? NO_ECHO : microseconds;}// ---------- Showing it ----------// Shows e.g.   12 ft  4.5 in//              148.5 inchesvoid showDistance(float inches) {  long tenths = lround(inches * 10);  // whole distance in tenths of an inch  int feet = tenths / 120;  int leftoverTenths = tenths % 120;  char top[17], bottom[17], number[8];  snprintf(top, sizeof(top), "%2d ft %2d.%d in", feet, leftoverTenths / 10, leftoverTenths % 10);  dtostrf(tenths / 10.0, 5, 1, number);  snprintf(bottom, sizeof(bottom), "%s inches", number);  showLines(top, bottom);  Serial.print(top);  Serial.print("   (");  Serial.print(bottom);  Serial.println(")");}// Puts one line on each row, centered, blanking out anything left over.void showLines(const char *top, const char *bottom) {  const char *lines[2] = {top, bottom};  for (byte row = 0; row < 2; row++) {    char line[17];    byte length = strlen(lines[row]);    if (length > 16) length = 16;    byte left = (16 - length) / 2;    memset(line, ' ', 16);    memcpy(line + left, lines[row], length);    line[16] = '\0';    lcdPrintAt(0, row, line);  }}// ---------- Settings from the computer ----------void readComputer() {  static char line[20];  static byte length = 0;  while (Serial.available()) {    char ch = Serial.read();    if (ch == '\n' || ch == '\r') {      line[length] = '\0';      if (length > 0) handleCommand(line);      length = 0;    } else if (length < sizeof(line) - 1) {      line[length++] = ch;    }  }}void handleCommand(const char *line) {  int value = constrain(atoi(line + 1), 0, 255);  if (line[0] == 'c') {    settings.contrast = value;    analogWrite(LCD_CONTRAST, settings.contrast);  } else if (line[0] == 'b') {    settings.backlight = value;    analogWrite(LCD_LIGHT, settings.backlight);  } else {    return;  }  EEPROM.put(0, settings);  Serial.print("Saved: contrast ");  Serial.print(settings.contrast);  Serial.print(", backlight ");  Serial.println(settings.backlight);}// ---------- Screen ----------// The screen takes letters 4 bits at a time on its D4-D7 pins. RS picks// between a command (LOW) and a letter (HIGH), and a pulse on E sends it.void lcdBegin() {  pinMode(LCD_RS, OUTPUT);  pinMode(LCD_E, OUTPUT);  for (byte i = 0; i < 4; i++) pinMode(LCD_DATA[i], OUTPUT);  digitalWrite(LCD_E, LOW);  digitalWrite(LCD_RS, LOW);  delay(50);  // give the screen time to wake up  lcdWake();  lcdSend(0x01, false);  // clear the screen  delay(2);}// The screen's start-up routine: whatever state it's in, this switches it// into 4-bit mode with 2 lines of text. It doesn't clear the screen, so it// can be repeated without any flicker.void lcdWake() {  digitalWrite(LCD_RS, LOW);  lcdNibble(0x3);  delay(5);  lcdNibble(0x3);  delayMicroseconds(150);  lcdNibble(0x3);  delayMicroseconds(150);  lcdNibble(0x2);  delayMicroseconds(150);  lcdSend(0x28, false);  // 2 lines of text  lcdSend(0x0C, false);  // screen on, no cursor  lcdSend(0x06, false);  // move right after each letter}void lcdNibble(byte bits) {  for (byte i = 0; i < 4; i++) digitalWrite(LCD_DATA[i], (bits >> i) & 1);  digitalWrite(LCD_E, HIGH);  delayMicroseconds(1);  digitalWrite(LCD_E, LOW);  delayMicroseconds(100);}void lcdSend(byte value, bool isLetter) {  digitalWrite(LCD_RS, isLetter ? HIGH : LOW);  lcdNibble(value >> 4);  lcdNibble(value & 0x0F);}void lcdPrintAt(byte column, byte row, const char *text) {  lcdSend(0x80 | (column + (row ? 0x40 : 0)), false);  while (*text) lcdSend(*text++, true);}

Related projects