Project Overview & Difficulty Rating
When tackling do it yourself robotics projects, the transition from a blinking LED to a moving platform introduces a harsh reality: power dynamics and real-time operating system (RTOS) conflicts. This guide walks through building a differential-drive obstacle-avoiding rover, but more importantly, it focuses on the firmware debugging required to keep the ESP32 from crashing under motor load.
Target Board Variant: ESP32-WROOM-32 DevKit V1 (Specifically the 38-pin variant. Do not use the 30-pin variant for this build, as it lacks the necessary GPIO breakouts for dual motor PWM and ultrasonic triggers without multiplexing).
Estimated Cost: $35 - $45 USD
Time to Build: 3 hours (hardware), 2 hours (firmware tuning)
Hardware Spec Sheet & Pin Mapping
Forget the L298N motor driver. It drops nearly 2V across its internal Darlington transistors, starving your motors and generating massive heat. For modern builds, we use the TB6612FNG MOSFET driver. It operates at >90% efficiency and handles PWM frequencies up to 100kHz, allowing us to push motor whine out of the audible range.
| Component | Exact Variant / Spec | Est. Price |
|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 (38-pin) | $6.00 |
| Motor Driver | TB6612FNG Dual H-Bridge (Pololu or generic) | $4.50 |
| Motors | 2x N20 Metal Gear 6V 300RPM (JGA25-370) | $9.00 |
| Distance Sensor | HC-SR04 Ultrasonic (5V tolerant trigger/echo) | $2.00 |
| Power Supply | 2S LiPo 7.4V 1000mAh 25C with XT60 connector | $14.00 |
| Voltage Regulator | LM2596 Buck Converter (Set to 5.0V for logic) | $2.50 |
ESP32 38-Pin to TB6612FNG & HC-SR04 Mapping
| ESP32 GPIO | Destination Module | Module Pin | Wire Gauge / Note |
|---|---|---|---|
| GPIO 13 | TB6612FNG | PWMA | 22 AWG Stranded |
| GPIO 12 | TB6612FNG | AIN1 | 22 AWG Stranded |
| GPIO 14 | TB6612FNG | AIN2 | 22 AWG Stranded |
| GPIO 27 | TB6612FNG | PWMB | 22 AWG Stranded |
| GPIO 26 | TB6612FNG | BIN1 | 22 AWG Stranded |
| GPIO 25 | TB6612FNG | BIN2 | 22 AWG Stranded |
| GPIO 5 | TB6612FNG | STBY | 22 AWG (Pull to 3.3V) |
| GPIO 32 | HC-SR04 | Trig | 22 AWG |
| GPIO 33 | HC-SR04 | Echo | Voltage divider to 3.3V! |
| GND | All Modules | GND | 18 AWG for main bus |
Assembly & Wiring Steps
- Prepare the Power Bus: Solder the XT60 connector to a main power distribution board. Run 18 AWG silicone wire from the LiPo to the LM2596 buck converter input. Crucial: Add a 470µF electrolytic capacitor across the LiPo terminals to absorb motor startup current spikes.
- Set the Logic Voltage: Power the buck converter from a bench supply (or carefully from the LiPo). Use a multimeter to adjust the trim pot until the output reads exactly 5.05V. This feeds the HC-SR04 VCC and the ESP32 VIN pin.
- Wire the Echo Pin Divider: The HC-SR04 outputs a 5V echo pulse. The ESP32 GPIOs are strictly 3.3V tolerant. Solder a voltage divider using a 1kΩ resistor (series to GPIO 33) and a 2kΩ resistor (GPIO 33 to GND). This drops the 5V signal to a safe ~3.33V.
- Connect Motor Power: Run 18 AWG wire from the raw 7.4V LiPo positive directly to the TB6612FNG
VMpin. Do not route motor power through the breadboard; the thin internal clips will overheat and cause voltage drop. - Verify Continuity: Before plugging in the ESP32, use your multimeter's continuity mode to check for shorts between VCC and GND on the motor driver and sensor modules.
The Firmware: Compilable Code with Error Handling
This code targets the ESP32 Arduino Core v3.x. It uses the modern ledcAttach API and implements strict timeouts on the ultrasonic sensor to prevent blocking the RTOS watchdog. Copy this directly into your Arduino IDE.
#include <WiFi.h>
// --- PIN DEFINITIONS ---
#define PWMA 13
#define AIN1 12
#define AIN2 14
#define PWMB 27
#define BIN1 26
#define BIN2 25
#define STBY 5
#define TRIG_PIN 32
#define ECHO_PIN 33
// --- CONSTANTS ---
const int PWM_FREQ = 20000; // 20kHz to avoid audible motor whine
const int PWM_RES = 8; // 0-255 duty cycle
const long MAX_DISTANCE = 200; // cm
const long MAX_PULSE_TIMEOUT = 15000; // microseconds (approx 2.5 meters)
// WiFi Credentials (used to demonstrate dual-core RTOS load)
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("[BOOT] Initializing ESP32 Rover...");
// Configure Motor Driver Pins (Modern ESP32 Core v3.x API)
ledcAttach(PWMA, PWM_FREQ, PWM_RES);
ledcAttach(PWMB, PWM_FREQ, PWM_RES);
pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT);
pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT);
pinMode(STBY, OUTPUT);
digitalWrite(STBY, HIGH); // Enable TB6612FNG
// Configure Ultrasonic Pins
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
// Connect to WiFi (Runs on Core 0, Motor logic on Core 1)
WiFi.begin(ssid, password);
Serial.print("[WIFI] Connecting");
int retries = 0;
while (WiFi.status() != WL_CONNECTED && retries < 20) {
delay(500);
Serial.print(".");
retries++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WIFI] Connected!");
} else {
Serial.println("\n[WIFI] Failed. Running in offline mode.");
}
}
void loop() {
// 1. Read Sensor with strict timeout to prevent WDT panic
long distance = readUltrasonicSafe();
// 2. Decision Logic
if (distance > 0 && distance < 30) {
Serial.printf("[NAV] Obstacle at %ld cm. Reversing.\n", distance);
driveMotors(-150, -150); // Reverse
delay(400);
driveMotors(-100, 100); // Pivot
delay(300);
} else {
driveMotors(200, 200); // Forward
}
// 3. Feed the RTOS Watchdog explicitly (Best practice for tight loops)
yield();
delay(50); // Throttle loop to 20Hz
}
// --- HELPER FUNCTIONS ---
void driveMotors(int speedA, int speedB) {
// Motor A Control
if (speedA > 0) {
digitalWrite(AIN1, HIGH); digitalWrite(AIN2, LOW);
} else if (speedA < 0) {
digitalWrite(AIN1, LOW); digitalWrite(AIN2, HIGH);
} else {
digitalWrite(AIN1, LOW); digitalWrite(AIN2, LOW);
}
ledcWrite(PWMA, abs(speedA));
// Motor B Control
if (speedB > 0) {
digitalWrite(BIN1, HIGH); digitalWrite(BIN2, LOW);
} else if (speedB < 0) {
digitalWrite(BIN1, LOW); digitalWrite(BIN2, HIGH);
} else {
digitalWrite(BIN1, LOW); digitalWrite(BIN2, LOW);
}
ledcWrite(PWMB, abs(speedB));
}
long readUltrasonicSafe() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
// CRITICAL: pulseIn with timeout prevents infinite blocking if echo is lost
long duration = pulseIn(ECHO_PIN, HIGH, MAX_PULSE_TIMEOUT);
if (duration == 0) {
return -1; // Timeout occurred, no valid reading
}
long distance = (duration * 0.0343) / 2;
return (distance > MAX_DISTANCE) ? MAX_DISTANCE : distance;
}
Debugging the "Guru Meditation Error" in Motor Loops
If you have built do it yourself robotics projects with the ESP32, you have likely encountered this exact serial monitor output when the robot stops moving and the board reboots:
Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)
This is the ESP32's Task Watchdog Timer (WDT) killing your process because the loop() function on Core 1 failed to yield control back to the FreeRTOS scheduler within the default timeout (usually 1.2 to 5 seconds). For a deeper architectural breakdown, refer to the Espressif WDT Documentation.
Ranked Causes & Fixes
- Blocking
pulseIn()without a timeout (Most Common): If the HC-SR04 echo pin never goes HIGH (due to sound scattering off an angled surface),pulseIn(ECHO_PIN, HIGH)will wait forever. Fix: Always use the three-argument version:pulseIn(pin, state, timeout_microseconds)as shown in the code above. - Power Brownout Resetting the Brownout Detector: When both N20 motors start simultaneously, they can pull 1.5A+ momentarily. If your LiPo is weak or wires are too thin, the ESP32's 3.3V rail dips, triggering a hardware brownout reset that mimics a WDT panic. Fix: Add the 470µF bulk capacitor on the battery leads and ensure motor power does not share the breadboard ground with the ESP32 logic.
- WiFi Stack Starvation: If you put a
delay(1000)inside a tight motor control loop, the WiFi stack (running on Core 0 or 1 depending on config) cannot process background TCP/IP keep-alives, causing the RTOS to panic. Fix: Usemillis()for timing instead ofdelay(), and callyield()at the end of every loop iteration.
- Multimeter Voltage Check: Probe the ESP32
3V3pin andGNDwhile the motors are stalled. If it drops below 3.1V, you have a power delivery failure, not a code bug. - I2C/PWM Pin Conflicts: Ensure you aren't using GPIO 1 (TX0) or GPIO 3 (RX0) for motor PWM, as serial debug output will interfere with the PWM signal, causing erratic motor behavior and serial buffer overflows.
- Watchdog Starvation: Search your code for any
while()loops waiting for a sensor state change. If there is noyield()orvTaskDelay()inside that while loop, it will trigger the WDT.
Extending or Simplifying the Build
Not every project needs to start at maximum complexity. Here is how to adjust this platform based on your current skill level and parts bin.
How to Simplify (For Beginners)
- Drop the Ultrasonic Sensor: Replace the HC-SR04 with two analog infrared bump sensors (like the Pololu QRE1113). This eliminates the 5V logic level shifting, removes the need for pulse timing entirely, and simplifies the code to basic analog reads.
- Remove WiFi: If you don't need telemetry, strip out the
WiFi.hincludes and connection logic. This frees up CPU cycles and eliminates Core 0/1 RTOS conflicts entirely.
How to Extend (For Advanced Builders)
- Add Closed-Loop PID Steering: Swap the basic N20 motors for versions with magnetic encoders (e.g., JGA25-370 with 7PPR encoders). Use the ESP32's PCNT (Pulse Counter) hardware peripheral to read encoder ticks without CPU overhead, and implement a PID controller to ensure the robot drives perfectly straight.
- Integrate Micro-ROS: Install the micro-ROS agent on a Raspberry Pi companion board, and flash the ESP32 with the micro-ROS Arduino client. This allows you to publish odometry topics and subscribe to
cmd_veltwist messages over WiFi, bridging your DIY rover into a full ROS2 navigation stack.
DIY Robotics Projects FAQ
What are the best do it yourself robotics projects for beginners?
For absolute beginners, a line-following robot using an Arduino Nano and a 3-channel IR sensor array is the best starting point. It requires no RTOS knowledge, uses simple 5V logic, and teaches the fundamentals of differential steering and closed-loop control without the complexity of WiFi stacks or voltage dividers. Once you master line following, upgrading to an ESP32-based obstacle avoidance rover (like the one in this guide) is the logical next step.
How do I power do it yourself robotics projects without burning out the ESP32?
Never power motors directly from the ESP32's 3.3V or 5V pins; the onboard voltage regulator maxes out around 500mA and will overheat instantly. Always use a separate motor driver (like the TB6612FNG) powered directly from the battery pack. Crucially, you must tie the battery ground, motor driver ground, and ESP32 ground together to establish a common reference voltage. Use a dedicated buck converter (like the LM2596) to step the battery voltage down to 5V for the ESP32's VIN pin.
Why do my do it yourself robotics projects disconnect from WiFi when motors start?
This is almost always caused by Electromagnetic Interference (EMI) or voltage sag. Brushed DC motors generate massive electrical noise that can detune the ESP32's 2.4GHz antenna if the motors are mounted too close to the chip. Keep motors at least 3 inches away from the ESP32 antenna trace. Additionally, if the motor startup current causes the battery voltage to sag, the ESP32's RF amplifier will brown out and drop the WiFi connection. Adding a large bulk capacitor (470µF - 1000µF) across the battery terminals solves the voltage sag issue.






