Build an ultrasonic radar that tracks toy aircraft

A servo sweeps an ultrasonic sensor across the sky like an airport radar. A toy helicopter or plane shows up in red on the green radar screen on your computer, three LEDs show how close it is, and the buzzer...
  • Level:Advanced
  • Duration: 2 h
  • Ages 11+
  • Platform: Arduino

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build an ultrasonic radar that tracks toy aircraft – EEMENTOR STEM™ lesson

At an airport, a radar dish turns round and round, and the air traffic controllers see every aircraft as a blip on a dark screen. This capstone builds a desk-sized version. The UNO R3 swings an ultrasonic sensor from side to side with a servo and measures the distance at every angle. A program called Processing on your computer draws the classic radar screen: green lines sweep across it, and wherever the radar hits something, the line turns red. The bar at the bottom shows the angle and whether something is in range. On the breadboard, three LEDs work like a traffic light: green when the sky is clear, yellow when an aircraft comes within 40 cm, and red with a beeping buzzer when it enters the 20 cm no-fly zone. Fly a toy helicopter or plane in front of it and watch it appear.

What you'll learn

  • How a radar finds things by timing echoes at many angles
  • How to send measurements from the UNO to a computer over USB
  • How to draw a radar screen in Processing, using angles, sine and cosine
  • How to group echoes into aircraft and follow them from one sweep to the next

What you need

  • UNO R3 board and USB cable (UNO R3 Starter Kit)
  • Ultrasonic sensor HC-SR04 (UNO R3 Starter Kit)
  • Servo motor SG90 with horns and screws (UNO R3 Starter Kit)
  • Active buzzer (UNO R3 Starter Kit)
  • Green, yellow and red LEDs and 3 resistors, 220 Ω (UNO R3 Starter Kit)
  • 4 female-to-male jumper wires for the sensor
  • A toy helicopter or plane, or the printed planes and some thread
  • A computer with the free Processing program (Windows, macOS or Linux)
  • The printed parts from the 3D printing box below (servo stand, sensor bracket and toy planes), or the cardboard version described there
  • Breadboard and jumper wires (UNO R3 Starter Kit)

Wiring

Breadboard wiring diagram: Build an ultrasonic radar that tracks toy aircraft
Breadboard view. Check each connection against the wiring table below.
From To
Ultrasonic VCC 5V
Ultrasonic Trig UNO pin 10
Ultrasonic Echo UNO pin 11
Ultrasonic GND GND
Servo signal (orange) UNO pin 9
Servo power (red) and GND (brown) 5V and GND
Active buzzer + (long leg) UNO pin 8
Active buzzer − GND
UNO pin 7 220 Ω resistor, then the red LED's long leg
UNO pin 5 220 Ω resistor, then the yellow LED's long leg
UNO pin 6 220 Ω resistor, then the green LED's long leg
All LED short legs GND

Use female-to-male jumper wires between the sensor and the breadboard, and leave them loose enough for the sensor to swing from one side to the other. The UNO works at 5 V like the HC-SR04, so no voltage divider is needed here, unlike in the ESP32 radar. A buzzer module with an S pin works too: S to pin 8, + to 5V and − to GND.

Build it in stages

  1. Sweep. Wire the servo and the sensor and upload the UNO sketch. The sensor swings from side to side. Open the Serial Monitor at 9600 baud: you see pairs like 90,34, the angle and the distance.
  2. Lights. Add the LEDs and the buzzer. With nothing in front, the green LED is on. Hold your hand 30 cm in front of the sensor: yellow. At 10 cm: red, and the buzzer beeps.
  3. Radar screen. Close the Serial Monitor (only one program can use the port), install Processing and run the radar screen.
  4. Fly. Move a toy helicopter or plane slowly across the radar's view, 10 to 40 cm away.

The UNO sketch

The Servo library comes with the Arduino IDE.

#include <Servo.h>

const int SERVO_PIN = 9;
const int TRIG_PIN = 10, ECHO_PIN = 11;
const int BUZZER_PIN = 8;
const int GREEN_PIN = 6, YELLOW_PIN = 5, RED_PIN = 7;
const int MIN_ANGLE = 15, MAX_ANGLE = 165, STEP = 2;
const int RANGE_CM = 40;                   // closer than this: yellow, "in range"
const int NO_FLY_CM = 20;                  // closer than this: red and the buzzer

