Community project

ESP32 Servo Control

ESP32
Photo of ESP32 Servo Control
Generated with AI

Zarf Parwez

Last updated August 11, 2026

This project builds an autonomous rover that uses an ESP32 microcontroller and camera to capture images, send them to a vision AI service, and respond with servo-controlled movements. The rover interprets AI responses (FORWARD, BACKWARD, LEFT, RIGHT, STOP, WIGGLE) to navigate autonomously, making it ideal for exploring obstacle courses or following visual cues.

The guide provides a complete wiring diagram showing how to connect two MG90S micro servos to the ESP32 for differential drive control, a parts list including the regulated 5V power supply required for reliable servo operation, and step-by-step assembly instructions for preparing the rover chassis and setting mechanical neutral. Firmware code is included with WiFi and secure vision-AI integration, allowing the rover to make real-time decisions based on camera input.

Wiring diagram

Wiring diagram for ESP32 Servo Control

Gather all the parts

QtyComponent
1

MG90S Micro Servo

continuous rotation

Tower Pro MG90S 9g metal-gear micro servo motor. Controlled via a standard 50 Hz PWM signal (1–2 ms pulse width). Operating voltage 4.8 V–6 V. Three-wire interface: VCC (red), GND (brown/black), and Signal (orange/yellow). Stall torque 1.8 kg·cm at 4.8 V / 2.2 kg·cm at 6 V. Rotation range 0°–180°.

1

MG90S Micro Servo

continuous rotation

Tower Pro MG90S 9g metal-gear micro servo motor. Controlled via a standard 50 Hz PWM signal (1–2 ms pulse width). Operating voltage 4.8 V–6 V. Three-wire interface: VCC (red), GND (brown/black), and Signal (orange/yellow). Stall torque 1.8 kg·cm at 4.8 V / 2.2 kg·cm at 6 V. Rotation range 0°–180°.

1

Regulated 5 V, 3 A DC power supply

5 V 3 A minimum

External regulated 5 V supply for two continuous-rotation servos and the ESP32-CAM 5 V pin. Use a 3 A or higher supply to tolerate servo startup/stall current.

Assemble it in 4 steps

1. Prepare the rover chassis

Mount the two continuous-rotation servos on opposite sides of a small chassis with their output shafts facing outward. Attach one wheel to each servo horn and add a low-friction rear skid or caster. Place the ESP32-CAM so its camera looks forward and has an unobstructed view.

  • Label the left and right servo before wiring; these labels assume forward is in the camera direction.
  • Keep the camera clear of wheels, wires, and the chassis edge.
  • Do not let the rover operate near a desk edge, stairs, liquids, or loose cables. The camera-only design cannot reliably detect every hazard.

2. Distribute the regulated 5 V power

With the 5 V supply unplugged, connect its 5V OUT to both servo red/VCC wires and to the ESP32-CAM 5V pin. Connect its GND OUT to both servo brown/black GND wires and to an ESP32-CAM GND pin. This common ground connection is required for reliable servo control.

  • Use short, adequately thick power wiring for the servo supply.
  • A 5 V supply rated at 3 A or more is required; the servos can cause high startup current.
  • Never power the servos from the ESP32-CAM 3.3 V pin or from a weak USB adapter.
  • Check polarity carefully: reversed 5 V and GND can permanently damage the ESP32-CAM and servos.

3. Connect the two servo signal wires

Connect the left servo SIG wire (usually orange/yellow/white) to ESP32-CAM GPIO16. Connect the right servo SIG wire to ESP32-CAM GPIO17. Do not connect either signal wire to the 5 V rail.

  • GPIO16 and GPIO17 are the safe available outputs when the built-in camera is in use.
  • Keep signal wires away from the camera ribbon and high-current motor wiring where practical.
  • Do not use camera-reserved pins for servo signals; doing so prevents the OV2640 camera from operating.

4. Power-on and set mechanical neutral

Lift the wheels off the desk, then power the regulated 5 V supply. The firmware initially commands a 1500 microsecond neutral pulse, so both wheels should remain stopped. If a wheel creeps, adjust that continuous-rotation servo's physical neutral trim screw. If a wheel moves backward when commanded forward later, reverse that servo mechanically or swap its direction terms in firmware.

  • Perform neutral adjustment before allowing it to drive on the desk.
  • The ESP32-CAM must be flashed through Schematik's Deploy button after you enter Wi-Fi and relay details in the firmware.
  • Keep fingers away from the wheels during this test.
  • The supplied firmware intentionally remains stopped until valid Wi-Fi and secure relay settings are entered.

Review all connections

1. Connections between "servo_power_supply" and "ESP32"

Functionservo_power_supplyESP32
power5V OUT5V
groundGND OUTGND

2. Connections between "left_drive_servo" and "ESP32"

Functionleft_drive_servoESP32
powerVCC5V
groundGNDGND
pwmSIGGPIO 16

3. Connections between "right_drive_servo" and "ESP32"

Functionright_drive_servoESP32
powerVCC5V
groundGNDGND
pwmSIGGPIO 17

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ESP32Servo.h>
#include "esp_camera.h"

