Building a mobile robot is the ultimate test of embedded systems integration. You are combining power electronics, sensor calibration, and real-time control loops on a single bench. But most online tutorials fail because they treat the microcontroller as a magic black box and ignore the electrical realities of motor inductive kickback and voltage sag. This guide cuts the fluff and gives you a decision-forward blueprint for a 2WD line-following robot project that actually works on the first power-up.

The Decision Path: Choosing Your Robot Project Brain and Driver

Before buying parts, you need to lock in your architecture. Use this decision tree to select your microcontroller and motor driver based on your actual project constraints.

Condition / Requirement Recommended Component Why This Pick?
Need WiFi/BLE for live telemetry or OTA updates ESP32-WROOM-32 Dual-core 240MHz, native wireless, 12-bit ADC for precise IR sensor reading.
Simple 5V logic, no wireless, strict budget Arduino Nano v3 (ATmega328P) Dead-simple 5V I/O, massive community support, no strapping pin headaches.
Motors draw < 1.5A total, need high efficiency TB6612FNG Dual H-Bridge MOSFET-based. Only ~0.5V drop vs the L298N's massive ~2V BJT drop.
Motors draw up to 2A, need rugged fault tolerance L298N Dual H-Bridge Bipolar junction design runs hot but survives accidental stalls and shorts better.
The Default Pick: For a standard indoor 2WD line-follower or obstacle avoider where you want wireless debugging without adding extra shields, terminate your decision here: ESP32-WROOM-32 DevKit v1 (38-pin USB-C variant) paired with a generic L298N Red Board. The rest of this guide is built exactly around this combination.

Exact Parts List and Spec Sheet

Do not substitute these exact variants without checking the pinouts and voltage tolerances. Prices reflect typical 2026 hobbyist market rates.

Component Exact Variant / Spec Est. Price Critical Notes
Microcontroller ESP32-WROOM-32 DevKit v1 (38-pin, USB-C) $6.50 Avoid the 30-pin variant; it lacks GPIO 34/35 needed for analog inputs.
Motor Driver L298N Dual H-Bridge (Generic Red Board) $3.50 Includes onboard 7805 5V regulator. Remove the jumper if VCC > 12V.
Sensors TCRT5000 IR Sensor Module (4-pin, Analog + Digital) $1.00 (x2) Must have the blue multi-turn potentiometer for threshold tuning.
Chassis & Motors 2WD Acrylic Chassis + TT Gearmotors (1:48 ratio) $12.00 Stall current is ~800mA per motor at 6V. Keep this in mind for battery sizing.
Power Supply 2x 18650 Li-ion in series (7.4V nominal) + 2S BMS $14.00 Never run raw Li-ion without a BMS. 7.4V is perfect for the L298N 5V regulator.

Wiring and Pin Mapping

The ESP32 has strict rules about which pins can be used for PWM and which are reserved for boot strapping. According to the Espressif GPIO documentation, pins like GPIO 0, 2, 12, and 15 dictate flash voltage and boot modes. Pulling them high or low with motor noise will brick your boot sequence. We use safe, strapping-free pins below.

ESP32 GPIO L298N / Sensor Pin Function
GPIO 13 ENA Left Motor PWM Speed
GPIO 14 IN1 Left Motor Direction A
GPIO 27 IN2 Left Motor Direction B
GPIO 25 ENB Right Motor PWM Speed
GPIO 26 IN3 Right Motor Direction A
GPIO 33 IN4 Right Motor Direction B
GPIO 34 IR Left (A0) Left Sensor Analog Read (Input Only)
GPIO 35 IR Right (A0) Right Sensor Analog Read (Input Only)
GND GND (L298N & Sensors) Common Ground (Critical!)
5V (Vin) L298N 5V Output Powering the ESP32 from the L298N regulator

Physical Wiring Steps

  1. Bond the Grounds: Connect the ESP32 GND, L298N GND, and the negative terminal of your 2S battery pack together. If you skip this, the PWM signals will float and the motors will jitter wildly.
  2. Power the Logic: Wire the battery positive (7.4V) to the L298N 12V terminal. Leave the 5V jumper on the L298N. Wire the L298N 5V output to the ESP32 5V (Vin) pin. Do not power the ESP32 via USB while this 5V wire is connected, or you will backfeed the USB port.
  3. Wire the H-Bridge: Connect the ESP32 GPIO pins to the L298N IN1-IN4 and ENA/ENB pins as mapped above. Ensure ENA and ENB have their physical jumpers removed so the ESP32 can control PWM.
  4. Mount the Sensors: Zip-tie the TCRT5000 modules to the front of the chassis, exactly 1.5 inches apart, and 1/8 inch (3mm) above the floor. Connect their VCC to the ESP32 3.3V pin, not 5V, to protect the ESP32's ADC from overvoltage.

