Obstacle-avoiding robot with a scanning ultrasonic head
- Build Logs
- 29 Jul, 2026
The standard obstacle-avoiding robot drives forward, hits something, reverses, turns a random direction and repeats. It works, in the way that a Roomba from 2004 works. Adding one servo turns it from a bumper car into something that actually surveys the room and picks the better way — and it is a ₹90 upgrade.
This build puts the ultrasonic sensor on a scanning head. Before committing to a turn, the robot looks left, looks right, and goes towards whichever has more space.
What you will need
| Part | Qty | Approx. |
|---|---|---|
| Arduino Uno R3 (compatible) | 1 | ₹418 |
| SG90 Servo + HC-SR04 Ultrasonic Combo | 1 | ₹183 |
| L298N Motor Driver Module | 1 | ₹139 |
| 100 RPM Dual Shaft BO Motor with Wheel (4 pcs) | 1 set | ₹420 |
| Acrylic Stand for Ultrasonic Sensor | 1 | ₹18 |
Roughly ₹1,180. The servo-and-sensor combo is the right buy here — the two parts are matched and the bracket problem is already solved.
How an HC-SR04 actually measures
You pulse TRIG high for 10 µs. The module emits eight 40 kHz bursts and takes ECHO high until it hears them return. You time that high pulse; sound travels at roughly 343 m/s, the sound made a round trip, so distance in cm is duration / 58.
Its limits matter for a robot:
- 2 cm to about 400 cm, and the near limit is real — closer than 2 cm the echo returns before the module is listening, and you get a nonsense reading, often a very large one. A robot that thinks a wall 1 cm away is 300 cm away drives into it.
- Roughly a 15° cone, not a laser. It reports the nearest thing anywhere in that cone, so a chair leg off to one side reads as an obstacle dead ahead.
- Soft and angled surfaces are invisible. Curtains absorb the burst. A wall at more than about 45° reflects it away like a mirror. Both return no echo at all.
That last one is why pulseIn() needs a timeout. With no echo it blocks for a full second by default, and your robot freezes mid-corridor.
Wiring
| From | To |
|---|---|
| HC-SR04 TRIG | Uno D8 |
| HC-SR04 ECHO | Uno D7 |
| HC-SR04 VCC / GND | Uno 5V / GND |
| Servo signal (orange) | Uno D3 |
| Servo VCC / GND (red / brown) | 5V supply / common GND |
| L298N IN1–IN4 | Uno D5, D6, D9, D10 |
| L298N ENA / ENB | Uno D11, D12 (remove jumpers) |
| L298N +12V / GND | Battery + / −, GND common with Uno |
The servo draws up to 700 mA when it stalls, which the Arduino's regulator cannot supply. On this build the servo only sweeps an unloaded sensor so it rarely stalls — but if the robot starts resetting during scans, that is what is happening. Feed the servo from the battery through a buck converter and keep the grounds common.
Reading distance without freezing
#define TRIG 8
#define ECHO 7
// 25ms timeout ~ 4.3m round trip. Beyond the sensor's useful range,
// so treat a timeout as "clear" rather than blocking for a full second.
const unsigned long ECHO_TIMEOUT = 25000UL;
int readDistanceCm() {
digitalWrite(TRIG, LOW);
delayMicroseconds(2);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
unsigned long duration = pulseIn(ECHO, HIGH, ECHO_TIMEOUT);
if (duration == 0) return 400; // nothing came back: assume clear
int cm = duration / 58;
if (cm < 2) return 400; // below minimum range = unreliable
return cm;
}
// Three reads, take the middle. Ultrasonic readings throw
// occasional wild outliers; a median kills them without the lag
// that averaging introduces.
int readDistanceStable() {
int a = readDistanceCm(); delay(30);
int b = readDistanceCm(); delay(30);
int c = readDistanceCm();
if (a > b) { int t = a; a = b; b = t; }
if (b > c) { int t = b; b = c; c = t; }
if (a > b) { int t = a; a = b; b = t; }
return b;
}
The median filter is worth the 60 ms. A single spurious short reading makes the robot swerve for no reason, and averaging three values would let one bad reading of 400 drag the result badly. Taking the middle value discards outliers entirely.
The scan-and-choose logic
#include <Servo.h>
Servo scanner;
const int CENTRE = 90, LEFT = 150, RIGHT = 30;
const int STOP_CM = 25; // start avoiding at this range
const int SPEED = 150;
void setup() {
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
// ... motor pinModes ...
scanner.attach(3);
scanner.write(CENTRE);
delay(500);
}
int lookAt(int angle) {
scanner.write(angle);
delay(350); // let the head actually arrive
return readDistanceStable();
}
void loop() {
int ahead = readDistanceStable();
if (ahead > STOP_CM) {
forward(SPEED);
return;
}
// Something is in the way. Stop before scanning, or the robot
// keeps closing on the obstacle while it makes up its mind.
stopMotors();
delay(200);
int leftClear = lookAt(LEFT);
int rightClear = lookAt(RIGHT);
scanner.write(CENTRE);
delay(250);
if (leftClear < STOP_CM && rightClear < STOP_CM) {
reverse(SPEED); delay(600); // boxed in
turnRight(SPEED); delay(700);
} else if (leftClear > rightClear) {
turnLeft(SPEED); delay(450);
} else {
turnRight(SPEED); delay(450);
}
stopMotors();
}
Two details carry this.
Stop before scanning. The scan takes about a second with the settle delays. A robot still rolling forward during that second has closed most of the gap by the time it decides — and will hit the obstacle while turning.
delay(350) after moving the head. An SG90 takes roughly 0.1 s per 60°, so a 60° sweep needs about 100 ms mechanically, plus settling. Read too early and you measure whatever the sensor was pointing at halfway through the sweep. This is the bug that makes a scanning robot behave worse than a non-scanning one, and it looks like bad luck rather than a timing error.
Choosing STOP_CM
It must exceed your stopping distance, which depends on speed and floor. Measure it: run the robot at your chosen PWM, cut the motors, and see how far it slides. On tile with BO motors at PWM 150 expect 10–15 cm. Set STOP_CM to roughly double that, so the robot has room to scan and turn rather than merely room to stop.
If it clips obstacles while turning, the issue is usually the 15° cone: the sensor cannot see the corner the robot is pivoting into. Two cheap IR sensors aimed diagonally forward make excellent short-range whiskers to cover exactly that blind spot.
Things that go wrong
Robot freezes for a second at a time. pulseIn() with no timeout, hitting a surface that returns no echo.
Random swerving on open floor. Unfiltered outlier readings. Add the median.
Drives straight into glass or curtains. Expected. Ultrasonic cannot see them reliably — that is a sensor-class limitation, and the fix is a second sensing modality, not more code.
Resets during a scan. Servo current pulling the rail down. Separate the servo supply.
Head sweeps but readings never change. Sensor mounted to the chassis instead of the servo horn. More common than it sounds.
Motors run flat out. ENA/ENB jumpers still on the L298N — same trap as the line follower.
Where to take it next
Sweep in 15° steps across the full 180° instead of sampling two points, store the distances in an array, and you have a crude polar map of the room — enough to steer towards the largest opening rather than merely the better of two. From there, mapping and path planning are a genuinely different class of problem, and a much more interesting final-year project than obstacle avoidance alone.
Get in touch if you want a hand sizing the motors or the battery for a heavier chassis.