Community project
ESP32 Servo Control
Zarf Parwez
Published August 9, 2026 · Updated August 11, 2026
Generated with AIThis 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
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| MG90S Micro Servocontinuous rotation | 1 | 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°. |
| MG90S Micro Servocontinuous rotation | 1 | 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°. |
| Regulated 5 V, 3 A DC power supply5 V 3 A minimum | 1 | 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. |
Assembly
4 stepsPrepare 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.
- Tip: Label the left and right servo before wiring; these labels assume forward is in the camera direction.
- Tip: 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.
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.
- Tip: Use short, adequately thick power wiring for the servo supply.
- Tip: 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.
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.
- Tip: GPIO16 and GPIO17 are the safe available outputs when the built-in camera is in use.
- Tip: 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.
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.
- Tip: Perform neutral adjustment before allowing it to drive on the desk.
- Tip: 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.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 5V | servo_power_supply 5V OUT | power |
| GND | servo_power_supply GND OUT | ground |
| 5V | left_drive_servo VCC | power |
| GND | left_drive_servo GND | ground |
| GPIO 16 | left_drive_servo SIG | pwm |
| 5V | right_drive_servo VCC | power |
| GND | right_drive_servo GND | ground |
| GPIO 17 | right_drive_servo SIG | pwm |
Firmware
ESP32#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);
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
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.