← All projects
Automation Beginner

An SG90 Servo Follows a Potentiometer: 50 Hz PWM Without Libraries

How a servo reads an angle from the pulse width, why it runs from 5 V rather than 3.3 V, and how to produce a 0.5–2.4 ms pulse directly with LEDC. Turn the knob and the servo follows.

Difficulty
Beginner
Time
30 min
Category
Automation
Updated

About the project

A servo is a motor with a gearbox and its own position controller: you give it an angle, it turns there by itself and holds it. Servos drive blinds, dampers and robot claws. We start with the simplest case: the servo copies the position of a potentiometer knob.

What you need

  • ESP32 DevKit
  • SG90 servo (or an MG90S with metal gears)
  • 10 kΩ potentiometer
  • For several servos, a separate 5 V ≥ 1 A supply

Wiring

Part Wire ESP32
SG90 brown (GND) GND
red (V+) 5V (VIN)
orange (signal) GPIO13
Potentiometer outer 3V3 and GND
middle GPIO34

The servo needs 5 V: at 3.3 V it is weak and jittery. Its signal input is still happy with the ESP32's 3.3 V. At start-up an SG90 draws spikes of 500–700 mA. USB copes with one servo but not with several: the board keeps resetting on the voltage dip (brownout).

How a servo reads an angle

Every 20 ms (50 Hz) the servo gets a pulse, and its width sets the angle:

Pulse Angle
~0.5 ms 0°
~1.45 ms 90°
~2.4 ms 180°

The exact limits vary between servos, so tune them until the servo stops pushing against its end stops and humming.

PWM with LEDC

ledcAttach(pin, 50, 14) gives 50 Hz at 14-bit resolution (0…16383 over the whole 20 ms period). To get a pulse in microseconds, convert: duty = us × 16383 / 20000. The uint32_t cast keeps the multiplication from overflowing a 16-bit int. On the ESP32 int is 32-bit, but the habit is worth having.

Code

// The servo follows a potentiometer (50 Hz PWM with LEDC)
const int SERVO_PIN = 13;
const int POT_PIN = 34;

void writeAngle(int angle) {
  int us = map(angle, 0, 180, 500, 2400);             // pulse width
  ledcWrite(SERVO_PIN, (uint32_t)us * 16383 / 20000);  // share of the 20 ms period
}

void setup() {
  Serial.begin(115200);
  ledcAttach(SERVO_PIN, 50, 14);   // 50 Hz, 14-bit
}

void loop() {
  int angle = map(analogRead(POT_PIN), 0, 4095, 0, 180);
  writeAngle(angle);
  Serial.println(angle);
  delay(50);
}

Library or by hand

The ESP32Servo library does the same in one line (servo.attach(13); servo.write(90);) and shares the LEDC channels between several servos for you. Doing it by hand is useful to understand what happens, and when you need a non-standard frequency or pulse limits.

Good to know

  • Jitter. ADC noise makes the angle jump by 1–2°. Average several readings, or ignore changes smaller than 2°.
  • Humming at rest. Under load the servo keeps correcting its angle and heats up. When it does not need to hold, stop the signal: ledcWrite(SERVO_PIN, 0).
  • Common ground. If the servo has its own supply, its GND must be connected to the ESP32 GND.