Complete ESP32 Control Code

This code targets the ESP32-WROOM-32 DevKit v1 and requires ESP32 Arduino Core v3.x. In Core v3.x, the legacy ledcSetup() functions were deprecated in favor of the simplified ledcAttach() API, as noted in the Espressif LEDC API Reference.

// Target: ESP32-WROOM-32 DevKit v1 (38-pin)
// Core Version: ESP32 Arduino Core v3.x
// Project: 2WD Line Following Robot

// --- Pin Definitions ---
#define PIN_ENA 13
#define PIN_IN1 14
#define PIN_IN2 27
#define PIN_ENB 25
#define PIN_IN3 26
#define PIN_IN4 33
#define PIN_IR_LEFT 34
#define PIN_IR_RIGHT 35

// --- Calibration Constants ---
#define BASE_SPEED 180       // 0-255 PWM duty cycle
#define TURN_SPEED 120       // Speed during corrections
#define BLACK_THRESHOLD 1500 // 12-bit ADC value (0-4095). Calibrate via Serial Plotter!
#define SENSOR_ERROR_MAX 4000 // Flag if sensor reads near max (disconnected/broken)

void setup() {
  Serial.begin(115200);
  delay(500); // Allow serial monitor to connect
  
  // Configure Direction Pins
  pinMode(PIN_IN1, OUTPUT);
  pinMode(PIN_IN2, OUTPUT);
  pinMode(PIN_IN3, OUTPUT);
  pinMode(PIN_IN4, OUTPUT);
  
  // Configure PWM for Motors (1000Hz frequency, 8-bit resolution)
  // Using modern v3.x ledcAttach API
  ledcAttach(PIN_ENA, 1000, 8);
  ledcAttach(PIN_ENB, 1000, 8);
  
  // Configure Analog Inputs
  analogReadResolution(12); // Ensure 12-bit ADC (0-4095)
  pinMode(PIN_IR_LEFT, INPUT);
  pinMode(PIN_IR_RIGHT, INPUT);
  
  Serial.println("Robot Project Initialized. Waiting for line...");
  stopMotors();
}

void loop() {
  int leftSensor = analogRead(PIN_IR_LEFT);
  int rightSensor = analogRead(PIN_IR_RIGHT);
  
  // Error Handling: Check for disconnected sensors
  if (leftSensor > SENSOR_ERROR_MAX || rightSensor > SENSOR_ERROR_MAX) {
    Serial.println("ERROR: Sensor reading maxed out. Check wiring or 3.3V power.");
    stopMotors();
    delay(1000); // Halt to prevent runaway
    return;
  }

  bool leftOnLine = (leftSensor > BLACK_THRESHOLD);
  bool rightOnLine = (rightSensor > BLACK_THRESHOLD);

  if (leftOnLine && rightOnLine) {
    // Both see black (wide line or intersection) -> Go straight
    driveForward(BASE_SPEED);
  } 
  else if (leftOnLine && !rightOnLine) {
    // Left sees black, Right sees white -> Turn Left
    turnLeft(TURN_SPEED);
  } 
  else if (!leftOnLine && rightOnLine) {
    // Left sees white, Right sees black -> Turn Right
    turnRight(TURN_SPEED);
  } 
  else {
    // Both see white -> Lost the line. Stop to avoid driving off a table.
    stopMotors();
  }
  
  // Telemetry for debugging
  if (millis() % 200 == 0) {
    Serial.printf("L: %d | R: %d\n", leftSensor, rightSensor);
  }
}

// --- Motor Control Functions ---
void driveForward(int speed) {
  digitalWrite(PIN_IN1, HIGH);
  digitalWrite(PIN_IN2, LOW);
  digitalWrite(PIN_IN3, HIGH);
  digitalWrite(PIN_IN4, LOW);
  ledcWrite(PIN_ENA, speed);
  ledcWrite(PIN_ENB, speed);
}

void turnLeft(int speed) {
  digitalWrite(PIN_IN1, LOW);  // Stop left motor
  digitalWrite(PIN_IN2, LOW);
  digitalWrite(PIN_IN3, HIGH); // Spin right motor forward
  digitalWrite(PIN_IN4, LOW);
  ledcWrite(PIN_ENA, 0);
  ledcWrite(PIN_ENB, speed);
}

void turnRight(int speed) {
  digitalWrite(PIN_IN1, HIGH); // Spin left motor forward
  digitalWrite(PIN_IN2, LOW);
  digitalWrite(PIN_IN3, LOW);  // Stop right motor
  digitalWrite(PIN_IN4, LOW);
  ledcWrite(PIN_ENA, speed);
  ledcWrite(PIN_ENB, 0);
}

