Build Your Own ESP32 RC Car: Wireless Joystick Controller Guide for Beginners

The “I Built My Own RC Car” Moment
I still remember holding the joystick for the first time, pushing it forward, and watching the little robot chassis zoom across my room.
Not a toy from a store. Not a kit where everything just snaps together. Something I built. Wires, motors, ESP32s, code—all of it.
That feeling? It never gets old.
In this tutorial, I’m going to show you exactly how to build your own wireless ESP32 RC car using two ESP32 boards and a joystick module. One ESP32 acts as the transmitter (controller) , reading the joystick. The other is the receiver (car) , driving four DC motors. They talk to each other wirelessly using ESP-NOW—fast, simple, and perfect for beginners. That’s how you build your ESP32 RC Car.
What You’ll Build
| Component (Side) | What It Does |
|---|---|
| Controller (TX) | Reads joystick X/Y values, sends them wirelessly |
| Car (RX) | Receives commands, controls 4 motors for smooth driving |
| Communication | ESP-NOW protocol—low latency, direct connection |
The Flow:
You move joystick → ESP32 reads analog values → sends data wirelessly → second ESP32 receives → motor driver spins wheels → car moves | ESP32 RC CAR
Why Build This Instead of Buying a ESP32 RC Car?
| Reason | Why It Matters |
|---|---|
| Learn Wireless Comms | Understand how ESP-NOW or WiFi works for real projects |
| Customizable | Add FPV camera, sensors, stronger motors—your call |
| Cheaper | ~2500-3500 BDT total, vs. 5000+ for a comparable toy |
| Repairable | Something breaks? You know exactly how to fix it |
| Portfolio-Ready | A perfect project for engineering students and hobbyists |
For a BUET student budget? This is a no-brainer.
What You’ll Need (Complete Parts List)
For the Controller (Transmitter)
| Component | Specs / Notes | Approx Price (BDT) |
|---|---|---|
| ESP32 Development Board | Any version works (I used a standard 30-pin) | 600-800 |
| Joystick Module | XY axes + center push button (SW) | 80-150 |
| Breadboard | Small 170 or 400 points | 50-100 |
| Jumper Wires | Male-to-Female and Male-to-Male | 50-100 |
| USB Cable | For programming and power (optional battery later) | 100-200 |
For the ESP32 RC Car (Receiver)
| Component | Specs / Notes | Approx Price (BDT) |
|---|---|---|
| ESP32 Development Board | Second one, identical to the first | 600-800 |
| 4-Wheel Car Chassis | Includes a frame, 4 DC motors, and wheels | 800-1200 |
| Motor Driver Module | L298N (easy to wire) or L9110S (compact) | 350-550 |
| 18650 Batteries | 2x, for 7.4V power | 500-700 |
| Battery Holder | For 2x 18650 batteries | 100-200 |
| Jumper Wires | Male-to-Female, for motor driver connections | 50-100 |

Quick Buy Links (Affiliate)
| Component | Where to Find |
|---|---|
| ESP32 Development Board ×2 | AliExpress / Amazon |
| 4-Wheel Car Chassis | AliExpress / Amazon |
| L298N Motor Driver | AliExpress / Amazon |
| Joystick Module | AliExpress / Amazon |
| 18650 Batteries + Holder | AliExpress / Amazon |
Step 1: Wiring the Controller (Transmitter)
This part is simple. The joystick module has 5 pins:
GND → ESP32 GND
5V (or VCC) → ESP32 VIN (5V)
VRx (X-axis, analog) → ESP32 GPIO 34 (ADC)
VRy (Y-axis, analog) → ESP32 GPIO 35 (ADC)
SW (button, digital) → ESP32 GPIO 15

Step 2: Wiring the ESP32 RC Car (Receiver)
This is where the robot comes to life. The motor driver is the bridge between the ESP32 and the powerful motors.
L298N Motor Driver to ESP32 Connections
| L298N Terminal | Connects To (ESP32) | Notes |
|---|---|---|
| IN1 | GPIO 26 | Left Motor Direction |
| IN2 | GPIO 27 | Left Motor Direction |
| IN3 | GPIO 14 | Right Motor Direction |
| IN4 | GPIO 12 | Right Motor Direction |
| ENA | GPIO 25 | Left Motor Speed (PWM) |
| ENB | GPIO 33 | Right Motor Speed (PWM) |
| +5V | ESP32 VIN (optional) | Can power ESP32 if motors not too heavy |
| GND | ESP32 GND | Common ground—critical! |
L298N to Motors and Battery
| L298N Terminal | Connects To |
|---|---|
| OUT1 & OUT2 | Left-side motors (parallel) |
| OUT3 & OUT4 | Right-side motors (parallel) |
| 12V Input | Battery + (7.4V from 18650s) |
| GND | Battery – |

