Home › Projects › ESP32 Obstacle-Avoiding Car

ESP32 Obstacle-Avoiding Car: a robot that dodges things on its own

Beginner~90 minutesNo solderingESP32 core 3.x

This is the first TinyBot Forge project. By the end you will know how to measure distance with ultrasound, control motor speed with PWM, and make a robot decide what to do based on a sensor.

✓ Compile-checked on 2026-09-29 with the Arduino ESP32 board package 3.3.12 (all warnings enabled, 0 warnings), board: ESP32 Dev Module.

1. Parts

PartQtyNotes
ESP32 DevKit V1 (30 or 38 pin)1Pin names are the same on both
HC-SR04 ultrasonic sensor1Range roughly 2–400 cm
L298N motor driver1The common red dual H-bridge board
2WD robot chassis kit (2 TT gear motors + caster)1
2× 18650 cells + holder1 set~7.4 V; a 6× AA holder also works
1 kΩ and 2 kΩ resistors1 eachFor the ECHO divider — do not skip
Jumper wires, small breadboard

2. How it works

The HC-SR04 sends a 40 kHz ping and holds its ECHO pin high for as long as the sound takes to come back. Sound travels about 343 m/s, so each centimetre of distance adds about 58 µs of round trip:

distance (cm) = high time (µs) ÷ 58

Every ~70 ms the car measures. More than 20 cm of space: drive straight. Less: stop, back up, turn right — and if the right is blocked too, turn left instead.

3. Wiring

ESP32 GPIO18 TRIGGPIO19 ECHOVIN ← 5VGND GPIO25 ENAGPIO26/27 IN1/2GPIO33 ENBGPIO32/13 IN3/4 HC-SR04VCC TRIG ECHO GND DividerECHO → 1kΩ → GPIO19GPIO19 → 2kΩ → GND L298NENA IN1 IN2ENB IN3 IN412V in · 5V outGND (common) Left motor Right motor Battery 7.4 V L298N 5V → ESP32 VIN; tie all grounds together
Wiring overview. Pin-by-pin details below.
FromToNotes
L298N ENAESP32 GPIO 25Remove the ENA jumper first
L298N IN1 / IN2GPIO 26 / 27
L298N ENBGPIO 33Remove the ENB jumper first
L298N IN3 / IN4GPIO 32 / 13
OUT1 / OUT2Left motorSpins backwards? Swap its two wires
OUT3 / OUT4Right motorSame
Battery + / −L298N 12V / GNDKeep the 5V-enable jumper on
L298N 5VESP32 VINDon't also power the motors from USB
L298N GNDESP32 GNDCommon ground is required
HC-SR04 VCC / GNDL298N 5V / GNDThe sensor needs 5 V
HC-SR04 TRIGESP32 GPIO 18A 3.3 V trigger is fine
HC-SR04 ECHO1 kΩ → GPIO 19; GPIO 19 → 2 kΩ → GNDDivides 5 V down to ~3.3 V
The #1 way to damage your board: connecting HC-SR04 ECHO straight to the ESP32. ECHO is 5 V; ESP32 pins are 3.3 V only. It may "work" for a while, then the pin dies. Always use the divider.

4. Set up the ESP32 toolchain

  1. Install Arduino IDE 2.
  2. File → Preferences → Additional boards manager URLs:
    https://espressif.github.io/arduino-esp32/package_esp32_index.json
  3. Boards Manager → search esp32 → install esp32 by Espressif Systems, version 3.0 or newer.
  4. Tools → Board → ESP32 Dev Module, then pick the right serial port.

No serial port? You are probably missing the USB driver: CP2102 chips need the Silicon Labs driver, CH340 chips need the WCH driver.

5. Code

/*
 * TinyBot Forge — ESP32 Obstacle-Avoiding Car
 * https://tinybotforge.com/en/
 *
 * Hardware: ESP32 DevKit V1, HC-SR04 ultrasonic sensor, L298N motor driver, two TT DC motors
 * Toolchain: Arduino IDE 2.x + ESP32 board package 3.x (uses the new ledcAttach / ledcWrite API)
 *
 * WARNING: the HC-SR04 ECHO pin outputs 5 V, but ESP32 pins are 3.3 V only.
 *          Route ECHO through a 1 kΩ + 2 kΩ voltage divider before GPIO 19.
 */

// ---------- Pins ----------
const int PIN_TRIG = 18;   // HC-SR04 TRIG
const int PIN_ECHO = 19;   // HC-SR04 ECHO (through the divider)

const int PIN_ENA = 25;    // left motor speed (PWM)
const int PIN_IN1 = 26;    // left motor direction
const int PIN_IN2 = 27;
const int PIN_ENB = 33;    // right motor speed (PWM)
const int PIN_IN3 = 32;    // right motor direction
const int PIN_IN4 = 13;

