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)
The Verdict: Choose the ESP32 DevKit V1 (38-pin variant). The dual-core architecture allows you to run motor control PID loops on Core 1 while handling WiFi telemetry (like MQTT or WebSockets) on Core 0 without interrupt latency. The 38-pin variant is critical because the narrower 30-pin boards often break out fewer usable GPIOs, leaving you short on pins for encoders and sensors.

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
VCC3.3VLogic power (TB6612FNG accepts 2.7V-5.5V logic)
VM7.4V (LiPo +)Motor power supply (Max 15V)
GNDGNDCommon ground (Crucial: tie LiPo, ESP32, and Driver grounds together)
STBYGPIO 13Standby (Active HIGH to enable driver)
AIN1GPIO 27Motor A Direction 1
AIN2GPIO 26Motor A Direction 2
PWMAGPIO 25Motor A Speed (PWM, avoid strapping pins)
BIN1GPIO 33Motor B Direction 1
BIN2GPIO 32Motor B Direction 2
PWMBGPIO 14Motor B Speed (PWM)
AO1 / AO2Motor A TerminalsPhysical motor wires
BO1 / BO2Motor B TerminalsPhysical 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:

  1. 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.
  2. 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.
  3. 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 in setup().

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.