Robot Maker Toolkit › Radar scanner

Arduino radar scanner

A servo sweeps the ultrasonic sensor while each reading is plotted as a blip on the OLED, sweep line and all. The one that makes people stop and watch.

Intermediate · ~60 min Arduino UNO 3 libraries
✔ Matches the app, pin for pin ✔ Sketch pins checked against the wiring ◻ Bench-tested — be the first, get credited

The video walks through the wiring, how an angle and a distance become a pixel (cos, minus sin, map()), why the OLED takes half the UNO's RAM, and the servo power problem. The OLED in the video is a simulation of what this sketch draws.

Parts

Wiring

FromTo (UNO)
Servo VCC (red)5V
Servo GND (brown)GND
Servo SIG (orange)D11
HC-SR04 VCC5V
HC-SR04 GNDGND
HC-SR04 TRIGD9
HC-SR04 ECHOD10
OLED VCC5V
OLED GNDGND
OLED SDAA4 (SDA)
OLED SCLA5 (SCL)

Before you wire it

▶ Run it in your browser first

Try it before you buy a single part:

  1. Open wokwi.com/projects/new/arduino-uno
  2. Paste in sketch.ino and diagram.json from this page
  3. Add the Adafruit SSD1306 and Adafruit GFX libraries in Wokwi's Library Manager
  4. Press ▶, click the HC-SR04 and drag its distance slider while the servo sweeps

Those two files are the ones the Robot Maker Toolkit app ships for this project, byte for byte.

The sketch

Libraries: Servo (bundled with the IDE), Adafruit SSD1306, Adafruit GFX.

sketch.ino (copy everything)
/*
  Radar scanner — Robot Maker Toolkit
  Arduino UNO + SG90 servo + HC-SR04 + SSD1306 OLED

  The servo sweeps the ultrasonic sensor back and forth; each reading is
  plotted as a blip on a polar display, with a sweep line that follows the
  servo. A cheap radar you can watch working.

  PIN NOTE: the SG90 module page uses D9 for the servo signal, but the
  HC-SR04 page already uses D9 for TRIG. This project keeps the sensor on
  its documented pins and moves the servo to D11 — any digital pin can
  drive a servo, so nothing is lost.

  Libraries: Servo (bundled), Adafruit SSD1306, Adafruit GFX.
*/

#include <Servo.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const int TRIG_PIN  = 9;
const int ECHO_PIN  = 10;
const int SERVO_PIN = 11;

const int SCREEN_WIDTH = 128;
const int SCREEN_HEIGHT = 64;
const int OLED_RESET = -1;
const uint8_t OLED_ADDR = 0x3C;   // a few modules are 0x3D

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Servo sweeper;

const unsigned long ECHO_TIMEOUT_US = 25000UL;
const int MAX_RANGE_CM = 100;     // anything beyond this plots at the rim
const int STEP_DEG = 3;           // sweep resolution

// Origin of the polar plot: bottom-centre of the screen.
const int CX = SCREEN_WIDTH / 2;
const int CY = SCREEN_HEIGHT - 1;
const int R  = SCREEN_HEIGHT - 4;

// One blip per sweep column, remembered so the whole arc stays on screen.
const int SLOTS = 180 / STEP_DEG + 1;
uint8_t blip[SLOTS];              // distance in cm, 0 = nothing seen

void setup() {
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  sweeper.attach(SERVO_PIN);

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    // Nothing to draw on — blink the built-in LED so the failure is visible.
    pinMode(LED_BUILTIN, OUTPUT);
    while (true) {
      digitalWrite(LED_BUILTIN, HIGH); delay(200);
      digitalWrite(LED_BUILTIN, LOW);  delay(200);
    }
  }
  display.clearDisplay();
  display.display();
  for (int i = 0; i < SLOTS; i++) blip[i] = 0;
}

float readDistanceCm() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  unsigned long us = pulseIn(ECHO_PIN, HIGH, ECHO_TIMEOUT_US);
  if (us == 0) return -1.0;
  return (us * 0.0343) / 2.0;
}

void drawFrame(int angle) {
  display.clearDisplay();

  // Range arcs at 1/3 and 2/3 of full range.
  display.drawCircle(CX, CY, R / 3, SSD1306_WHITE);
  display.drawCircle(CX, CY, (R * 2) / 3, SSD1306_WHITE);
  display.drawCircle(CX, CY, R, SSD1306_WHITE);
  display.drawFastHLine(CX - R, CY, R * 2, SSD1306_WHITE);

  // Blips collected so far.
  for (int i = 0; i < SLOTS; i++) {
    if (blip[i] == 0) continue;
    float a = radians(i * STEP_DEG);
    int len = map(blip[i], 0, MAX_RANGE_CM, 0, R);
    int x = CX + (int)(cos(a) * len);
    int y = CY - (int)(sin(a) * len);
    display.fillCircle(x, y, 1, SSD1306_WHITE);
  }

  // Sweep line at the servo's current angle.
  float a = radians(angle);
  display.drawLine(CX, CY, CX + (int)(cos(a) * R), CY - (int)(sin(a) * R), SSD1306_WHITE);

  // Current reading, top-left.
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  int slot = angle / STEP_DEG;
  if (slot >= 0 && slot < SLOTS && blip[slot] > 0) {
    display.print(blip[slot]);
    display.print(F(" cm"));
  } else {
    display.print(F("--"));
  }

  display.display();
}

void sweep(int from, int to, int step) {
  for (int angle = from; step > 0 ? angle <= to : angle >= to; angle += step) {
    sweeper.write(angle);
    delay(40);                      // let the horn actually arrive

    float cm = readDistanceCm();
    int slot = angle / STEP_DEG;
    if (slot >= 0 && slot < SLOTS) {
      blip[slot] = (cm > 0 && cm <= MAX_RANGE_CM) ? (uint8_t)cm : 0;
    }
    drawFrame(angle);
  }
}

void loop() {
  sweep(0, 180, STEP_DEG);
  sweep(180, 0, -STEP_DEG);
}

If it doesn't work

SymptomCauseFix
Screen stays blackThe OLED is at address 0x3D rather than 0x3C.Change OLED_ADDR to 0x3D. If it's still black, run an I2C scanner to see what address actually answers.
Board resets part-way through a sweepServo stall current dragging the 5 V rail down.Power the servo from a separate 5 V supply with GND tied to the board. This is the single most common failure here.
Blips appear at the wrong angleThe sensor isn't square to the servo horn, or the horn was fitted at an offset.Re-seat the horn with the servo commanded to 90°, so the sensor faces straight ahead at mid-sweep.
The sweep is jerky and readings look staleThe 40 ms settle time isn't enough for the horn plus load to arrive.Raise the delay in sweep() to 60–80 ms. Smoother, slower — a fair trade.
Everything plots at the outer rimNothing is within MAX_RANGE_CM, so no blip is recorded.Put an object 20–60 cm in front, or raise MAX_RANGE_CM for a larger room.

This project, with the same wiring and sketch, is free in Robot Maker Toolkit, alongside wiring for 56 modules on UNO, ESP32 and Pico — all offline.

Get it onGoogle Play
Board pinout library →