// Enter Wi-Fi details and the URL of your secure vision-AI relay.
// The relay receives a JPEG and returns one word: STOP, FORWARD, BACKWARD, LEFT, RIGHT, or WIGGLE.
// Keep any AI-provider API key on that relay, never on this rover.

// Forward declarations
void stopDrive();
void driveFor(const String &action, uint32_t durationMs);
String actionFromReply(const String &reply);
bool connectWiFi();
String askVisionAI(camera_fb_t *frame);
bool initCamera();

static const char *WIFI_SSID = "YOUR_WIFI_NAME";
static const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
static const char *VISION_ENDPOINT = "https://YOUR-SECURE-VISION-RELAY.example/analyze";

static const int LEFT_SERVO_PIN = 16;
static const int RIGHT_SERVO_PIN = 17;
static const int STOP_US = 1500;
static const int DRIVE_OFFSET_US = 115;
static const uint32_t OBSERVE_INTERVAL_MS = 6000;
static const uint32_t MAX_MOVE_MS = 350;

#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

Servo leftServo;
Servo rightServo;
uint32_t lastObservationMs = 0;

void stopDrive() {
  leftServo.writeMicroseconds(STOP_US);
  rightServo.writeMicroseconds(STOP_US);
}

void driveFor(const String &action, uint32_t durationMs) {
  int leftPulse = STOP_US;
  int rightPulse = STOP_US;
  if (action == "FORWARD") { leftPulse += DRIVE_OFFSET_US; rightPulse -= DRIVE_OFFSET_US; }
  else if (action == "BACKWARD") { leftPulse -= DRIVE_OFFSET_US; rightPulse += DRIVE_OFFSET_US; }
  else if (action == "LEFT") { leftPulse -= DRIVE_OFFSET_US; rightPulse -= DRIVE_OFFSET_US; }
  else if (action == "RIGHT") { leftPulse += DRIVE_OFFSET_US; rightPulse += DRIVE_OFFSET_US; }
  else if (action == "WIGGLE") {
    leftServo.writeMicroseconds(STOP_US + DRIVE_OFFSET_US);
    rightServo.writeMicroseconds(STOP_US + DRIVE_OFFSET_US);
    delay(durationMs / 2);
    leftServo.writeMicroseconds(STOP_US - DRIVE_OFFSET_US);
    rightServo.writeMicroseconds(STOP_US - DRIVE_OFFSET_US);
    delay(durationMs / 2);
    stopDrive();
    return;
  } else { stopDrive(); return; }
  leftServo.writeMicroseconds(leftPulse);
  rightServo.writeMicroseconds(rightPulse);
  delay(durationMs);
  stopDrive();
}

String actionFromReply(const String &reply) {
  const char *actions[] = {"STOP", "FORWARD", "BACKWARD", "LEFT", "RIGHT", "WIGGLE"};
  for (uint8_t i = 0; i < 6; i++) if (reply.indexOf(actions[i]) >= 0) return String(actions[i]);
  return "STOP";
}

bool connectWiFi() {
  if (WiFi.status() == WL_CONNECTED) return true;
  if (String(WIFI_SSID).startsWith("YOUR_")) return false;
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  uint32_t started = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - started < 15000) delay(250);
  return WiFi.status() == WL_CONNECTED;
}

String askVisionAI(camera_fb_t *frame) {
  if (!connectWiFi() || String(VISION_ENDPOINT).indexOf("YOUR-") >= 0) return "STOP";
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  if (!http.begin(client, VISION_ENDPOINT)) return "STOP";
  http.addHeader("Content-Type", "image/jpeg");
  http.addHeader("X-Rover-Prompt", "You are Curio, a playful curious desk rover. Inspect the image and reply exactly with one safe action: STOP, FORWARD, BACKWARD, LEFT, RIGHT, or WIGGLE. Prefer STOP. Never move toward people, hands, desk edges, stairs, cables, cups, or uncertainty. Movement is capped at 350 ms.");
  int status = http.POST(frame->buf, frame->len);
  String response = status == HTTP_CODE_OK ? http.getString() : "STOP";
  http.end();
  return actionFromReply(response);
}

bool initCamera() {
  camera_config_t config = {};
  config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM; config.pin_pclk = PCLK_GPIO_NUM; config.pin_vsync = VSYNC_GPIO_NUM; config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM; config.pin_sccb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = FRAMESIZE_QVGA;
  config.jpeg_quality = 14; config.fb_count = 1;
  return esp_camera_init(&config) == ESP_OK;
}

void setup() {
  Serial.begin(115200);
  leftServo.setPeriodHertz(50); rightServo.setPeriodHertz(50);
  leftServo.attach(LEFT_SERVO_PIN, 500, 2500); rightServo.attach(RIGHT_SERVO_PIN, 500, 2500);
  stopDrive();
  if (!initCamera()) Serial.println("Camera failed; rover remains stopped.");
}

void loop() {
  if (millis() - lastObservationMs < OBSERVE_INTERVAL_MS) return;
  lastObservationMs = millis();
  camera_fb_t *frame = esp_camera_fb_get();
  if (frame == nullptr) { stopDrive(); return; }
  String action = askVisionAI(frame);
  esp_camera_fb_return(frame);
  Serial.printf("Curio chose: %s\n", action.c_str());
  driveFor(action, MAX_MOVE_MS);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik