ESP32 Obstacle-Avoiding Car: a robot that dodges things on its own
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.
1. Parts
| Part | Qty | Notes |
|---|---|---|
| ESP32 DevKit V1 (30 or 38 pin) | 1 | Pin names are the same on both |
| HC-SR04 ultrasonic sensor | 1 | Range roughly 2–400 cm |
| L298N motor driver | 1 | The common red dual H-bridge board |
| 2WD robot chassis kit (2 TT gear motors + caster) | 1 | |
| 2× 18650 cells + holder | 1 set | ~7.4 V; a 6× AA holder also works |
| 1 kΩ and 2 kΩ resistors | 1 each | For 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
| From | To | Notes |
|---|---|---|
| L298N ENA | ESP32 GPIO 25 | Remove the ENA jumper first |
| L298N IN1 / IN2 | GPIO 26 / 27 | |
| L298N ENB | GPIO 33 | Remove the ENB jumper first |
| L298N IN3 / IN4 | GPIO 32 / 13 | |
| OUT1 / OUT2 | Left motor | Spins backwards? Swap its two wires |
| OUT3 / OUT4 | Right motor | Same |
| Battery + / − | L298N 12V / GND | Keep the 5V-enable jumper on |
| L298N 5V | ESP32 VIN | Don't also power the motors from USB |
| L298N GND | ESP32 GND | Common ground is required |
| HC-SR04 VCC / GND | L298N 5V / GND | The sensor needs 5 V |
| HC-SR04 TRIG | ESP32 GPIO 18 | A 3.3 V trigger is fine |
| HC-SR04 ECHO | 1 kΩ → GPIO 19; GPIO 19 → 2 kΩ → GND | Divides 5 V down to ~3.3 V |
4. Set up the ESP32 toolchain
- Install Arduino IDE 2.
- File → Preferences → Additional boards manager URLs:
https://espressif.github.io/arduino-esp32/package_esp32_index.json - Boards Manager → search
esp32→ install esp32 by Espressif Systems, version 3.0 or newer. - 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);
}
Key points
ledcAttach(pin, freq, bits)is the ESP32 core 3.x API. Many older tutorials useledcSetup+ledcAttachPin, which fail to compile on 3.x — a quick way to spot an outdated guide.- Median of three readings rejects the occasional bad echo (angled surfaces, soft objects) far better than an average.
- A 25 ms
pulseIntimeout keeps a missing echo from freezing the loop.
6. Test and tune
- 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.
- Check direction: both wheels should spin forward. Swap the wires of any motor that doesn't.
- Put it on the floor.
| Problem | Change |
|---|---|
| Hits things before reacting | Lower SPEED_CRUISE or raise STOP_CM |
| Doesn't turn far enough | Raise TURN_MS |
| Motors hum but don't move | Raise 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