// ---------- Tuning ----------
const int PWM_FREQ = 1000;      // 1 kHz PWM
const int PWM_BITS = 8;         // 8-bit resolution -> speed 0-255
const int SPEED_CRUISE = 170;   // straight-line speed
const int SPEED_TURN   = 160;   // turning speed
const float STOP_CM    = 20.0;  // avoid anything closer than this
const unsigned long BACK_MS = 350;  // how long to reverse
const unsigned long TURN_MS = 450;  // how long to turn

// ---------- Motors ----------
// speed: -255 (full reverse) to 255 (full forward)
void setMotor(int pinEn, int pinA, int pinB, int speed) {
  speed = constrain(speed, -255, 255);
  digitalWrite(pinA, speed > 0 ? HIGH : LOW);
  digitalWrite(pinB, speed < 0 ? HIGH : LOW);
  ledcWrite(pinEn, abs(speed));
}

void drive(int left, int right) {
  setMotor(PIN_ENA, PIN_IN1, PIN_IN2, left);
  setMotor(PIN_ENB, PIN_IN3, PIN_IN4, right);
}

void stopCar() { drive(0, 0); }

// ---------- Ultrasonic distance ----------
// Returns centimetres; 999 if nothing is in range
float readOnceCm() {
  digitalWrite(PIN_TRIG, LOW);
  delayMicroseconds(2);
  digitalWrite(PIN_TRIG, HIGH);
  delayMicroseconds(10);
  digitalWrite(PIN_TRIG, LOW);
  unsigned long us = pulseIn(PIN_ECHO, HIGH, 25000UL);  // 25 ms ≈ 4 m
  if (us == 0) return 999.0;
  return us / 58.0;  // speed of sound: ~58 µs per cm, round trip
}

// Take three readings and keep the median to reject glitches
float readDistanceCm() {
  float a = readOnceCm(); delay(10);
  float b = readOnceCm(); delay(10);
  float c = readOnceCm();
  if (a > b) { float t = a; a = b; b = t; }
  if (b > c) { float t = b; b = c; c = t; }
  if (a > b) { float t = a; a = b; b = t; }
  return b;
}

// ---------- Avoidance ----------
void avoid() {
  stopCar();
  delay(100);
  drive(-SPEED_CRUISE, -SPEED_CRUISE);   // back up
  delay(BACK_MS);

  drive(SPEED_TURN, -SPEED_TURN);        // try turning right first
  delay(TURN_MS);
  stopCar();
  delay(80);
  if (readDistanceCm() > STOP_CM * 1.5) return;

  drive(-SPEED_TURN, SPEED_TURN);        // right is blocked too: turn left for twice as long
  delay(TURN_MS * 2);
  stopCar();
  delay(80);
}

void setup() {
  Serial.begin(115200);

  pinMode(PIN_TRIG, OUTPUT);
  pinMode(PIN_ECHO, INPUT);
  pinMode(PIN_IN1, OUTPUT);
  pinMode(PIN_IN2, OUTPUT);
  pinMode(PIN_IN3, OUTPUT);
  pinMode(PIN_IN4, OUTPUT);

  ledcAttach(PIN_ENA, PWM_FREQ, PWM_BITS);
  ledcAttach(PIN_ENB, PWM_FREQ, PWM_BITS);

  stopCar();
  Serial.println("TinyBot obstacle car: starting in 2 seconds");
  delay(2000);
}

void loop() {
  float cm = readDistanceCm();
  Serial.printf("distance %.1f cm\n", cm);

  if (cm < STOP_CM) {
    avoid();
  } else {
    drive(SPEED_CRUISE, SPEED_CRUISE);
  }
  delay(40);
}
Download the .ino file

Key points

6. Test and tune

  1. Test on blocks first: lift the wheels off the table, upload, open Serial Monitor at 115200 baud and move your hand in front of the sensor.
  2. Check direction: both wheels should spin forward. Swap the wires of any motor that doesn't.
  3. Put it on the floor.
ProblemChange
Hits things before reactingLower SPEED_CRUISE or raise STOP_CM
Doesn't turn far enoughRaise TURN_MS
Motors hum but don't moveRaise the speed — TT motors usually stall below ~120

7. Troubleshooting

Distance always shows 999

No echo. Check the ECHO divider, 5 V to the sensor, and common ground.

The ESP32 resets when the motors start

The motor inrush pulls the voltage down. Use charged 18650s instead of USB, and add a 100 µF capacitor across VIN and GND.

Upload fails with "Failed to connect"

Hold the BOOT button until you see Connecting…, then release.

The car drifts to one side

TT motors are never identical. Lower the faster side's speed by 5–15.

Free: ESP32 Robot Wiring Cheat Sheet

One printable page: safe ESP32 pins, L298N, HC-SR04, servo wiring and the most common fixes.

Download the PDF