Make a Joystick-Controlled Car with ESP32 and NodeMCU

Posted

in

,

by

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 VCCESP32 3.3V
  • Joystick GNDESP32 GND
  • Joystick X-axisESP32 GPIO 34
  • Joystick Y-axisESP32 GPIO 35

Car (NodeMCU + L298N Motor Driver + Motors)

  • Motor 1 IN1NodeMCU D1
  • Motor 1 IN2NodeMCU D2
  • Motor 2 IN1NodeMCU D3
  • Motor 2 IN2NodeMCU D4
  • Motor 1 ENANodeMCU D5
  • Motor 2 ENBNodeMCU D6
  • LM2596 Output (3.3V/5V)NodeMCU Vin
  • LM2596 GroundNodeMCU 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

  1. Upload the remote code to your ESP32.
  2. Upload the car code to your NodeMCU.
  3. Power up both devices and check serial monitor outputs for debugging.

Testing the Car

  1. Move the joystick forward → The car moves forward.
  2. Move the joystick backward → The car moves backward.
  3. Move the joystick left → The car turns left.
  4. Move the joystick right → The car turns right.
  5. 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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *