Judging panels at events like the Society for Science fairs rarely award top marks to a robot that merely drives around and bumps into walls. A winning robotics science fair project requires a testable hypothesis, controlled variables, and empirical data. To bridge the gap between a toy and a scientific instrument, we are building an autonomous ESP32-based rover that doesn't just avoid obstacles—it logs distance, decision latency, and motor states to a CSV stream for post-run statistical analysis.

This guide targets the ESP32-WROOM-32 DevKit v1 (30-pin variant). We will cover the exact power budget, logic-level shifting requirements, and the complete firmware needed to turn raw sensor pings into a science fair-winning dataset.

System Architecture and Power Budget

The most common point of failure in student robotics projects is power starvation. Microcontrollers brown out when motors draw peak stall current. Before wiring a single component, you must understand the current draw and voltage logic of your system. The L298N motor driver uses Darlington transistor pairs, which introduce a voltage drop of roughly 1.5V to 2.0V between the input and output terminals.

Table 1: Component Power Budget and Logic Specifications
Component Operating Voltage Quiescent Current Peak/Stall Current Logic Level
ESP32-WROOM-32 DevKit v1 5V (USB/VIN) / 3.3V (Logic) ~80 mA ~240 mA (WiFi TX) 3.3V CMOS
L298N Motor Driver Module 5V to 12V (Motor) ~36 mA 2A per channel (3A peak) 5V TTL (Accepts 3.3V)
HC-SR04 Ultrasonic Sensor 5V DC ~2 mA ~15 mA (During ping) 5V TTL (Requires divider)
SG90 Micro Servo (Scan) 4.8V to 6.0V ~10 mA ~650 mA (Stall) 3.3V to 5V PWM
TT Gear Motor (x4) 3V to 6V ~20 mA (No load) ~800 mA (Stall) N/A (Driven by L298N)
Power Supply Rule: Do not power the motors from the ESP32's 5V pin. Use a 2S LiPo battery (7.4V nominal, 8.4V fully charged, 1500mAh minimum) connected directly to the L298N 12V input terminal, and use the L298N's onboard 5V regulator (with jumper attached) to power the ESP32 VIN and the SG90 servo.

Hardware Parts List and Pin Mapping

Procure these exact variants to ensure the code and physical clearances align. Generic substitutions often result in mismatched mounting holes or different pinout silkscreens.

  • Chassis: 4WD Acrylic Rover Chassis Kit (includes 4x TT motors, wheels, and acrylic plates)
  • MCU: ESP32-WROOM-32 DevKit v1 (30-pin, Type-C or Micro-USB)
  • Motor Driver: L298N Dual H-Bridge Module (Red board variant)
  • Sensor: HC-SR04 Ultrasonic Distance Sensor
  • Scanner: SG90 9g Micro Servo with pan/tilt bracket
  • Logic Shifter: 1x 1kΩ resistor, 1x 2kΩ resistor (for HC-SR04 Echo pin voltage divider)
  • Power: 2S 7.4V 1500mAh LiPo battery with XT60 connector and a rocker switch
Table 2: ESP32 to Peripheral Pin Mapping
ESP32 GPIO Target Component Target Pin Notes / Constraints
GPIO 5 HC-SR04 Trig 3.3V output is sufficient to trigger 5V module
GPIO 18 Voltage Divider Echo (via 1kΩ/2kΩ) Must step 5V Echo down to ~3.3V to prevent MCU damage
GPIO 13 SG90 Servo PWM Signal Supports hardware PWM
GPIO 27 L298N IN1 (Right Fwd) Digital HIGH/LOW
GPIO 26 L298N IN2 (Right Rev) Digital HIGH/LOW
GPIO 25 L298N IN3 (Left Fwd) Digital HIGH/LOW
GPIO 33 L298N IN4 (Left Rev) Digital HIGH/LOW

