The difference between a robotics project that works on the bench and one that survives the living room floor is almost always power delivery. When you spin up two DC motors while simultaneously sweeping a servo and polling an ultrasonic sensor, current spikes will collapse your voltage rails and reset your microcontroller. This guide walks through building a robust 2WD obstacle-avoiding rover using the ESP32, focusing heavily on the power architecture, exact pinouts, and the debugging steps required when the inevitable brownout occurs.
Estimated Build Time: 2.5 hours
Target Board: ESP32 DevKit V1 (ESP32-WROOM-32 module)
Bill of Materials & Power Spec Sheet
Before cutting wires, you need to understand the current budget. The ESP32-WROOM-32 draws roughly 240mA during peak WiFi transmission, while stalled TT motors can pull over 1A each. If your power source cannot handle transient spikes, the ESP32's internal brownout detector will trigger a reset. Below is the exact hardware list and the real-world power data you need to size your battery pack.
| Component | Exact Model / Variant | Nominal Voltage | Peak Current | Notes & Cost (Approx) |
|---|---|---|---|---|
| Microcontroller | ESP32 DevKit V1 (30-pin) | 5V (USB) / 3.3V (Logic) | 240 mA | Ensure it's the WROOM-32 variant. ($6) |
| Motor Driver | L298N Dual H-Bridge | 5V - 12V (Logic: 5V) | 2.0 A (per channel) | Bipolar; drops ~2V. Keep onboard 5V jumper removed. ($4) |
| Drive Motors | 130-size TT Gear Motors (x2) | 3V - 6V | 1.2 A (stall) | Yellow plastic chassis motors. Noisy, but cheap. ($3/pair) |
| Steering Servo | TowerPro MG90S (Metal Gear) | 4.8V - 6.0V | 700 mA (stall) | Metal gears prevent stripping on impacts. ($5) |
| Distance Sensor | HC-SR04 Ultrasonic | 5V | 15 mA | Requires 5V logic for reliable echo returns. ($2) |
| Power Source | 2S LiPo (7.4V) 1500mAh | 7.4V - 8.4V | 20C+ Discharge | Must handle 3A+ transient spikes. ($12) |
Pin Mapping & Wiring Steps
The ESP32 DevKit V1 has specific GPIO restrictions. GPIOs 0, 2, 12, and 15 have boot-strapping requirements that can cause the board to hang if pulled high or low during startup. Furthermore, the ADC2 pins cannot be used when WiFi is active. The mapping below avoids these traps, utilizing only safe, output-capable pins for motor control and input-capable pins for the sensor.
| ESP32 GPIO | Component | Function | Notes |
|---|---|---|---|
| GPIO 14 | L298N ENA | PWM Motor A Speed | Safe for PWM output |
| GPIO 27 | L298N IN1 | Motor A Direction 1 | Digital OUT |
| GPIO 26 | L298N IN2 | Motor A Direction 2 | Digital OUT |
| GPIO 12 | L298N ENB | PWM Motor B Speed | Must be LOW on boot |
| GPIO 25 | L298N IN3 | Motor B Direction 1 | Digital OUT (DAC capable) |
| GPIO 33 | L298N IN4 | Motor B Direction 2 | Digital OUT (Input only on some clones, verify) |
| GPIO 13 | MG90S Servo Signal | PWM Servo Control | Safe for PWM output |
| GPIO 5 | HC-SR04 Trig | Ultrasonic Trigger | Digital OUT |
| GPIO 18 | HC-SR04 Echo | Ultrasonic Echo Return | 5V tolerant on most DevKits, but use voltage divider if unsure |
- Power the L298N: Connect the 2S LiPo positive to the L298N 12V terminal and LiPo negative to the L298N GND terminal. Remove the 5V enable jumper on the L298N—we will not use its onboard linear regulator to power the ESP32, as motor noise will cause resets.
- Establish Common Ground: Run a wire from the L298N GND terminal directly to one of the ESP32 GND pins. Without a shared ground reference, the ESP32's 3.3V logic signals will float relative to the L298N, causing erratic motor behavior or dead shorts.
- Power the ESP32: For initial bench testing, power the ESP32 via its micro-USB port. For untethered operation, use a dedicated 5V UBEC (Universal Battery Elimination Circuit) step-down converter connected to the LiPo, feeding 5V into the ESP32's 5V/VIN pin.
- Wire the HC-SR04: The HC-SR04 requires 5V to operate reliably. Power it from the L298N's 5V output terminal (which is now just a passthrough if you have an external 5V source, or use the UBEC 5V rail). Use a voltage divider (two 1k resistors) on the Echo pin if your specific ESP32 clone lacks 5V tolerant GPIOs.
Complete ESP32 Rover Control Code
This code targets the ESP32 DevKit V1 running Arduino IDE with the ESP32 Core v2.x+. It uses the NewPing library to handle ultrasonic sensor timeouts gracefully. Standard pulseIn() functions will hang the ESP32's watchdog timer if an echo is never received (e.g., when pointed at sound-absorbing curtains), crashing the rover. NewPing uses timer interrupts to prevent this lockup.
Prerequisite: Install the "NewPing" and "ESP32Servo" libraries via the Arduino Library Manager.
#include <NewPing.h>
#include <ESP32Servo.h>
// --- PIN DEFINITIONS ---
#define TRIG_PIN 5
#define ECHO_PIN 18
#define SERVO_PIN 13
#define ENA 14
#define IN1 27
#define IN2 26
#define ENB 12
#define IN3 25
#define IN4 33
// --- SYSTEM CONSTANTS ---
#define MAX_DISTANCE 200 // Maximum ping distance in cm
#define STOP_DISTANCE 20 // Distance to trigger avoidance maneuver
#define MOTOR_SPEED 200 // PWM value (0-255)
NewPing sonar(TRIG_PIN, ECHO_PIN, MAX_DISTANCE);
Servo steeringServo;
void setup() {
Serial.begin(115200);
Serial.println("ESP32 Rover Initializing...");
// Configure Motor Pins
pinMode(ENA, OUTPUT);
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(ENB, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
// Initialize Motors to STOP
stopMotors();
// Attach Servo and center it
steeringServo.attach(SERVO_PIN);
steeringServo.write(90);
delay(500); // Allow servo to reach center position
Serial.println("System Ready. Starting navigation loop.");
}
void loop() {
// NewPing returns 0 if no echo is received (timeout handled internally)
unsigned int distance = sonar.ping_cm();
// Error Handling: If ping returns 0, treat it as an obstacle to be safe
if (distance == 0 || distance < STOP_DISTANCE) {
Serial.print("Obstacle detected or timeout. Distance: ");
Serial.println(distance);
avoidObstacle();
} else {
Serial.print("Path clear. Distance: ");
Serial.println(distance);
driveForward();
}
delay(50); // 20Hz update rate
}
// --- MOTOR CONTROL FUNCTIONS ---
void driveForward() {
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
analogWrite(ENA, MOTOR_SPEED);
digitalWrite(IN3, HIGH);
digitalWrite(IN4, LOW);
analogWrite(ENB, MOTOR_SPEED);
}
void stopMotors() {
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
analogWrite(ENA, 0);
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
analogWrite(ENB, 0);
}
void avoidObstacle() {
stopMotors();
delay(200);
// Scan Left
steeringServo.write(30);
delay(400);
unsigned int leftDist = sonar.ping_cm();
// Scan Right
steeringServo.write(150);
delay(400);
unsigned int rightDist = sonar.ping_cm();
// Return to center
steeringServo.write(90);
delay(300);
// Decision Logic
if (leftDist > rightDist) {
turnLeft();
} else if (rightDist > leftDist) {
turnRight();
} else {
// Both blocked or equal, spin 180 degrees
turnRight();
}
delay(600); // Time to complete the turn
stopMotors();
}
void turnLeft() {
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
analogWrite(ENA, MOTOR_SPEED);
digitalWrite(IN3, HIGH);
digitalWrite(IN4, LOW);
analogWrite(ENB, MOTOR_SPEED);
}
void turnRight() {
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
analogWrite(ENA, MOTOR_SPEED);
digitalWrite(IN3, LOW);
digitalWrite(IN4, HIGH);
analogWrite(ENB, MOTOR_SPEED);
}
Debugging: Brownouts and Motor Jitter
When building robotics projects, you will inevitably encounter the ESP32 resetting itself mid-run. If you are connected to the Serial Monitor, you will see this exact error string printed right before the reboot:
Brownout detector was triggered ets Jan 8 2013,rst cause:4, boot mode:(3,6)
The ESP32 has an internal brownout detector (BOD) that triggers a system reset if the 3.3V rail drops below roughly 2.4V for more than a few microseconds. This is not a software bug; it is a hardware power failure. Here are the first three things to check when this happens:
- Check the Common Ground: Use your multimeter in continuity mode. Place one probe on the ESP32 GND pin and the other on the L298N GND terminal. It must read < 1 ohm. If you are using a breadboard for ground distribution, the internal metal clips often fail under motor vibration. Solder ground wires directly or use a terminal block.
- Measure Battery Sag Under Load: A 2S LiPo might read 8.2V on the bench, but when two TT motors start simultaneously, the voltage can sag to 5V if the battery's C-rating is too low or the wires are too thin. Measure the voltage at the L298N 12V terminal while the rover is trying to drive over carpet. If it drops below 6V, upgrade to a higher C-rating battery or use thicker silicone wire (18 AWG minimum for the main power bus).
- Isolate the USB Cable: If the brownout only happens when plugged into your PC, your USB cable is likely the culprit. Cheap micro-USB cables use 28 AWG power wires that suffer massive voltage drop at 500mA. Swap to a high-quality, short data cable, or power the ESP32 exclusively from your 5V UBEC during testing.
You can technically disable the BOD in the ESP32 Arduino Core menu (Tools > Core Debug Level or via
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); in code). Do not do this. Disabling it just means your ESP32 will operate in an undefined state when voltage sags, leading to corrupted flash memory or erratic GPIO outputs that could short your motor driver.
Extending and Simplifying the Build
Once the basic rover is navigating, you will likely want to refine the platform. Depending on your goals, you can either simplify the power architecture or extend the sensor suite for closed-loop control.
Simplifying (and Upgrading) the Motor Driver
The L298N is a legacy bipolar junction transistor (BJT) driver. It is physically large and drops roughly 2V across its internal transistors. If you feed it 7.4V from a LiPo, your 6V TT motors only see 5.4V, and the remaining 2V is wasted as heat. For modern robotics projects, swap the L298N for a MOSFET-based driver like the TB6612FNG.
| Feature | L298N (BJT) | TB6612FNG (MOSFET) |
|---|---|---|
| Voltage Drop | ~2.0V | ~0.5V |
| Continuous Current | 1.5A per channel | 1.2A per channel |
| PWM Frequency Limit | ~25 kHz | 100 kHz+ |
| Standby Current | ~36 mA | 0.1 μA |
The TB6612FNG requires soldering header pins and uses more GPIO pins (it has separate STBY pins), but the efficiency gains and elimination of the massive heatsink make it the superior choice for battery-constrained rovers.
Extending: Adding Dead Reckoning
The open-loop timing used in the avoidObstacle() function (e.g., delay(600) to turn) is highly unreliable on different surfaces. A 600ms turn on hardwood might yield 90 degrees, while the same code on carpet yields 45 degrees. To fix this, extend the build by adding quadrature wheel encoders to the TT motors. By counting the encoder pulses via ESP32 hardware interrupts, you can implement a PID controller to ensure both wheels spin at the exact same RPM, allowing the rover to drive in a perfectly straight line and execute mathematically precise 90-degree turns regardless of floor friction.
For comprehensive details on ESP32 GPIO current limits and boot-strapping pin states, always refer to the official Espressif ESP32 Datasheet. When implementing non-blocking ultrasonic reads, the NewPing library documentation provides excellent examples of timer-based interrupt handling that keeps your main loop free for motor control calculations.






