🔗Goal
Build a robot car controlled from your phone over WiFi. The ESP32 runs a web server with an HTML/JavaScript joystick interface -- open it in any phone browser, no app needed. The car also has an autonomous obstacle avoidance mode: an ultrasonic sensor mounted on a servo sweeps left and right to find the clearest path, and the car navigates around obstacles on its own.
graph LR
subgraph Robot Car
ESP[ESP32] -->|PWM| MD[L298N Motor Driver]
MD --> M1[Left Motor]
MD --> M2[Right Motor]
ESP -->|GPIO| SRV[Servo Motor]
SRV --> US[HC-SR04 Sensor]
ESP -->|WiFi| WEB[Web Server]
end
PHONE[Phone Browser] -->|WiFi| WEB
BAT[18650 Battery Pack<br/>7.4V] --> MD
MD -->|5V Regulator| ESPThe car has two modes that you can switch between from the phone interface:
- Manual mode: Full joystick control with variable speed and direction
- Autonomous mode: The car drives itself, avoiding obstacles by scanning with the ultrasonic sensor
🔗Prerequisites
| Component | Qty | Notes |
|---|---|---|
| ESP32 dev board | 1 | ESP32-WROOM-32 DevKit |
| L298N motor driver module | 1 | Dual H-bridge, supports up to 36V/2A per channel |
| DC gearbox motors (3-6V) | 2 | Typically sold with wheels (TT motors) |
| Wheels | 2 | Matching the motors |
| Caster wheel | 1 | Ball caster or swivel wheel for the front/back |
| HC-SR04 ultrasonic sensor | 1 | For distance measurement |
| SG90 servo motor | 1 | For sweeping the ultrasonic sensor |
| 18650 Li-ion batteries | 2 | In series for 7.4V (use protected cells) |
| 2S 18650 battery holder | 1 | With leads |
| Robot car chassis | 1 | 2WD acrylic or 3D-printed platform |
| Toggle switch | 1 | Main power on/off |
| Jumper wires | ~20 | Various lengths |
| Small breadboard | 1 | Half-size, mounted on the chassis |
| Zip ties / standoffs | Several | For mounting components |
About the chassis: You can buy a ready-made 2WD robot car chassis kit for a few USD that includes the acrylic platform, motors, wheels, caster, and mounting hardware. This saves a lot of time. Alternatively, you can use any flat rigid surface (even thick cardboard for a prototype).
🔗Software
No additional Arduino libraries are needed. The code uses only libraries built into the ESP32 Arduino core:
WiFi.h-- WiFi connectivityWebServer.h-- HTTP serverESP32Servo.h-- Install via Library Manager (search "ESP32Servo" by Kevin Harrington)
🔗Power System Architecture
Power management is critical for a robot car. The motors and the ESP32 have very different power requirements:
graph TD
BAT[18650 Battery Pack<br/>7.4V 2S] --> SW[Power Switch]
SW --> L298N[L298N Motor Driver<br/>VIN: 7.4V]
L298N -->|5V Regulator Output| ESP[ESP32 DevKit<br/>via VIN pin]
L298N -->|Motor A Output| M1[Left Motor]
L298N -->|Motor B Output| M2[Right Motor]
ESP -->|3.3V| SERVO[Servo Motor<br/>Power from ESP 5V pin]
ESP -->|3.3V / 5V| HC[HC-SR04]| Component | Voltage | Current |
|---|---|---|
| DC motors (x2) | 3-6V | 100-250 mA each (up to 1A stall) |
| ESP32 | 3.3V (via onboard regulator) | ~240 mA peak |
| Servo (SG90) | 4.8-6V | 100-250 mA |
| HC-SR04 | 5V | ~15 mA |
The L298N module has a built-in 5V voltage regulator. When you supply 7.4V to the motor input (12V terminal), the L298N outputs 5V on its 5V pin. You can use this to power the ESP32 through its VIN pin (the ESP32's onboard regulator then steps it down to 3.3V).
Important: The L298N's 5V regulator can only supply about $500\,\text{mA}$. This is enough for the ESP32 and HC-SR04, but the servo should be powered from the ESP32's 5V pin (which comes from the same source). If the servo causes brownouts, consider powering it from the battery through a separate 5V buck converter.
Do not connect the battery directly to the ESP32 3.3V pin. The 7.4V will destroy it. Always go through the L298N's 5V regulator or a separate buck converter to the ESP32's VIN pin.
🔗L298N Internal Voltage Drop
The L298N is a BJT-based H-bridge, so it has a significant voltage drop of about $2\,\text{V}$. With a 7.4V battery, the motors will actually see approximately:
$$V_{motor} = V_{battery} - V_{drop} = 7.4 - 2.0 = 5.4\,\text{V}$$
This is fine for 3-6V TT motors. If you need more motor voltage, use a 3S pack (11.1V), but check that your motors can handle the resulting ~9V.
🔗Tutorial
🔗Step 1: Wire the L298N Motor Driver
The L298N has several terminal blocks and pins. Here is how to wire it:
| L298N Pin | Connection |
|---|---|
| 12V / VIN | Battery positive (7.4V through switch) |
| GND | Battery negative AND ESP32 GND |
| 5V (output) | ESP32 VIN pin |
| ENA | ESP32 GPIO 14 (PWM speed control, left motor) |
| IN1 | ESP32 GPIO 27 |
| IN2 | ESP32 GPIO 26 |
| IN3 | ESP32 GPIO 25 |
| IN4 | ESP32 GPIO 33 |
| ENB | ESP32 GPIO 32 (PWM speed control, right motor) |
| OUT1 | Left motor terminal 1 |
| OUT2 | Left motor terminal 2 |
| OUT3 | Right motor terminal 1 |
| OUT4 | Right motor terminal 2 |
Remove the ENA and ENB jumpers on the L298N board. These jumpers connect the enable pins directly to 5V (full speed, always on). We want PWM speed control, so we need to drive ENA and ENB from the ESP32.
Important: The L298N GND must be connected to the ESP32 GND. Without a common ground, signals between the boards will not work.
The motor direction is controlled by IN1-IN4:
| IN1 | IN2 | Left Motor |
|---|---|---|
| HIGH | LOW | Forward |
| LOW | HIGH | Backward |
| LOW | LOW | Stop (coast) |
| HIGH | HIGH | Stop (brake) |
The same logic applies to IN3/IN4 for the right motor.
🔗Step 2: Wire the HC-SR04 Ultrasonic Sensor
| HC-SR04 Pin | ESP32 Pin |
|---|---|
| VCC | 5V (from ESP32) |
| GND | GND |
| TRIG | GPIO 5 |
| ECHO | GPIO 18 |
The HC-SR04 operates at 5V logic, but GPIO 18 on the ESP32 is 5V-tolerant as an input (the internal protection diodes handle the 5V ECHO signal). For a more robust setup, you can use a voltage divider (two resistors) on the ECHO line to step it down to 3.3V, but in practice most ESP32 boards work fine without one.
The sensor measures distance by sending a 10 microsecond pulse on TRIG and timing how long the ECHO pin stays HIGH:
$$d = \frac{t \times v_{sound}}{2} = \frac{t \times 343}{2 \times 10000} \,\text{cm}$$
where $t$ is the echo time in microseconds, and we divide by 2 because the sound travels to the object and back.
🔗Step 3: Wire the Servo Motor
The servo mounts the HC-SR04 on top of it so the sensor can sweep left and right.
| Servo Wire | ESP32 Pin |
|---|---|
| Red (VCC) | 5V (from ESP32) |
| Brown/Black (GND) | GND |
| Orange/Yellow (Signal) | GPIO 13 |
Mount the servo on the front of the chassis (pointing forward) and attach the HC-SR04 on top of the servo horn using a small bracket, hot glue, or zip ties. The servo will rotate the sensor left, center, and right to scan for obstacles.
🔗Step 4: Complete Wiring Summary
Here is every connection in one table:
| ESP32 Pin | Connected To | Purpose |
|---|---|---|
| VIN | L298N 5V output | Power from motor driver |
| GND | L298N GND, HC-SR04 GND, Servo GND | Common ground |
| GPIO 14 | L298N ENA | Left motor speed (PWM) |
| GPIO 27 | L298N IN1 | Left motor direction |
| GPIO 26 | L298N IN2 | Left motor direction |
| GPIO 25 | L298N IN3 | Right motor direction |
| GPIO 33 | L298N IN4 | Right motor direction |
| GPIO 32 | L298N ENB | Right motor speed (PWM) |
| GPIO 5 | HC-SR04 TRIG | Ultrasonic trigger |
| GPIO 18 | HC-SR04 ECHO | Ultrasonic echo |
| GPIO 13 | Servo signal | Sensor sweep |
| 5V | HC-SR04 VCC, Servo VCC | Sensor and servo power |
🔗Step 5: Upload the Code
This sketch is the complete robot car firmware. It includes the web server, joystick interface, motor control, and autonomous obstacle avoidance mode.
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>
// ==== WiFi CONFIGURATION ====
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// ==== MOTOR PINS ====
#define ENA 14 // Left motor speed (PWM)
#define IN1 27 // Left motor direction
#define IN2 26
#define IN3 25 // Right motor direction
#define IN4 33
#define ENB 32 // Right motor speed (PWM)
// ==== SENSOR PINS ====
#define TRIG_PIN 5
#define ECHO_PIN 18
#define SERVO_PIN 13
// ==== PWM CONFIGURATION ====
#define PWM_FREQ 1000
#define PWM_RESOLUTION 8 // 0-255
#define PWM_CHANNEL_A 0
#define PWM_CHANNEL_B 1
// ==== OBSTACLE AVOIDANCE ====
#define STOP_DISTANCE 25 // cm - stop and look around
#define SLOW_DISTANCE 50 // cm - reduce speed
#define MAX_DISTANCE 300 // cm - ignore readings beyond this
// ==== OBJECTS ====
WebServer server(80);
Servo scanServo;
// ==== STATE ====
bool autonomousMode = false;
int currentSpeed = 200; // 0-255
// ==============================
// MOTOR FUNCTIONS
// ==============================
void motorsInit() {
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
ledcSetup(PWM_CHANNEL_A, PWM_FREQ, PWM_RESOLUTION);
ledcSetup(PWM_CHANNEL_B, PWM_FREQ, PWM_RESOLUTION);
ledcAttachPin(ENA, PWM_CHANNEL_A);
ledcAttachPin(ENB, PWM_CHANNEL_B);
motorsStop();
}
void setMotors(int leftSpeed, int rightSpeed) {
// leftSpeed and rightSpeed: -255 to 255
// Positive = forward, negative = backward
// Left motor
if (leftSpeed > 0) {
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
} else if (leftSpeed < 0) {
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
} else {
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
}
ledcWrite(PWM_CHANNEL_A, abs(leftSpeed));
// Right motor
if (rightSpeed > 0) {
digitalWrite(IN3, HIGH);
digitalWrite(IN4, LOW);
} else if (rightSpeed < 0) {
digitalWrite(IN3, LOW);
digitalWrite(IN4, HIGH);
} else {
digitalWrite(IN3, LOW);
digitalWrite(IN4, LOW);
}
ledcWrite(PWM_CHANNEL_B, abs(rightSpeed));
}
void motorsStop() {
setMotors(0, 0);
}
void moveForward(int speed) {
setMotors(speed, speed);
}
void moveBackward(int speed) {
setMotors(-speed, -speed);
}
void turnLeft(int speed) {
setMotors(-speed, speed);
}
void turnRight(int speed) {
setMotors(speed, -speed);
}
// ==============================
// ULTRASONIC SENSOR
// ==============================
float measureDistance() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH, 30000); // 30ms timeout
if (duration == 0) return MAX_DISTANCE; // No echo = no obstacle
float distance = duration * 0.0343 / 2.0;
return (distance > MAX_DISTANCE) ? MAX_DISTANCE : distance;
}
// ==============================
// SERVO SCANNING
// ==============================
float scanDirection(int angle) {
// angle: 0 = right, 90 = center, 180 = left
scanServo.write(angle);
delay(300); // Wait for servo to reach position
float d = measureDistance();
delay(50);
return d;
}
// ==============================
// AUTONOMOUS NAVIGATION
// ==============================
void autonomousLoop() {
// Look straight ahead
float frontDist = scanDirection(90);
Serial.printf("Front: %.0f cm\n", frontDist);
if (frontDist > SLOW_DISTANCE) {
// Clear ahead - full speed
moveForward(currentSpeed);
}
else if (frontDist > STOP_DISTANCE) {
// Getting close - slow down
int slowSpeed = map(frontDist, STOP_DISTANCE, SLOW_DISTANCE,
currentSpeed / 3, currentSpeed);
moveForward(slowSpeed);
}
else {
// Obstacle too close - stop and scan
motorsStop();
delay(200);
// Scan left and right
float leftDist = scanDirection(180); // Look left
float rightDist = scanDirection(0); // Look right
scanServo.write(90); // Return to center
Serial.printf("Left: %.0f cm, Right: %.0f cm\n",
leftDist, rightDist);
if (leftDist > STOP_DISTANCE && leftDist >= rightDist) {
// More room on the left - turn left
Serial.println("Turning left");
turnLeft(currentSpeed);
delay(400);
}
else if (rightDist > STOP_DISTANCE) {
// More room on the right - turn right
Serial.println("Turning right");
turnRight(currentSpeed);
delay(400);
}
else {
// Blocked on all sides - reverse and turn around
Serial.println("Dead end - reversing");
moveBackward(currentSpeed);
delay(500);
turnRight(currentSpeed);
delay(600);
}
motorsStop();
delay(100);
}
}
// ==============================
// WEB SERVER
// ==============================
const char HTML_PAGE[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1,
maximum-scale=1, user-scalable=no">
<title>Robot Car Control</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
overflow: hidden;
touch-action: none;
}
h1 { font-size: 1.3em; margin: 10px 0 5px; color: #81c784; }
.info { font-size: 0.85em; color: #aaa; margin-bottom: 8px; }
.controls {
display: flex;
gap: 15px;
margin: 8px 0;
flex-wrap: wrap;
justify-content: center;
}
.btn {
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
color: #fff;
min-width: 80px;
}
.btn-auto { background: #e65100; }
.btn-auto.active { background: #4caf50; }
.btn-stop { background: #c62828; font-size: 1.2em; padding: 12px 30px; }
#joystick-container {
width: 250px;
height: 250px;
background: radial-gradient(circle, #16213e, #0f3460);
border-radius: 50%;
border: 3px solid #444;
position: relative;
margin: 15px 0;
}
#joystick {
width: 70px;
height: 70px;
background: radial-gradient(circle, #64b5f6, #1565c0);
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 15px rgba(100,181,246,0.5);
}
.speed-control {
display: flex;
align-items: center;
gap: 10px;
margin: 5px 0;
}
input[type=range] { width: 180px; }
#distance { font-size: 1.1em; color: #ffb74d; margin: 5px 0; }
</style>
</head>
<body>
<h1>Robot Car Control</h1>
<p class="info" id="status">Connecting...</p>
<p id="distance">Distance: -- cm</p>
<div class="controls">
<button class="btn btn-auto" id="autoBtn"
onclick="toggleAuto()">AUTO: OFF</button>
<button class="btn btn-stop"
onclick="sendCmd('/stop')">STOP</button>
</div>
<div class="speed-control">
<span>Speed:</span>
<input type="range" min="80" max="255" value="200"
id="speedSlider" oninput="updateSpeed(this.value)">
<span id="speedVal">200</span>
</div>
<div id="joystick-container">
<div id="joystick"></div>
</div>
<script>
let autoMode = false;
let sending = false;
let lastCmd = '';
function sendCmd(cmd) {
if (sending && cmd === lastCmd) return;
sending = true;
lastCmd = cmd;
fetch(cmd).then(r => r.text()).then(t => {
sending = false;
}).catch(e => { sending = false; });
}
function toggleAuto() {
autoMode = !autoMode;
const btn = document.getElementById('autoBtn');
btn.textContent = 'AUTO: ' + (autoMode ? 'ON' : 'OFF');
btn.classList.toggle('active', autoMode);
sendCmd(autoMode ? '/auto' : '/manual');
}
function updateSpeed(val) {
document.getElementById('speedVal').textContent = val;
sendCmd('/speed?v=' + val);
}
// Joystick logic
const container = document.getElementById('joystick-container');
const stick = document.getElementById('joystick');
const centerX = 125, centerY = 125, maxR = 90;
let dragging = false;
function handleMove(clientX, clientY) {
const rect = container.getBoundingClientRect();
let x = clientX - rect.left - centerX;
let y = clientY - rect.top - centerY;
// Clamp to circle
const dist = Math.sqrt(x*x + y*y);
if (dist > maxR) { x = x/dist*maxR; y = y/dist*maxR; }
stick.style.left = (centerX + x) + 'px';
stick.style.top = (centerY + y) + 'px';
// Normalize to -100..100
const nx = Math.round(x / maxR * 100);
const ny = Math.round(-y / maxR * 100); // Invert Y
sendCmd('/joy?x=' + nx + '&y=' + ny);
}
function resetStick() {
stick.style.left = centerX + 'px';
stick.style.top = centerY + 'px';
sendCmd('/stop');
dragging = false;
}
container.addEventListener('mousedown', e => { dragging = true; });
document.addEventListener('mouseup', e => { if (dragging) resetStick(); });
document.addEventListener('mousemove', e => {
if (dragging) handleMove(e.clientX, e.clientY);
});
container.addEventListener('touchstart', e => {
dragging = true;
e.preventDefault();
});
document.addEventListener('touchend', e => { if (dragging) resetStick(); });
document.addEventListener('touchmove', e => {
if (dragging) {
handleMove(e.touches[0].clientX, e.touches[0].clientY);
e.preventDefault();
}
}, { passive: false });
// Poll distance and status
setInterval(() => {
fetch('/status').then(r => r.json()).then(d => {
document.getElementById('distance').textContent =
'Distance: ' + d.dist + ' cm';
document.getElementById('status').textContent =
'Mode: ' + d.mode + ' | IP: ' + d.ip;
}).catch(e => {});
}, 500);
</script>
</body>
</html>
)rawliteral";
void handleRoot() {
server.send(200, "text/html", HTML_PAGE);
}
void handleJoystick() {
if (autonomousMode) {
server.send(200, "text/plain", "In auto mode");
return;
}
int x = server.arg("x").toInt(); // -100 to 100 (left/right)
int y = server.arg("y").toInt(); // -100 to 100 (back/forward)
// Convert joystick X,Y to differential drive (left/right motor speeds)
// Y = forward/backward thrust, X = turning
int leftSpeed = constrain(y + x, -100, 100);
int rightSpeed = constrain(y - x, -100, 100);
// Scale from -100..100 to -currentSpeed..currentSpeed
leftSpeed = map(leftSpeed, -100, 100, -currentSpeed, currentSpeed);
rightSpeed = map(rightSpeed, -100, 100, -currentSpeed, currentSpeed);
setMotors(leftSpeed, rightSpeed);
server.send(200, "text/plain", "OK");
}
void handleStop() {
autonomousMode = false;
motorsStop();
server.send(200, "text/plain", "Stopped");
}
void handleAuto() {
autonomousMode = true;
scanServo.write(90); // Center the sensor
server.send(200, "text/plain", "Autonomous mode ON");
}
void handleManual() {
autonomousMode = false;
motorsStop();
server.send(200, "text/plain", "Manual mode ON");
}
void handleSpeed() {
currentSpeed = constrain(server.arg("v").toInt(), 0, 255);
server.send(200, "text/plain", String(currentSpeed));
}
void handleStatus() {
float dist = measureDistance();
String json = "{\"dist\":" + String(dist, 0)
+ ",\"mode\":\"" + (autonomousMode ? "Auto" : "Manual") + "\""
+ ",\"ip\":\"" + WiFi.localIP().toString() + "\""
+ ",\"speed\":" + String(currentSpeed)
+ "}";
server.send(200, "application/json", json);
}
// ==============================
// SETUP & LOOP
// ==============================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n=== RC Robot Car ===");
// Initialize motors
motorsInit();
Serial.println("Motors initialized");
// Initialize ultrasonic sensor
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
// Initialize servo
scanServo.attach(SERVO_PIN);
scanServo.write(90); // Center position
delay(500);
Serial.println("Servo centered");
// Connect WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nConnected! IP: %s\n",
WiFi.localIP().toString().c_str());
} else {
Serial.println("\nWiFi failed! Starting AP mode...");
// Fallback: create own access point
WiFi.softAP("RobotCar", "12345678");
Serial.printf("AP started. Connect to 'RobotCar' WiFi.\n");
Serial.printf("Open http://%s\n",
WiFi.softAPIP().toString().c_str());
}
// Setup web server routes
server.on("/", handleRoot);
server.on("/joy", handleJoystick);
server.on("/stop", handleStop);
server.on("/auto", handleAuto);
server.on("/manual", handleManual);
server.on("/speed", handleSpeed);
server.on("/status", handleStatus);
server.begin();
Serial.println("Web server started");
Serial.println("Ready! Open the IP address in your phone browser.");
}
void loop() {
server.handleClient();
if (autonomousMode) {
autonomousLoop();
}
delay(10);
}🔗Step 6: Upload and Initial Test
- Connect the ESP32 via USB (leave the battery disconnected for now)
- Upload the sketch from the Arduino IDE
- Open Serial Monitor at 115200 baud
- Note the IP address printed after WiFi connection
- Open that IP address in your phone's browser
At this point, you should see the joystick interface. The motors will not spin yet because there is no battery power to the L298N, but you can verify that the web interface loads and the distance sensor reads values.
🔗Step 7: Motor Direction Test
Before assembling everything on the chassis, verify the motor directions:
- Connect the battery pack through the switch
- On the web interface, push the joystick forward
- Both motors should spin the same way (forward)
- If one motor spins backward, swap the two wires for that motor on the L298N output terminals (swap OUT1/OUT2 or OUT3/OUT4)
- Push the joystick left/right and verify the car would turn correctly (left = left motor slower or reversed, right motor faster)
Tip: If the car veers to one side when driving straight, the motors have slightly different speeds. You can add a software trim factor: multiply one motor's speed by 0.9 or 0.95 to compensate.
🔗Step 8: Assemble on the Chassis
Mount the components on the chassis in this general layout:
Front (caster wheel)
┌──────────────────────┐
│ [Servo+HC-SR04] │
│ │
│ [Breadboard] │
│ [ESP32] │
│ │
│ [L298N] │
│ │
│ [Battery Pack] │
│ │
│ [Motor L] [Motor R] │
└──────────────────────┘
Back (drive wheels)Key mounting tips:
- The servo and HC-SR04 go at the front so the sensor has a clear view
- The L298N should be near the motors to keep motor wires short (short wires reduce electrical noise)
- The battery pack can go underneath the chassis to lower the center of gravity
- Keep the ESP32 accessible so you can reach the USB port for reprogramming
- Use zip ties, double-sided tape, or standoffs to secure everything
🔗Step 9: Test Autonomous Mode
- Place the car on the floor with some obstacles (books, boxes, chair legs)
- Open the web interface on your phone
- Press the AUTO button
- The car should drive forward, slow down near obstacles, and turn to avoid them
- Press STOP at any time to halt the car
The autonomous algorithm works like this:
graph TD
A[Measure front distance] --> B{> 50 cm?}
B -->|Yes| C[Drive forward<br/>full speed]
C --> A
B -->|No| D{> 25 cm?}
D -->|Yes| E[Drive forward<br/>reduced speed]
E --> A
D -->|No| F[Stop]
F --> G[Scan left]
G --> H[Scan right]
H --> I{Left > Right<br/>AND Left > 25cm?}
I -->|Yes| J[Turn left]
I -->|No| K{Right > 25cm?}
K -->|Yes| L[Turn right]
K -->|No| M[Reverse and<br/>turn around]
J --> A
L --> A
M --> A🔗The Joystick Math: Differential Drive
The joystick gives us an X value (left/right, -100 to 100) and a Y value (forward/backward, -100 to 100). We need to convert these into separate left and right motor speeds. This is called differential drive or tank mixing.
The formula used in the code is:
$$\text{leftSpeed} = Y + X$$ $$\text{rightSpeed} = Y - X$$
For example:
- Joystick full forward ($X=0, Y=100$): left = 100, right = 100 (both motors forward, straight)
- Joystick full right ($X=100, Y=0$): left = 100, right = -100 (pivot right)
- Joystick forward-right ($X=50, Y=100$): left = 150 (clamped to 100), right = 50 (wide right turn)
The values are clamped to the -100 to 100 range and then mapped to the actual PWM range ($0{-}255$).
🔗Common Issues and Solutions
| Problem | Cause | Fix |
|---|---|---|
| Motors do not spin | No battery power, or ENA/ENB jumpers still installed | Connect battery, remove ENA/ENB jumpers on the L298N |
| One motor spins wrong direction | Motor wires swapped | Swap the two wires for that motor on the L298N output terminals |
| Car veers to one side | Motors have different speeds | Add a trim multiplier in code (e.g., rightSpeed *= 0.93) |
| ESP32 resets when motors start | Voltage drop / brownout | Ensure common GND. Add a 470uF capacitor across the L298N 5V and GND pins |
| Servo jitters or buzzes | Insufficient current or noisy signal | Power servo from a dedicated 5V supply. Add a 100uF capacitor near the servo |
| HC-SR04 reads 0 or max | Bad wiring or echo timeout | Check TRIG/ECHO connections. Ensure the sensor is not blocked or angled at the floor |
| Web page doesn't load | Wrong IP, or WiFi not connected | Check Serial Monitor for IP. If WiFi fails, the code creates an AP named "RobotCar" |
| Joystick is unresponsive | Browser issue or WiFi lag | Try a different browser. Reduce WiFi distance. The car must be on the same network |
| Motors run at full speed, no PWM | ENA/ENB jumpers still installed | Remove the jumpers so the ESP32 controls speed via PWM |
| Car flips or tips over | Top-heavy or turning too fast | Lower the battery to the bottom of the chassis. Reduce max speed |
| "ESP32Servo.h not found" | Library not installed | Install "ESP32Servo" by Kevin Harrington from Library Manager |
| AP mode but no web page | Phone auto-switches to mobile data | Disable mobile data temporarily so the phone stays on the RobotCar WiFi |
🔗Improving Motor Control
🔗Minimum PWM Threshold
Most DC motors will not spin below a certain PWM duty cycle. They just hum without rotating. This is because the motor needs a minimum voltage to overcome friction. For typical TT gearbox motors, the minimum useful PWM value is around 60-80 (out of 255).
In the joystick handler, you can add a dead zone:
// Add after calculating leftSpeed / rightSpeed:
if (abs(leftSpeed) < 60) leftSpeed = 0;
if (abs(rightSpeed) < 60) rightSpeed = 0;🔗Acceleration Ramping
Sudden speed changes are hard on the motors and battery, and can cause the ESP32 to brownout. You can add smooth acceleration by gradually changing the speed:
void rampMotors(int targetLeft, int targetRight, int stepDelay) {
static int curLeft = 0, curRight = 0;
while (curLeft != targetLeft || curRight != targetRight) {
if (curLeft < targetLeft) curLeft++;
else if (curLeft > targetLeft) curLeft--;
if (curRight < targetRight) curRight++;
else if (curRight > targetRight) curRight--;
setMotors(curLeft, curRight);
delay(stepDelay);
}
}🔗Battery Life
With a 2S 18650 pack ($2 \times 2500\,\text{mAh}$ in series = $2500\,\text{mAh}$ at $7.4\,\text{V}$), the run time depends on driving style:
| Driving Style | Avg Current Draw | Estimated Run Time |
|---|---|---|
| Gentle driving (low speed) | ~400 mA | ~6 hours |
| Normal driving | ~700 mA | ~3.5 hours |
| Aggressive (full speed, lots of turns) | ~1.2 A | ~2 hours |
| Autonomous mode | ~600 mA | ~4 hours |
Low battery warning: When the 2S pack drops below about $6.4\,\text{V}$, the L298N's 5V regulator may not have enough headroom, causing ESP32 brownouts. If the car starts behaving erratically, recharge the batteries.
🔗Extensions
- Speed display on OLED: Add an SSD1306 OLED to show the current speed, distance, mode, and battery voltage.
- Line following: Add two IR line-tracking sensors to the bottom of the car for line-following mode.
- Headlights: Wire two white LEDs to a GPIO pin so the car can light up dark areas.
- Horn: Add a buzzer triggered by a button on the web interface.
- Camera: Mount an ESP32-CAM as a separate module on the car, streaming video to your phone while driving. Control would come from the main ESP32, video from the ESP32-CAM.
- Encoders: Add rotary encoders to the motor shafts for precise distance tracking and PID speed control.
- Bluetooth control: Replace WiFi with Bluetooth Classic SPP for lower-latency control, paired with a phone app or custom gamepad.
- GPS navigation: Add a GPS module and have the car navigate to waypoints outdoors.