Servo radar;
int angle = MIN_ANGLE;
int direction = STEP;
unsigned long nearUntil = 0, alarmUntil = 0;

// Distance in cm, or 0 when there is no echo within about 1 m
int measureCm() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  long us = pulseIn(ECHO_PIN, HIGH, 6000);   // give up after 6 ms, so the sweep stays quick
  return us / 58;
}

void setup() {
  Serial.begin(9600);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  radar.attach(SERVO_PIN);
  radar.write(angle);
  delay(500);
}

void loop() {
  radar.write(angle);
  delay(30);                               // let the servo arrive and stop wobbling
  int distance = measureCm();

  Serial.print(angle);                     // one line per angle: "angle,distance"
  Serial.print(',');
  Serial.println(distance);

  // the sweep only passes the aircraft now and then, so each light stays on for 1.5 s
  if (distance > 0 && distance < RANGE_CM) nearUntil = millis() + 1500;
  if (distance > 0 && distance < NO_FLY_CM) alarmUntil = millis() + 1500;
  bool alarm = millis() < alarmUntil;
  bool near = millis() < nearUntil;
  digitalWrite(RED_PIN, alarm);
  digitalWrite(YELLOW_PIN, near && !alarm);
  digitalWrite(GREEN_PIN, !near && !alarm);
  digitalWrite(BUZZER_PIN, alarm && (millis() / 150) % 2);   // beep, beep, beep

  angle += direction;                      // turn round at both ends
  if (angle >= MAX_ANGLE || angle <= MIN_ANGLE) direction = -direction;
}

The radar screen

  1. Download Processing 4 for free from processing.org, unzip it and start it. It looks a lot like the Arduino IDE.
  2. Paste the sketch below into a new Processing window and save it as RadarScreen.
  3. Make sure the Arduino IDE's Serial Monitor is closed, then press the round Run button.
  4. The console at the bottom lists your computer's ports, numbered from 0. If the screen stays empty or you get an error, change PORT_INDEX to the number of the UNO's port (the same one as in the Arduino IDE) and run it again.
import processing.serial.*;

final int PORT_INDEX = 0;          // which port in the list is the UNO
final int RANGE_CM = 40;           // the outer ring
final int NO_FLY_CM = 20;
final boolean MIRROR = false;      // true if left and right are swapped
final boolean SHOW_LABELS = true;  // false for the plain radar screen

Serial port;
float cx, cy, radius;
int sweepAngle = 90, lastAngle = 90, lastDir = 0, lastCm = 0;
int[] seenCm = new int[181];       // what each angle saw on its latest pass (0 = nothing)
int[] seenTime = new int[181];     // when each angle was last swept (millis)
ArrayList<Aircraft> aircraft = new ArrayList<Aircraft>();
ArrayList<PVector> contacts = new ArrayList<PVector>();   // this sweep: x = angle, y = distance
float runAngles = 0, runCm = 0;
int runCount = 0;
int nextFlight = 101;

class Aircraft {
  float angle, cm, lastCm;
  int flight, missed = 0;
  Aircraft(float a, float d, int f) { angle = a; cm = d; lastCm = d; flight = f; }
}

void setup() {
  size(1200, 700);
  printArray(Serial.list());
  port = new Serial(this, Serial.list()[PORT_INDEX], 9600);
  cx = width / 2;
  cy = height - 70;
  radius = 540;
  textFont(createFont("Arial", 22));
}

void draw() {
  while (port.available() > 0) {          // read every line the UNO has sent
    String line = port.readStringUntil('\n');
    if (line == null) break;
    String[] v = split(trim(line), ',');
    if (v.length == 2) reading(int(v[0]), int(v[1]));
  }
  background(30, 30, 30);
  drawSweep();
  drawGrid();
  if (SHOW_LABELS) for (Aircraft p : aircraft) drawLabel(p);
  drawBar();
}