Step-by-Step Assembly and Wiring

  1. Prepare the Voltage Divider: The HC-SR04 Echo pin outputs 5V. The ESP32 GPIO pins are strictly 3.3V tolerant; feeding 5V will degrade or destroy the silicon. Solder the 2kΩ resistor between GPIO 18 and GND. Solder the 1kΩ resistor between GPIO 18 and the HC-SR04 Echo pin. This creates a divider that steps 5V down to ~3.33V.
  2. Mount the L298N: Bolt the L298N to the lower acrylic plate. Connect the 4 TT motors to the OUT1/OUT2 (Right side) and OUT3/OUT4 (Left side) terminal blocks.
  3. Wire the Power: Connect the LiPo battery positive (red) to a rocker switch, then to the L298N 12V terminal. Connect the battery ground (black) directly to the L298N GND terminal. Crucial: Leave the 5V-EN jumper cap ON the L298N's 3-pin header to enable the onboard 7805 regulator.
  4. Connect MCU Power: Run a wire from the L298N 5V terminal to the ESP32 VIN (or 5V) pin. Run a wire from the L298N GND to the ESP32 GND. A common ground is mandatory for logic signals to register.
  5. Wire Logic Pins: Connect the ESP32 GPIOs to the L298N IN1-IN4 pins according to Table 2. Remove the ENA and ENB jumper caps on the L298N and wire them to the ESP32 5V pin (or a PWM pin if you want speed control, but tie to 5V for max torque during science fair runs).
  6. Mount Sensor Assembly: Bolt the SG90 servo to the front chassis bracket. Attach the HC-SR04 to the servo horn. Wire the servo signal to GPIO 13, servo VCC to L298N 5V, and servo GND to common ground.

Complete ESP32 Data-Logging Firmware

This firmware targets the ESP32 DevKit v1 board in the Arduino IDE. It avoids blocking delays where possible and outputs a continuous CSV stream over Serial at 115200 baud. You can log this directly to a text file using the Arduino IDE Serial Monitor or a tool like PuTTY, providing the raw data needed for your science fair poster graphs.

#include <ESP32Servo.h>

// --- PIN DEFINITIONS ---
#define TRIG_PIN 5
#define ECHO_PIN 18
#define SERVO_PIN 13

#define IN1 27 // Right Forward
#define IN2 26 // Right Reverse
#define IN3 25 // Left Forward
#define IN4 33 // Left Reverse

// --- SYSTEM CONSTANTS ---
const int MAX_DISTANCE = 200; // cm
const int SAFE_DISTANCE = 30; // cm
const unsigned long PING_TIMEOUT_US = 12000; // ~200cm at speed of sound

Servo scanServo;
unsigned long loopCounter = 0;

void setup() {
  Serial.begin(115200);
  while(!Serial) { delay(10); }
  
  // Print CSV Header for data logging
  Serial.println("Timestamp_ms,Loop_Count,Servo_Angle,Front_Dist_cm,Left_Dist_cm,Right_Dist_cm,Action_Taken");

  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);

  scanServo.setPeriodHertz(50);
  scanServo.attach(SERVO_PIN, 500, 2400);
  
  // Center servo and stop motors
  scanServo.write(90);
  stopMotors();
  delay(500);
}

void loop() {
  unsigned long currentTime = millis();
  loopCounter++;

  // 1. Scan Front
  scanServo.write(90);
  delay(300); // Allow servo to settle
  int distFront = readUltrasonic();

  int distLeft = 0;
  int distRight = 0;
  String action = "FORWARD";

  // 2. Decision Logic
  if (distFront < SAFE_DISTANCE && distFront >= 0) {
    stopMotors();
    
    // Scan Left
    scanServo.write(160);
    delay(400);
    distLeft = readUltrasonic();
    
    // Scan Right
    scanServo.write(20);
    delay(400);
    distRight = readUltrasonic();
    
    // Return to center
    scanServo.write(90);
    delay(300);

    // Decide turn based on data
    if (distLeft > distRight) {
      turnLeft();
      action = "TURN_LEFT";
    } else {
      turnRight();
      action = "TURN_RIGHT";
    }
  } else {
    moveForward();
  }

  // 3. Log Data (CSV format)
  Serial.print(currentTime);
  Serial.print(",");
  Serial.print(loopCounter);
  Serial.print(",");
  Serial.print(scanServo.read());
  Serial.print(",");
  Serial.print(distFront);
  Serial.print(",");
  Serial.print(distLeft);
  Serial.print(",");
  Serial.print(distRight);
  Serial.print(",");
  Serial.println(action);
}

// --- MOTOR CONTROL FUNCTIONS ---
void moveForward() {
  digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
  digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
}

