arduino intermediate 60 min

Arduino: build an obstacle-avoiding robot from scratch

A 2WD Arduino robot that drives forward and dodges what is in front of it, using an HC-SR04 on a servo and an L298N driver. The full build, wiring to behavior.

Code available for: Arduino CESP32 Arduino
Published Sep 22, 2026

The obstacle-avoider is the robot that makes people ask “wait, it decides that by itself?” Yes, it does, and the decision is three lines of code. Drive forward, ping the sensor, and if something is closer than about 25 cm, stop, look left and right with the sensor on a servo, and turn toward the side with more room. That is the whole brain. Everything else in this build is wiring and power.

I built my first one with the sensor bolted to the chassis, facing forward, and it drove confidently into everything. The trap: one forward-looking distance reading is not a view, it is a single number, and the robot needs to compare two directions before it picks one. Mount the HC-SR04 on a servo, sweep it, and the same $2 sensor becomes good enough to navigate a living room.

What you need

Needed

ItemQtyPurposeEst. cost
Arduino Uno or Nano1the brain$10-$25
2WD robot chassis kit (TT motors, wheels, battery holder)1the body; the yellow-TT kit is the standard$15
L298N motor driver module1drives the two motors from the battery$2
HC-SR04 ultrasonic sensor1distance to the nearest obstacle$2
SG90 micro servo1sweeps the sensor left and right$2
Jumper wires20connections$3
2x 18650 batteries + holder (7.4V)1motor power that survives the L298N dropout$8

Nice to have

  • Soldering iron + solder: the TT motor leads often arrive bare, and soldered joints survive vibration where twisted wires do not
  • Soldering mat and iron stand: the desk-protection pair, worth having before the first hot joint lands on the table
  • Wire stripper: custom-length motor and battery runs look better and snag less
  • Multimeter: for checking battery voltage before you blame the code for a robot that will not move
  • Helping hands: hold the servo horn while you screw the sensor to it

Wiring

Two circuits share a ground: the logic (Arduino, sensor, servo) and the power (battery, L298N). The battery must not feed the Arduino’s 5V pin; the L298N’s onboard regulator handles the 5V rail.

Wire key: VCC5VGNDTRIGD-pinECHO
HC-SR04Arduino
VCC5V
GNDGND
TRIGD2
ECHOD3
Wire key: D-pin5VGND
Wire key: D-pinGND
SG90 servoArduino
Orange (signal)D9
Red5V
BrownGND
L298NArduino / motors / power
IN1D5
IN2D6
IN3D10
IN4D11
ENA, ENBJumpers on (full speed) for the first run
Motor A (OUT1/OUT2)Left motor
Motor B (OUT3/OUT4)Right motor
+12V terminal7.4V battery pack
GND terminalBattery GND and Arduino GND (common ground, always)

Power the Arduino from the L298N’s 5V output or from USB during bench testing, never from both at once. USB plus an external 5V feed fight each other through the Arduino’s regulator.

Mount the HC-SR04 on the servo horn at the front of the chassis, facing forward, high enough that the sensor beam clears the wheels. Servo centered should point straight ahead.

Install

No libraries needed for this build. The HC-SR04 runs on raw pulseIn() timing (the same one-formula approach from the ultrasonic tutorial) and the servo runs on Servo.h, which ships with the Arduino IDE already. Nothing to install from Arduino IDE >> Sketch >> Include Library >> Manage Libraries, which is one less thing to break on a moving robot.

The code

#include <Servo.h>

const int TRIG = 2, ECHO = 3;
const int SERVO = 9;
const int IN1 = 5, IN2 = 6, IN3 = 10, IN4 = 11;
const int OBSTACLE_CM = 25;

Servo scan;

int pingCm(int angle) {
  scan.write(angle);        // point the sensor
  delay(150);               // let the servo settle
  digitalWrite(TRIG, LOW);  delayMicroseconds(2);
  digitalWrite(TRIG, HIGH); delayMicroseconds(10);
  digitalWrite(TRIG, LOW);
  long t = pulseIn(ECHO, HIGH, 30000);   // 30 ms timeout
  return t ? t / 29 / 2 : 300;           // no echo = treat as far
}

void drive(int l, int r) {   // l,r: +1 forward, -1 back, 0 stop
  digitalWrite(IN1, l > 0); digitalWrite(IN2, l < 0);
  digitalWrite(IN3, r > 0); digitalWrite(IN4, r < 0);
}

void setup() {
  pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT);
  pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  scan.attach(SERVO);
  scan.write(90);           // center
  delay(500);
}

void loop() {
  int ahead = pingCm(90);
  if (ahead > OBSTACLE_CM) { drive(1, 1); return; }   // clear: go

  drive(0, 0);                        // stop and look around
  int left  = pingCm(160);
  int right = pingCm(20);

  if (left > right && left > OBSTACLE_CM) {           // more room left
    drive(-1, 1); delay(400); drive(0, 0);            // spin left
  } else if (right > OBSTACLE_CM) {
    drive(1, -1); delay(400); drive(0, 0);            // spin right
  } else {
    drive(-1, -1); delay(500); drive(0, 0);           // boxed in: back up
  }
  scan.write(90);
  delay(100);
}

The 300 fallback when pulseIn times out is a design decision, not a shortcut: a sensor pointed at a soft object (a curtain, a couch cushion) returns no echo because ultrasound is absorbed, not reflected. Treating “no echo” as “clear” keeps the robot moving in the exact situation where a naive build freezes. (e.g. pointing the sensor at a pillow reads 300 cm every time; that is the sound being eaten, not the sensor failing.)

What you learned

  • Sweeping one sensor beats staring forward with it: two measurements give the robot a choice, and choosing is navigation.
  • The L298N’s motor supply and the Arduino’s logic supply are separate circuits with one shared ground.
  • pulseIn timeouts are error handling: no echo is data, and deciding what it means is the actual programming job.

When something breaks

  • Robot twitches, motors hum, nothing moves: battery voltage under 7V. The L298N eats about 2V of the supply as dropout, and 4x AA (6V) is not enough. Two 18650s (7.4V) fix it.
  • Robot drives backward forever: one motor wired backwards. Every TT motor polarity is arbitrary until you test it. Run the robot with wheels off the ground, confirm both spin forward on “forward”, and flip whichever motor’s leads are wrong.
  • Sensor reads fine on the desk, garbage on the robot: the servo’s current draw browns out the 5V rail during sweeps. Add a 470 uF capacitor across the servo’s power leads, or power the servo from a separate 5V buck with common ground.
  • Robot avoids walls but clips table legs: thin obstacles (chair legs, table edges) barely reflect ultrasound at a grazing angle. Slow down: turn the ENA/ENB jumpers into PWM speed control at about 180/255 and give the sensor more settle time in pingCm.
  • Resets when motors start: brownout from motor inrush on a shared power path. Keep the Arduino fed from its own regulator path and add a 100-470 uF capacitor across the L298N’s 5V rail.

What to build next

  • The line-following robot is the same chassis and driver with the sensing problem inverted: instead of avoiding what is there, follow what is.
  • The ESP32 robot base tutorial is this exact chassis on Wi-Fi, driven from a web page instead of flying solo.
  • Pair the L298N tutorial with the servo tutorial if you want the motor and sensor halves of this build explained in isolation first.
  • The 8x8 LED matrix makes a decent rear-facing mood display for a robot that backs up a lot.

The Arduino Robotics book collects the robot projects (line follower, this one, the arm) as chapters that build on each other.