void stopMotors() {
  digitalWrite(PIN_IN1, LOW);
  digitalWrite(PIN_IN2, LOW);
  digitalWrite(PIN_IN3, LOW);
  digitalWrite(PIN_IN4, LOW);
  ledcWrite(PIN_ENA, 0);
  ledcWrite(PIN_ENB, 0);
}

Debugging: First Three Checks and Exact Error Strings

When you upload this code and the robot fails to move, or moves erratically, do not start rewriting the logic. Hardware and power issues cause 95% of embedded robot failures. Run through this exact diagnostic sequence.

The First Three Things to Check When It Fails

  1. Common Ground Continuity: Put your multimeter in continuity mode. Probe the ESP32 GND pin and the L298N GND terminal. It must read < 1 ohm. If it reads OL (open loop), your PWM signals have no reference return path, and the H-bridge will ignore them.
  2. PWM Frequency Whine: If the motors emit a high-pitched squeal but don't turn, your PWM frequency is too low or the duty cycle is below the motor's stall torque threshold. The code uses 1000Hz. If you changed it to 50Hz, the L298N BJTs will switch too slowly and overheat. Increase the BASE_SPEED variable by 20 to overcome static friction.
  3. IR Sensor Thresholds: Open the Arduino IDE Serial Plotter. Set the baud rate to 115200. Slide the robot over your black tape. If the analog values only swing from 3800 to 4000, your sensor is too far from the floor or the blue potentiometer on the TCRT5000 is misadjusted. Turn the pot until the white surface reads ~500 and the black tape reads ~3000.

Exact Error String: "Brownout detector was triggered"

If your ESP32 constantly reboots and the serial monitor spits out Brownout detector was triggered, the chip's internal voltage monitor has detected the 3.3V rail dropping below ~2.4V. This is the most common ESP32 robot project failure. Here are the ranked causes and fixes:

Rank Cause Fix
1 USB Cable Voltage Drop: You are testing via a cheap, thin-gauge USB cable while the WiFi radio initializes. Swap to a high-quality, short USB-C data cable, or power the ESP32 directly from the L298N 5V output as wired above.
2 Motor Backfeed Noise: The TT gearmotors are generating inductive voltage spikes that collapse the L298N 5V regulator output. Solder a 100nF (0.1uF) ceramic capacitor directly across the metal terminals of each TT motor. This is mandatory for brushed DC motors.
3 L298N 7805 Overheating: The battery voltage is too high (e.g., 3S LiPo at 11.1V), causing the linear 5V regulator to thermally shut down. Stick to a 2S Li-ion (7.4V) or 6x AA NiMH (7.2V). If you must use 12V, remove the 5V jumper and power the ESP32 with a separate buck converter.

Extending or Simplifying the Build

Once the base platform is rolling, you need to decide whether to scale the complexity up for a competition, or strip it down for a classroom workshop.

How to Simplify (Classroom / Beginner Build)

  • Swap the Brain: Replace the ESP32 with an Arduino Nano v3. You eliminate the 3.3V vs 5V logic translation headaches, and you can use the simpler analogWrite() function instead of the LEDC API.
  • Ditch Analog Sensors: Use the digital (D0) pin on the TCRT5000 modules instead of the analog pin. Tune the physical potentiometer on the sensor to output a clean HIGH/LOW signal, reducing the code to basic digitalRead() logic.

How to Extend (Competition / Advanced Build)

  • Upgrade the Driver: Swap the L298N for a TB6612FNG breakout board. The L298N wastes ~2V as heat across its bipolar transistors. The TB6612FNG uses MOSFETs, dropping only ~0.5V. This gives your motors 1.5V more headroom, resulting in noticeably faster lap times and longer battery life.
  • Add PID Steering: Mount an MPU6050 IMU via I2C (SDA to GPIO 21, SCL to GPIO 22). Use the Z-axis gyroscope data to implement a PID control loop that corrects for uneven floor friction, ensuring the robot drives perfectly straight even when one motor is slightly weaker than the other.
  • Implement OTA Updates: Leverage the ESP32's native WiFi to add ArduinoOTA to the setup block. This allows you to tune the BLACK_THRESHOLD and BASE_SPEED variables wirelessly while the robot is on the track, saving you from plugging and unplugging the USB cable dozens of times.
Safety Caveat: When testing extended builds with higher-capacity LiPo batteries (3S or 4S), always use a dedicated balance charger and never leave the pack unattended. The L298N is rated for up to 35V, but the onboard 5V regulator will instantly vaporize if fed more than 12V. Always verify your battery voltage with a multimeter before connecting it to the robot chassis.