Are you interested in making a remote-controlled car using ESP32 and NodeMCU? In this project, we will use a joystick module to control a car wirelessly. This is a fun and simple way to explore ESP-NOW communication and motor control!
What You Need
To build this joystick-controlled car, you will need:
- ESP32 (for the remote controller)
- NodeMCU (ESP8266) (for the car)
- Joystick module
- L298N motor driver
- Two DC motors (500 RPM Johnson motors)
- Two wheels (70mm diameter, 20mm width)
- LM2596 voltage regulator (to power NodeMCU)
- Power source (3S battery or similar)
- Jumper wires
How It Works
- The ESP32 reads joystick movements and sends commands wirelessly using ESP-NOW.
- The NodeMCU receives these commands and controls the motors accordingly.
- The car moves forward, backward, left, and right based on joystick input.
Wiring Connections
Remote (ESP32 + Joystick Module)
- Joystick VCC → ESP32 3.3V
- Joystick GND → ESP32 GND
- Joystick X-axis → ESP32 GPIO 34
- Joystick Y-axis → ESP32 GPIO 35
Car (NodeMCU + L298N Motor Driver + Motors)
- Motor 1 IN1 → NodeMCU D1
- Motor 1 IN2 → NodeMCU D2
- Motor 2 IN1 → NodeMCU D3
- Motor 2 IN2 → NodeMCU D4
- Motor 1 ENA → NodeMCU D5
- Motor 2 ENB → NodeMCU D6
- LM2596 Output (3.3V/5V) → NodeMCU Vin
- LM2596 Ground → NodeMCU GND
ESP32 Code for the Remote
#include <esp_now.h>
#include <WiFi.h>
// Define DEBUG to enable or disable serial debug statements
//#define DEBUG // Comment this line to disable debug statements
const int xPin = 34; // X-axis connected to GPIO 34
const int yPin = 35; // Y-axis connected to GPIO 35
// Define thresholds for dead zone (adjust as needed)
const float DEAD_ZONE_THRESHOLD = 0.2; // Joystick values within ±0.2V are considered centered
uint8_t broadcastAddress[] = {0xCC, 0x50, 0xE3, 0x0D, 0x15, 0x09};
typedef struct struct_message {
char command[32];
int b;
} struct_message;
struct_message myData;
esp_now_peer_info_t peerInfo;
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
#ifdef DEBUG
Serial.print("\r\nLast Packet Send Status:\t");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
#endif
}
void setup() {
#ifdef DEBUG
Serial.begin(115200);
#endif
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
#ifdef DEBUG
Serial.println("Error initializing ESP-NOW");
#endif
return;
}
esp_now_register_send_cb(OnDataSent);
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
#ifdef DEBUG
Serial.println("Failed to add peer");
#endif
return;
}
}
void loop() {
int xValue = analogRead(xPin); // Read X-axis value
int yValue = analogRead(yPin); // Read Y-axis value
float xVoltage = (xValue * 3.3) / 4095; // Convert to voltage (0-3.3V)
float yVoltage = (yValue * 3.3) / 4095; // Convert to voltage (0-3.3V)
#ifdef DEBUG
Serial.print("X Voltage: "); Serial.print(xVoltage);
Serial.print(" | Y Voltage: "); Serial.println(yVoltage);
#endif
// Check if the joystick is within the dead zone (centered)
if (abs(xVoltage - 1.65) < DEAD_ZONE_THRESHOLD && abs(yVoltage - 1.65) < DEAD_ZONE_THRESHOLD) {
strcpy(myData.command, "S"); // Stop (centered)
myData.b = 0; // No movement
}
// Check for movement outside the dead zone
else if (xVoltage > (1.65 + DEAD_ZONE_THRESHOLD)) {
strcpy(myData.command, "F"); // Forward
myData.b = xVoltage;
} else if (xVoltage < (1.65 - DEAD_ZONE_THRESHOLD)) {
strcpy(myData.command, "B"); // Backward
myData.b = xVoltage;
} else if (yVoltage > (1.65 + DEAD_ZONE_THRESHOLD)) {
strcpy(myData.command, "L"); // Left
myData.b = yVoltage;
} else if (yVoltage < (1.65 - DEAD_ZONE_THRESHOLD)) {
strcpy(myData.command, "R"); // Right
myData.b = yVoltage;
}
// Send the command via ESP-NOW
esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
#ifdef DEBUG
if (result == ESP_OK) {
Serial.println("Sent with success");
} else {
Serial.println("Error sending the data");
}
#endif
delay(20); // Small delay to make output readable
}
NodeMCU Code for the Car
#include <ESP8266WiFi.h>
#include <espnow.h>
// Structure to receive data
typedef struct struct_message {
char a[32];
int b;
float c;
bool d;
} struct_message;
struct_message myData;
// Pin definitions
#define LED_PIN 2 // Onboard LED pin (D4 on ESP8266)
#define M1_ENA D5 // Motor 1 PWM (Speed)
#define M2_ENB D6 // Motor 1 PWM (Speed)
#define M1_IN1 D1 // Motor 1 input 1
#define M1_IN2 D2 // Motor 1 input 2
#define M2_IN1 D3 // Motor 2 input 1
#define M2_IN2 D4 // Motor 2 input 2
// Default speeds (adjust these values experimentally)
int speedLeft = 800; // 0-1023 (80% speed)
int speedRight = 700; // Right motor slightly slower
// Callback function when data is received
void OnDataRecv(uint8_t *mac, uint8_t *incomingData, uint8_t len) {
memcpy(&myData, incomingData, sizeof(myData));
Serial.print("Bytes received: ");
Serial.println(len);
Serial.print("Data received: ");
Serial.println(myData.a);
Serial.print("value received: ");
Serial.println(myData.b);
// Control motors based on received character
if (strcmp(myData.a, "F") == 0) {
// Move forward
analogWrite(M1_ENA, speedLeft);
analogWrite(M2_ENB, speedRight);
digitalWrite(M1_IN1, HIGH); digitalWrite(M1_IN2, LOW);
digitalWrite(M2_IN1, HIGH); digitalWrite(M2_IN2, LOW);
digitalWrite(LED_PIN, LOW); // Turn on LED (inverted logic on ESP8266)
} else if (strcmp(myData.a, "B") == 0) {
// Move backward
analogWrite(M1_ENA, speedLeft);
analogWrite(M2_ENB, speedRight);
digitalWrite(M1_IN1, LOW); digitalWrite(M1_IN2, HIGH);
digitalWrite(M2_IN1, LOW); digitalWrite(M2_IN2, HIGH);
digitalWrite(LED_PIN, HIGH); // Turn off LED (inverted logic on ESP8266)
} else if (strcmp(myData.a, "L") == 0) {
// Turn left
analogWrite(M1_ENA, speedLeft);
analogWrite(M2_ENB, speedRight);
digitalWrite(M1_IN1, LOW); digitalWrite(M1_IN2, HIGH);
digitalWrite(M2_IN1, HIGH); digitalWrite(M2_IN2, LOW);
} else if (strcmp(myData.a, "R") == 0) {
// Turn right
analogWrite(M1_ENA, speedLeft);
analogWrite(M2_ENB, speedRight);
digitalWrite(M1_IN1, HIGH); digitalWrite(M1_IN2, LOW);
digitalWrite(M2_IN1, LOW); digitalWrite(M2_IN2, HIGH);
}
else if (strcmp(myData.a, "S") == 0) {
// Turn right
analogWrite(M1_ENA, 0);
analogWrite(M2_ENB, 0);
digitalWrite(M1_IN1, HIGH); digitalWrite(M1_IN2, LOW);
digitalWrite(M2_IN1, LOW); digitalWrite(M2_IN2, HIGH);
} else {
// Stop motors
analogWrite(M1_ENA, 0);
analogWrite(M2_ENB, 0);
// digitalWrite(M1_IN1, LOW); digitalWrite(M1_IN2, LOW);
// digitalWrite(M2_IN1, LOW); digitalWrite(M2_IN2, LOW);
}
}
void setup() {
// Initialize Serial Monitor
Serial.begin(115200);
// Initialize pins
pinMode(LED_PIN, OUTPUT);
pinMode(M1_IN1, OUTPUT);
pinMode(M1_IN2, OUTPUT);
pinMode(M2_IN1, OUTPUT);
pinMode(M2_IN2, OUTPUT);
// Ensure motors and LED are off initially
digitalWrite(LED_PIN, HIGH); // LED off (inverted logic on ESP8266)
digitalWrite(M1_IN1, LOW); digitalWrite(M1_IN2, LOW);
digitalWrite(M2_IN1, LOW); digitalWrite(M2_IN2, LOW);
// Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
// Initialize ESP-NOW
if (esp_now_init() != 0) {
Serial.println("Error initializing ESP-NOW");
return;
}
// Register the receive callback
esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
// Nothing to do here
}
How to Upload the Code
- Upload the remote code to your ESP32.
- Upload the car code to your NodeMCU.
- Power up both devices and check serial monitor outputs for debugging.
Testing the Car
- Move the joystick forward → The car moves forward.
- Move the joystick backward → The car moves backward.
- Move the joystick left → The car turns left.
- Move the joystick right → The car turns right.
- Release the joystick (center position) → The car stops.
3d Printed parts
Since when i made this car. I had Johnson motors of 500RPM. But i did not had any bracket so i designed the brackets in FreeCAD and printed on Creality ender 3 v3 KE printer.
Leave a Reply