// ----- turning readings into aircraft -----
void reading(int a, int cm) {
  if (a < 0 || a > 180) return;
  if (MIRROR) a = 180 - a;
  int dir = a > lastAngle ? 1 : (a < lastAngle ? -1 : 0);
  if (dir != 0 && lastDir != 0 && dir != lastDir) endOfSweep();   // the radar turned round
  if (dir != 0) lastDir = dir;
  lastAngle = a;
  sweepAngle = a;
  boolean hit = cm > 0 && cm <= RANGE_CM;
  lastCm = hit ? cm : 0;
  seenCm[a] = lastCm;
  seenTime[a] = millis();
  if (hit) {
    runAngles += a;
    runCm += cm;
    runCount++;
  } else {
    closeRun();
  }
}

// Echoes at neighbouring angles belong to the same aircraft
void closeRun() {
  if (runCount >= 2) contacts.add(new PVector(runAngles / runCount, runCm / runCount));
  runAngles = 0;
  runCm = 0;
  runCount = 0;
}

// Match this sweep's contacts to the aircraft we already know
void endOfSweep() {
  closeRun();
  for (Aircraft p : aircraft) p.missed++;
  for (PVector c : contacts) {
    Aircraft best = null;
    float bestGap = 25;                   // further away than this is a new aircraft
    for (Aircraft p : aircraft) {
      float gap = dist(c.x, c.y, p.angle, p.cm);
      if (p.missed > 0 && gap < bestGap) { best = p; bestGap = gap; }
    }
    if (best == null) {
      aircraft.add(new Aircraft(c.x, c.y, nextFlight++));
    } else {
      best.lastCm = best.cm;
      best.cm = c.y;
      best.angle = c.x;
      best.missed = 0;
    }
  }
  for (int i = aircraft.size() - 1; i >= 0; i--) {
    if (aircraft.get(i).missed > 2) aircraft.remove(i);   // not seen for 3 sweeps: gone
  }
  contacts.clear();
}

// ----- drawing -----
float px(float a, float r) { return cx + cos(radians(a)) * r; }
float py(float a, float r) { return cy - sin(radians(a)) * r; }

// One line per angle: green up to the echo, red beyond it. Older lines fade out.
void drawSweep() {
  strokeWeight(9);
  for (int a = 0; a <= 180; a++) {
    if (seenTime[a] == 0) continue;
    int age = millis() - seenTime[a];
    if (age > 2500) continue;
    float alpha = map(age, 0, 2500, 230, 0);
    if (seenCm[a] > 0) {
      float r = seenCm[a] * radius / RANGE_CM;
      stroke(98, 245, 31, alpha);
      line(cx, cy, px(a, r), py(a, r));
      stroke(255, 10, 10, alpha);
      line(px(a, r), py(a, r), px(a, radius), py(a, radius));
    } else {
      stroke(98, 245, 31, alpha * 0.6);
      line(cx, cy, px(a, radius), py(a, radius));
    }
  }
  stroke(98, 245, 31);                     // the bright line of the current angle
  strokeWeight(4);
  line(cx, cy, px(sweepAngle, radius), py(sweepAngle, radius));
}

void drawGrid() {
  noFill();
  stroke(98, 245, 31);
  strokeWeight(2);
  for (int cm = 10; cm <= RANGE_CM; cm += 10) {
    float d = 2 * cm * radius / RANGE_CM;
    arc(cx, cy, d, d, PI, TWO_PI);
  }
  line(cx - radius - 20, cy, cx + radius + 20, cy);
  fill(98, 245, 31);
  textAlign(CENTER, CENTER);
  for (int a = 30; a <= 150; a += 30) {
    line(cx, cy, px(a, radius), py(a, radius));
    text(a + "°", px(a, radius + 30), py(a, radius + 30));
  }
  for (int cm = 10; cm <= RANGE_CM; cm += 10) text(cm + "cm", cx + cm * radius / RANGE_CM - 30, cy - 14);
  textAlign(LEFT, BASELINE);
}

