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.
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.