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.
| 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) |
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
| 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
- 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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.