void drawLabel(Aircraft p) {
  float r = p.cm * radius / RANGE_CM;
  float x = px(p.angle, r), y = py(p.angle, r);
  String trend = "";
  if (p.cm < p.lastCm - 1) trend = "  approaching";
  if (p.cm > p.lastCm + 1) trend = "  leaving";
  fill(p.cm < NO_FLY_CM ? color(255, 90, 90) : color(255, 225, 90));
  noStroke();
  ellipse(x, y, 12, 12);
  text("EE" + p.flight + "  " + round(p.cm) + " cm" + trend, x + 14, y - 10);
  if (p.cm < NO_FLY_CM) text("NO-FLY ZONE!", x + 14, y + 16);
}

void drawBar() {
  noStroke();
  fill(0);
  rect(0, height - 50, width, 50);
  fill(98, 245, 31);
  text("Radar System", 40, height - 16);
  text("Angle: " + sweepAngle + "°", width / 2 - 70, height - 16);
  if (lastCm > 0) text("Status: In Range (" + lastCm + " cm)", width - 330, height - 16);
  else text("Status: Out of Range", width - 330, height - 16);
}
Example screen for: Build an ultrasonic radar that tracks toy aircraft
Example radar screen with sample values. Yours updates live.

Fly your aircraft

  1. Hold a toy helicopter or plane about 30 cm in front of the radar. The yellow LED lights, and on the screen the lines turn red behind it as the sweep passes. The bottom bar says Status: In Range with the distance.
  2. Keep it there for a few sweeps: a label appears with a flight number, like EE101 30 cm. Move it slowly towards the radar: approaching. Move it back: leaving.
  3. Bring it closer than 20 cm: the red LED lights and the buzzer beeps. The airport controller has to send it away!
  4. Fly two aircraft at once, at least 15 cm apart, and watch the radar give each its own flight number. Set SHOW_LABELS to false for the plain radar screen.

How it works

A real radar sends out radio waves; this one sends out sound that is too high to hear. The HC-SR04 clicks, and the UNO times how long the echo takes to come back. Sound travels about 34 cm per millisecond, there and back, so dividing the time in microseconds by 58 gives centimetres. pulseIn() gives up after 6 ms, which is about 1 m: anything further away counts as "nothing", and the sweep never waits long.

The servo turns 2° at a time, from 15° to 165° and back. At every step, the UNO sends one line over USB: the angle, a comma and the distance. The LEDs only need that one number: closer than 40 cm is yellow, closer than 20 cm is red. Because the sweep passes the aircraft only once every few seconds, each light stays on for 1.5 seconds after the last echo.

Processing turns the angle and distance into a position on the screen with cosine and sine: cos says how far to the side, sin how far up. For every angle it remembers the latest distance and draws a line: green from the centre to the echo, red from the echo to the edge, the "shadow" behind the aircraft. Older lines fade out, so you see the sweep move.

One aircraft makes echoes at several neighbouring angles. The screen collects such a run of echoes and turns it into one contact, at the average angle and distance. When the servo turns round, a sweep is over, and each contact is matched to the nearest aircraft from the sweep before. If it is close enough, it is the same one, and comparing its old and new distance says whether it is approaching or leaving. If not, it gets a new flight number. Real air traffic radars do this too; it is called tracking.

Try this next

  • Show each aircraft's speed in cm per second, from the change in distance and the time between sweeps.
  • Play a different beep for yellow, so you can hear an aircraft coming without looking.
  • Use the 1602 LCD from the kit to show the angle and distance next to the radar, without a computer.

Troubleshooting

  • The board restarts when the servo moves: servos draw bursts of current. Use a good USB port or a powered hub, and check that the servo's red wire is on 5V.
  • The aircraft doesn't show up: the sensor sees flat surfaces best. Turn the toy so a flat side faces the radar, move it slowly, and keep it between 10 and 40 cm away.
  • Processing says "Port busy" or shows nothing: close the Arduino IDE's Serial Monitor, and check PORT_INDEX against the list in the Processing console.
  • Things appear on the wrong side: set MIRROR to true in the Processing sketch.
  • The yellow LED never goes off: something in the room is closer than 40 cm, like a wall or a cup. Clear the space in front of the radar.
  • A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

UNO R3 Starter Kit | Arduino IDE Compatible | Free Tutorials $56.00

Shop the kit