Step 3: The Code – How the Magic Happens
The code is split into two parts. Let’s break down the logic first.
The Controller Logic:
Read the analog values from VRx and VRy (0-4095).
Map these values to a joystick position (Forward, Backward, Left, Right, Stop).
Package this command into a simple data structure.
Send the data wirelessly using ESP-NOW to the car’s MAC address.
The Car Logic:
Listen for incoming ESP-NOW data packets.
Read the command (e.g., ‘F’ for Forward).
Based on the command, set the 4 motor driver pins (IN1-IN4) HIGH or LOW.
Use PWM on the ENA and ENB pins to control speed smoothly.
Upload This MAC Address Scanner Sketch
Connect the car ESP32 (receiver) to your computer via USB. Copy and paste this code into Arduino IDE and upload it:
#include "WiFi.h"
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_MODE_STA);
delay(500);
}
void loop() {
Serial.println(WiFi.macAddress());
delay(1000);
}
Step 4: The Code – Controller (Transmitter)
This code runs on the ESP32 that has the joystick attached. It reads the joystick position and sends commands wirelessly to the car.
Copy and paste this code into your Arduino IDE. Upload it to the controller ESP32 (the one with the joystick).
#include <Arduino.h>
#include <esp_now.h>
#include <WiFi.h>
#include <esp_wifi.h>
#define JOY_X_PIN 34 // ADC input for joystick X
#define JOY_Y_PIN 35 // ADC input for joystick Y
// Receiver MAC Address (Your Car)
uint8_t receiverAddress[] = {0xEC, 0xE3, 0x34, 0x1A, 0xF4, 0x04};
typedef struct struct_message {
int16_t x; // Joystick X value
int16_t y; // Joystick Y value
} struct_message;
struct_message myData;
void onSent(const uint8_t *macAddr, esp_now_send_status_t status) {
Serial.print("Send Status: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
}
void setup() {
Serial.begin(115200);
// Initialize joystick pins
pinMode(JOY_X_PIN, INPUT);
pinMode(JOY_Y_PIN, INPUT);
// Set WiFi mode
WiFi.mode(WIFI_STA);
WiFi.disconnect();
// Set channel for better reliability
esp_wifi_set_promiscuous(true);
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
esp_wifi_set_promiscuous(false);
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
while (1);
}
// Register send callback
esp_now_register_send_cb(onSent);
// Add peer
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, receiverAddress, 6);
peerInfo.channel = 1;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add peer");
while (1);
}
Serial.println("ESP-NOW Transmitter Ready");
Serial.print("Target MAC: ");
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
receiverAddress[0], receiverAddress[1], receiverAddress[2],
receiverAddress[3], receiverAddress[4], receiverAddress[5]);
Serial.println(macStr);
}
void loop() {
// Read joystick values (0-4095)
int rawX = analogRead(JOY_X_PIN);
int rawY = analogRead(JOY_Y_PIN);
// Send raw values (0-4095) to maintain your existing motor control logic
myData.x = rawX;
myData.y = rawY;
// Send data via ESP-NOW
esp_err_t result = esp_now_send(receiverAddress, (uint8_t *)&myData, sizeof(myData));
if (result == ESP_OK) {
Serial.printf("Sent - X:%4d Y:%4d\n", rawX, rawY);
} else {
Serial.println("Send error");
}
delay(50); // Send every 50ms
}
Step 5: The Code – Receiver (Car)
This code runs on the ESP32 mounted on the car. It receives joystick commands and controls the motors via the L298N driver.
Copy and paste this code into your Arduino IDE. Upload it to the receiver ESP32 (the one on the car chassis).
#include <esp_now.h>
#include <WiFi.h>
// L298N Motor Control Pins (Your original pins)
#define out1 12 // Left Motor Direction 1
#define out2 14 // Left Motor Direction 2
#define out3 27 // Right Motor Direction 1
#define out4 26 // Right Motor Direction 2
typedef struct struct_message {
int16_t x; // Joystick X value (0-4095)
int16_t y; // Joystick Y value (0-4095)
} struct_message;
struct_message incomingData;
// Function declarations
void initializeMotors();
void controlMotors(int xValue, int yValue);
void stopMotors();
void moveForward();
void moveBackward();
void turnLeft();
void turnRight();
void onReceive(const uint8_t * mac, const uint8_t *incomingDataRaw, int len) {
memcpy(&incomingData, incomingDataRaw, sizeof(incomingData));
Serial.printf("Received - X:%4d Y:%4d\n", incomingData.x, incomingData.y);
// Use your existing motor control logic
controlMotors(incomingData.x, incomingData.y);
}
// Your original motor control functions
void initializeMotors() {
pinMode(out1, OUTPUT);
pinMode(out2, OUTPUT);
pinMode(out3, OUTPUT);
pinMode(out4, OUTPUT);
digitalWrite(out1, LOW);
digitalWrite(out2, LOW);
digitalWrite(out3, LOW);
digitalWrite(out4, LOW);
}
void controlMotors(int xValue, int yValue) {
// Convert joystick values to motor control
// Joystick range: 0-4095, Center: ~2048
const int center = 2700;
const int deadZone = 500;
// Check if in dead zone (center position)
if ((xValue > center - deadZone) && (xValue < center + deadZone) &&
(yValue > center - deadZone) && (yValue < center + deadZone)) {
stopMotors();
Serial.println("Action: STOP");
return;
}
// Determine primary direction based on which axis has stronger input
int xDiff = abs(xValue - center);
int yDiff = abs(yValue - center);
if (yDiff > xDiff) {
// Y-axis dominant (Forward/Backward)
if (yValue < center - deadZone) {
moveForward();
Serial.println("Action: FORWARD");
} else if (yValue > center + deadZone) {
moveBackward();
Serial.println("Action: BACKWARD");
}
} else {
if (xValue == 4095 && yValue == 4095) {
// Special case: Both axes maxed - move backward
moveBackward();
Serial.println("Action: BACKWARD");
} else if (xValue == 0 && yValue == 0) {
moveBackward();
Serial.println("Action: BACKWARD");
} else {
// X-axis dominant (Left/Right)
if (xValue < center - deadZone) {
turnLeft();
Serial.println("Action: LEFT");
} else if (xValue > center + deadZone) {
turnRight();
Serial.println("Action: RIGHT");
}
}
}
}
void moveForward() {
// Both motors forward
digitalWrite(out1, HIGH); // Left motor forward
digitalWrite(out2, LOW);
digitalWrite(out3, LOW); // Right motor forward
digitalWrite(out4, HIGH);
}
void moveBackward() {
// Both motors backward
digitalWrite(out1, LOW);
digitalWrite(out2, HIGH); // Left motor backward
digitalWrite(out3, HIGH);
digitalWrite(out4, LOW); // Right motor backward
}
void turnLeft() {
// Right motor forward, left motor stopped
digitalWrite(out1, HIGH); // Left motor stop
digitalWrite(out2, LOW);
digitalWrite(out3, HIGH); // Right motor forward
digitalWrite(out4, LOW);
}
void turnRight() {
// Left motor forward, right motor stopped
digitalWrite(out1, LOW); // Left motor forward
digitalWrite(out2, HIGH);
digitalWrite(out3, LOW); // Right motor stop
digitalWrite(out4, HIGH);
}
void stopMotors() {
digitalWrite(out1, LOW);
digitalWrite(out2, LOW);
digitalWrite(out3, LOW);
digitalWrite(out4, LOW);
}
void setup() {
Serial.begin(115200);
// Initialize motors
initializeMotors();
// Set WiFi mode
WiFi.mode(WIFI_STA);
WiFi.disconnect();
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
while(1);
}
// Register receive callback
esp_now_register_recv_cb(onReceive);
Serial.println("ESP-NOW Receiver Ready");
Serial.print("Receiver MAC: ");
Serial.println(WiFi.macAddress());
// Stop motors initially
stopMotors();
}
void loop() {
// Nothing to do here - everything handled in callback
delay(1000); // Small delay to prevent watchdog timer issues
}
What to Do After Uploading
1. Upload Controller Code:
Connect the controller ESP32 (with joystick) via USB
Select the correct board and port
Click Upload
2. Upload Receiver Code:
Disconnect the controller ESP32
Connect the car ESP32 via USB
Make sure you’ve updated the MAC address in the controller code first (if you haven’t, the car won’t receive anything yet)
Click Upload
3. Power and Test:
Power the ESP32 RC car with its 18650 batteries
Power the controller via USB power bank or its own battery
Push the joystick – your car should move!
Step 6: Upload and Test – The Moment of Truth
Program the Controller: Connect the first ESP32 to your computer. Select the correct board and port in Arduino IDE. Upload the
ESP32_RC_Car_Controllercode.Program the Car: Disconnect the first ESP32. Connect the second ESP32 to your computer. Important: You need to modify the code and put the MAC address of your controller ESP32 in the car’s code so they can pair. Upload the
ESP32_RC_Car_Receivercode.Power Up: Disconnect both ESP32s from USB. Power the car with its 18650 batteries. Power the controller via USB power bank or its own battery.
Drive! Push the joystick forward. Your ESP32 RC CAR should move!
Troubleshooting – Don’t Panic!
| Problem | Likely Cause | Fix |
|---|---|---|
| ESP32 RC Car doesn’t move | Battery not connected or insufficient voltage | Check 18650 voltage (should be >6V). Recharge batteries. |
| Car moves erratically | Common ground missing between ESP32 and L298N | Connect a GND pin from ESP32 to the GND terminal on the L298N. |
| One side doesn’t turn | L298N connection issue | Check IN1-IN4 and OUT1-OUT4 wiring. Swap motor wires to test. |
| No wireless connection | MAC address incorrect | Get the correct MAC address of your controller ESP32 (use a scanner sketch) and put it in the receiver’s code. |
| Joystick axes swapped | Wiring mismatch | Swap the VRx and VRy pins on the controller’s ESP32, or swap X/Y in the code. |
Level Up: What’s Next?
You’ve built the foundation. Now, make it yours.
| Upgrade | What You’ll Need |
|---|---|
| FPV Camera | ESP32-CAM module – stream video to your phone |
| Obstacle Avoidance | Ultrasonic (HC-SR04) sensor – make it drive itself |
| Speed Control | Use the joystick’s analog values for proportional speed, not just on/off |
| Better Power | 2S LiPo battery (7.4V) with higher discharge rate for more punch |
| Custom Controller | Build the controller into a 3D-printed box with a proper joystick knob |
| Follow Me Mode | Use a smartphone’s GPS/compass data sent to the ESP32 RC car |
Cost Breakdown (Bangladesh)
| Component | Approx Price (USD) |
|---|---|
| ESP32 ×2 | $10.00 – $13.50 |
| 4WD Chassis with Motors | $6.70 – $10.00 |
| L298N Motor Driver | $2.90 – $4.60 |
| Joystick Module | $0.70 – $1.25 |
| 18650 Batteries (2x) + Holder | $5.00 – $7.50 |
| Breadboard & Jumpers | $1.25 – $2.10 |
| Total | ~$26.50 – $38.75 |
For a fully functional, custom-built ESP32 RC car? That’s incredible value.
Final Thoughts
This project taught me something important: wireless control is not magic. It’s just two microcontrollers agreeing on a language and a few GPIO pins firing in the right sequence.
Is it perfect? No.
The turning could be sharper.
The range could be longer.
I’d love a nicer case for the controller.
But it’s mine. I built it. I understand how it works. And when something goes wrong, I know how to fix it.
That’s what Roborear is about. Not just copying tutorials – understanding them, improving them, making them your own.
Your Turn
Got the ESP32 RC car working? Modified it with an FPV camera or sensors? Stuck on a step? Drop a comment below – I read all of them.
And if you want to see this project in action, check out the video on YouTube.
FAQs
Affiliate Disclosure
Some links in this post are affiliate links. If you purchase through them, I may earn a small commission at no extra cost to you. This helps support Roborear. Thanks!