void turnLeft() {
  digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);
  digitalWrite(IN3, LOW); digitalWrite(IN4, HIGH);
  delay(400); // Time-based turn
  stopMotors();
}

void turnRight() {
  digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH);
  digitalWrite(IN3, HIGH); digitalWrite(IN4, LOW);
  delay(400); // Time-based turn
  stopMotors();
}

void stopMotors() {
  digitalWrite(IN1, LOW); digitalWrite(IN2, LOW);
  digitalWrite(IN3, LOW); digitalWrite(IN4, LOW);
}

// --- SENSOR FUNCTION WITH ERROR HANDLING ---
int readUltrasonic() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  // Read echo with strict timeout to prevent blocking
  unsigned long duration = pulseIn(ECHO_PIN, HIGH, PING_TIMEOUT_US);
  
  if (duration == 0) {
    Serial.println("Error: Ultrasonic pulse timeout (0us) - Check wiring or 5V rail.");
    return -1; // Return -1 to indicate error state
  }
  
  // Calculate distance (speed of sound = 343 m/s -> 0.0343 cm/us)
  int distance = duration * 0.0343 / 2;
  
  if (distance > MAX_DISTANCE) return MAX_DISTANCE;
  return distance;
}

Debugging: When the Rover Fails to Navigate

When your rover misbehaves on the lab bench, do not guess. Follow this diagnostic sequence. These are the first three things to check when the system fails, based on the most common failure modes in student builds.

  1. Verify the Common Ground: If the motors twitch randomly or the ESP32 ignores sensor inputs, the L298N ground and ESP32 ground are likely not connected. Logic signals require a shared reference voltage. Measure the resistance between the ESP32 GND pin and the L298N GND terminal; it must read < 1 ohm.
  2. Check the HC-SR04 Voltage Divider: Use a multimeter to probe GPIO 18 while the sensor is pinging a wall. If you see 5V pulses, your resistor network is wired backward or open. You are slowly cooking the ESP32's input protection diodes. It must read ~3.3V.
  3. Inspect the L298N Enable Jumpers: If the logic LEDs on the L298N light up but the motors don't spin, the ENA and ENB jumper caps are missing, and you haven't wired them to 5V. The H-bridges are disabled by default without these connections.

Common Exact Error Strings

If the ESP32 reboots randomly during a turn, check your Serial Monitor for this exact string:

Brownout detector was triggered

Cause: The SG90 servo and two TT motors are drawing peak current simultaneously, causing the battery voltage to sag below the ESP32's brownout threshold (usually ~2.4V at the silicon die). Fix: Upgrade to a battery with a higher C-rating (discharge rate), or add a 470µF electrolytic capacitor across the ESP32's 5V and GND pins to buffer transient current spikes.

If the rover drives blindly into walls and the Serial Monitor prints:

Error: Ultrasonic pulse timeout (0us) - Check wiring or 5V rail.

Cause: The pulseIn() function timed out before detecting the 5V echo wave. This usually means the HC-SR04 is unpowered (check the 5V rail on the breadboard) or the Trig pin is wired to the wrong GPIO.

Scaling the Build: Simplify or Extend

Science fair projects must fit your timeline and skill level. Here is how to adjust the scope of this build without losing the scientific method.

To Simplify (Middle School / Early High School):
Remove the SG90 servo entirely. Mount the HC-SR04 facing dead forward. Modify the code to use a "bang-bang" control loop: if the front distance is < 20cm, stop, reverse for 500ms, spin right for 800ms, and resume forward driving. You lose the multi-angle data points, but the wiring complexity drops by 40%, and you eliminate servo-induced brownouts.

To Extend (Advanced High School / Collegiate):
Replace the time-based turns with closed-loop PID control. Add two IR reflective sensors (like the TCRT5000) pointing down at the wheels to act as rudimentary wheel encoders. By counting the black/white stripes on the inside of the wheels, you can calculate actual distance traveled per turn. Log the target turn angle vs. actual turn angle to the CSV stream, allowing you to graph the mechanical slip and inefficiency of the TT motors on different surface materials (tile vs. carpet). This transforms the project from a simple robotics demo into a rigorous study of kinematic friction and open-loop vs. closed-loop control systems.