Building a mobile robot is where embedded theory meets physical reality. You can have flawless C++ logic, but if your motor driver drops 2V across its H-bridge or your 3.3V logic rail sags when the wheels stall, your project robotics build will end up as a very expensive paperweight. This guide cuts through the generic "blink an LED" tutorials and gives you a decision-forward blueprint for a 2-wheel drive (2WD) differential rover. We will select the exact components, wire them avoiding boot-strapping conflicts, write hardware-PWM code using the modern ESP32 Arduino Core v3.x API, and debug the inevitable power panics.
The Decision Matrix: Picking Your Project Robotics Brain
Before buying parts, you need to lock in the microcontroller. The choice dictates your PWM resolution, logic levels, and telemetry options. Here is the decision path for a standard hobby-to-prosumer robotics platform:
| Criteria | Arduino Uno R4 WiFi | Raspberry Pi Pico W | ESP32 DevKit V1 (38-pin) |
|---|---|---|---|
| Logic Voltage | 5V (Direct to most drivers) | 3.3V (Needs level shifting for some drivers) | 3.3V (Direct to modern MOSFET drivers) |
| PWM Channels | 6 (Hardware) | 16 (Hardware) | 16 (LEDC Hardware, independent duty) |
| Telemetry | WiFi (ESP32-S3 module onboard) | WiFi (Infineon CYW43439) | WiFi + Bluetooth (Native ESP32-WROOM-32E) |
| Core Architecture | Single-core Renesas RA4M1 | Dual-core ARM Cortex-M0+ | Dual-core Xtensa LX6 (240MHz) |
Spec Sheet & BOM: The 2026 ESP32 Rover
Do not use the L298N motor driver. It is a bipolar junction transistor (BJT) design that drops 1.5V to 2.0V across the H-bridge, wasting battery life and starving your motors. Use a MOSFET-based driver. Here is the exact Bill of Materials (BOM) for a robust, low-loss build:
- Microcontroller: ESP32 DevKit V1 (38-pin, ESP32-WROOM-32E module) — ~$6.00
- Motor Driver: Pololu TB6612FNG Dual Motor Driver Carrier (1.2A continuous, 3.2A peak, 0.5V drop) — ~$5.50
- Motors: N20 JGA25-370 6V 100RPM gear motors with magnetic encoders (pair) — ~$14.00
- Chassis: 2WD laser-cut acrylic base with 42mm rubber wheels and rear caster — ~$9.00
- Power: 2S 7.4V 1000mAh LiPo battery with XT60 connector — ~$16.00
- Regulator: LM2596 DC-DC buck converter (set to 5.0V for the ESP32 Vin pin) — ~$3.00
- Sensor: HC-SR04 Ultrasonic (5V tolerant version) or VL53L0X Time-of-Flight (I2C) — ~$4.00
Pin Mapping & Wiring the Drive Train
The ESP32 has notorious "strapping pins" that dictate boot modes. If you wire a motor driver to GPIO 0, 2, 12, or 15, the motor's initial state or pull-up resistors can force the ESP32 into flash-download mode on reboot, bricking your robot's startup sequence. Always consult the official Espressif hardware design guidelines before assigning pins.
| TB6612FNG Pin | ESP32 GPIO | Function & Notes |
|---|---|---|
| VCC | 3.3V | Logic power (TB6612FNG accepts 2.7V-5.5V logic) |
| VM | 7.4V (LiPo +) | Motor power supply (Max 15V) |
| GND | GND | Common ground (Crucial: tie LiPo, ESP32, and Driver grounds together) |
| STBY | GPIO 13 | Standby (Active HIGH to enable driver) |
| AIN1 | GPIO 27 | Motor A Direction 1 |
| AIN2 | GPIO 26 | Motor A Direction 2 |
| PWMA | GPIO 25 | Motor A Speed (PWM, avoid strapping pins) |
| BIN1 | GPIO 33 | Motor B Direction 1 |
| BIN2 | GPIO 32 | Motor B Direction 2 |
| PWMB | GPIO 14 | Motor B Speed (PWM) |
| AO1 / AO2 | Motor A Terminals | Physical motor wires |
| BO1 / BO2 | Motor B Terminals | Physical motor wires |
Compilable Control Code (ESP32 Core v3.x)
The legacy analogWrite() and ledcSetup() functions are deprecated in ESP32 Arduino Core v3.x. The code below uses the modern ledcAttach() API. It also implements the hardware Watchdog Timer (WDT) to reset the board if the control loop hangs—a mandatory safety feature in project robotics to prevent runaway motors.
#include <esp_task_wdt.h>
// --- PIN DEFINITIONS ---
const int PIN_STBY = 13;
const int PIN_AIN1 = 27;
const int PIN_AIN2 = 26;
const int PIN_PWMA = 25;
const int PIN_BIN1 = 33;
const int PIN_BIN2 = 32;
const int PIN_PWMB = 14;
// PWM Configuration
const int PWM_FREQ = 1000; // 1kHz is ideal for DC motors (avoids audible whine)
const int PWM_RESOLUTION = 8; // 0-255 duty cycle
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("ESP32 Rover Booting...");
// Initialize Hardware Watchdog (5 second timeout)
esp_task_wdt_init(5, true);
esp_task_wdt_add(NULL);
// Configure Direction Pins
pinMode(PIN_STBY, OUTPUT);
pinMode(PIN_AIN1, OUTPUT);
pinMode(PIN_AIN2, OUTPUT);
pinMode(PIN_BIN1, OUTPUT);
pinMode(PIN_BIN2, OUTPUT);
digitalWrite(PIN_STBY, HIGH); // Enable motor driver
// Configure PWM using Core v3.x API
ledcAttach(PIN_PWMA, PWM_FREQ, PWM_RESOLUTION);
ledcAttach(PIN_PWMB, PWM_FREQ, PWM_RESOLUTION);
// Ensure motors are stopped on boot
stopMotors();
}
void loop() {
// Reset watchdog timer to prevent panic resets during normal operation
esp_task_wdt_reset();
// Demo sequence: Forward, Turn, Stop
drive(200, 200); // Forward at ~78% duty
delay(2000);
esp_task_wdt_reset();
drive(150, -150); // Pivot right
delay(1000);
esp_task_wdt_reset();
stopMotors();
delay(3000);
}
// --- MOTOR CONTROL FUNCTIONS ---
void drive(int speedA, int speedB) {
// Motor A Logic
if (speedA >= 0) {
digitalWrite(PIN_AIN1, HIGH);
digitalWrite(PIN_AIN2, LOW);
} else {
digitalWrite(PIN_AIN1, LOW);
digitalWrite(PIN_AIN2, HIGH);
speedA = -speedA;
}
// Motor B Logic
if (speedB >= 0) {
digitalWrite(PIN_BIN1, HIGH);
digitalWrite(PIN_BIN2, LOW);
} else {
digitalWrite(PIN_BIN1, LOW);
digitalWrite(PIN_BIN2, HIGH);
speedB = -speedB;
}
// Clamp values to 8-bit resolution
speedA = constrain(speedA, 0, 255);
speedB = constrain(speedB, 0, 255);
ledcWrite(PIN_PWMA, speedA);
ledcWrite(PIN_PWMB, speedB);
}
void stopMotors() {
ledcWrite(PIN_PWMA, 0);
ledcWrite(PIN_PWMB, 0);
digitalWrite(PIN_AIN1, LOW);
digitalWrite(PIN_AIN2, LOW);
digitalWrite(PIN_BIN1, LOW);
digitalWrite(PIN_BIN2, LOW);
}
Debugging: When the Rover Drags, Drifts, or Panics
When your robot fails, it rarely fails gracefully. If your serial monitor spits out the exact error string below, your robot has experienced a catastrophic lockup.
Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)
This means the Watchdog Timer wasn't reset because your code hung, or a hardware brownout caused the CPU to stall. Here are the first three things to check, ranked by probability:
- 3.3V Rail Brownout (80% of cases): When both motors start simultaneously, they draw a massive inrush current. If your LiPo is weak or your wiring is too thin (use at least 20 AWG for motor power), the voltage drops. The ESP32's brownout detector (BOD) triggers a reset, or the CPU stalls mid-instruction, tripping the WDT. Fix: Add a 470µF electrolytic capacitor across the VM and GND terminals on the TB6612FNG, and ensure the ESP32 is powered via a dedicated buck converter, not directly from the motor battery rail.
- Strapping Pin Conflict (15% of cases): If you accidentally wired a motor direction pin to GPIO 12, and the motor driver pulls it HIGH on boot, the ESP32 shifts its flash voltage to 1.8V, fails to read its own firmware, and panics. Fix: Verify your wiring against the safe GPIO list in the pin mapping table above.
- I2C Bus Lockup (5% of cases): If you added an I2C sensor (like a VL53L0X) and the SDA line gets stuck LOW due to a mid-read power glitch, the
Wire.requestFrom()function will block indefinitely, starving the WDT. Fix: Implement a timeout in your I2C reads or use the watchdog to force a hardware reset and re-initialize the I2C bus insetup().
Extending or Simplifying the Build
You have a working baseline. From here, you must decide whether your application requires more autonomy or less complexity.
Simplify: The Cost-Cut Build
If you are building a simple line-follower or a classroom demo and don't need encoder feedback or high efficiency:
- Swap the TB6612FNG for a DRV8833 ($3). It handles lower currents but requires no standby pin logic.
- Drop the magnetic encoders on the N20 motors. Use standard DC gear motors ($4/pair).
- Power the ESP32 directly from a 4x AA battery pack (6V) into the Vin pin, bypassing the LiPo and buck converter entirely.
Extend: The ROS 2 Autonomy Build
If you are building a mapping or SLAM (Simultaneous Localization and Mapping) platform:
- Add micro-ROS via WiFi to integrate the ESP32 as a node in a ROS 2 network running on a Raspberry Pi 5.
- Upgrade the powertrain to NEMA 17 stepper motors with TMC2209 silent drivers for exact open-loop odometry without wheel slip.
- Mount a RPLidar A1 360-degree LiDAR on the top deck, feeding serial data directly to the Pi 5 via USB.
Robotics is an exercise in managing trade-offs between power, logic, and physical mechanics. By starting with a MOSFET-based driver, respecting the ESP32's strapping pins, and utilizing hardware watchdogs, you eliminate the 90% of failures that plague beginner builds. Flash the code, check your ground bonds, and put it on the floor.






