← All projects
Automation Beginner

Servo with a Web Slider: Smooth Control Without Libraries

Control an SG90/MG996R servo from your phone: drag the slider and the shaft turns in real time. PWM comes from the built-in LEDC peripheral — no third-party libraries.

Difficulty
Beginner
Time
45 min
Category
Automation

The idea

A servo is the heart of countless DIY builds: a cat feeder, a pan camera, a blind opener, a vent damper. It's driven by 50 Hz PWM pulses: a ~0.5 ms pulse means 0°, ~2.4 ms means 180°.

The ESP32 has a hardware LEDC peripheral that generates this signal with zero CPU load. So we skip libraries entirely — just the arduino-esp32 3.x core.

Parts

  • ESP32 DevKit
  • SG90 (small) or MG996R (strong, metal gears) servo
  • For the MG996R — a separate 5–6 V ≥ 2 A supply

Wiring

Servo ESP32
Brown / black (GND) GND
Red (+5 V) 5V (VIN) — SG90 only
Orange / yellow (signal) GPIO 13

A strong servo draws peak currents above 1 A on start-up and the ESP32 will reset. Power it separately and tie the grounds together. A 470–1000 µF capacitor next to the servo helps too.

Code

#include <WiFi.h>
#include <WebServer.h>

const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASSWORD";

const int SERVO_PIN = 13;
const int PWM_FREQ  = 50;     // 50 Hz → 20 ms period
const int PWM_BITS  = 14;     // resolution 0..16383
const int MIN_US    = 500;    // pulse for 0°
const int MAX_US    = 2400;   // pulse for 180°

WebServer server(80);
int currentAngle = 90;

void writeAngle(int angle) {
  angle = constrain(angle, 0, 180);
  int us = map(angle, 0, 180, MIN_US, MAX_US);
  uint32_t duty = (uint32_t)us * ((1 << PWM_BITS) - 1) / 20000;
  ledcWrite(SERVO_PIN, duty);
  currentAngle = angle;
}

const char PAGE[] = R"rawliteral(
<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Servo</title>
<style>
  body{font-family:system-ui;text-align:center;margin:40px 16px}
  input{width:100%;max-width:420px;height:40px}
  #v{font-size:3rem;font-weight:700}
</style></head><body>
<h2>Servo</h2>
<div id="v">--°</div>
<input id="s" type="range" min="0" max="180">
<script>
const s = document.getElementById('s'), v = document.getElementById('v');
let busy = false, pending = null;
async function send(angle) {
  if (busy) { pending = angle; return; }
  busy = true;
  await fetch('/angle?value=' + angle);
  busy = false;
  if (pending !== null) { const a = pending; pending = null; send(a); }
}
s.oninput = () => { v.textContent = s.value + '°'; send(s.value); };
fetch('/angle').then(r => r.text()).then(a => { s.value = a; v.textContent = a + '°'; });
</script></body></html>
)rawliteral";

void setup() {
  Serial.begin(115200);
  ledcAttach(SERVO_PIN, PWM_FREQ, PWM_BITS);   // core 3.x API
  writeAngle(90);

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.println(WiFi.localIP());

  server.on("/", [] { server.send(200, "text/html", PAGE); });
  server.on("/angle", [] {
    if (server.hasArg("value")) writeAngle(server.arg("value").toInt());
    server.send(200, "text/plain", String(currentAngle));
  });
  server.begin();
}

void loop() {
  server.handleClient();
}

How it works

  • ledcAttach() sets up hardware PWM on the pin: 50 Hz, 14 bits.
  • writeAngle() converts the angle into a pulse width in microseconds, then into a duty value from 0 to 16383.
  • The page's JavaScript sends the new angle while you drag, but never faster than the board can answer: while a request is in flight, only the latest value is kept.

Calibration

Servos from different makers have different ranges. If the shaft buzzes and strains at the ends, narrow MIN_US/MAX_US (e.g. 600 and 2300). If it doesn't reach 180°, widen them.

Core 2.x

Older core versions used channels instead of ledcAttach:

ledcSetup(0, PWM_FREQ, PWM_BITS);
ledcAttachPin(SERVO_PIN, 0);
ledcWrite(0, duty);   // write to the channel, not the pin

Ideas

  • Pet feeder: on a schedule (NTP) rotate 90° and back — a portion of food drops out.
  • Pan-tilt for an ESP32-CAM: two servos and two sliders.
  • Smooth motion: step the angle by 1° every 15 ms instead of jumping.