To control a servo with PWM, you need a microcontroller (like an ESP32) outputting a 50 Hz pulse-width modulated signal—typically a 1ms to 2ms pulse width—to the servo's signal wire. Meanwhile, the servo's VCC and GND must be powered by a dedicated 5V or 6V power supply capable of delivering at least 1A per standard hobby servo. Relying on the microcontroller's onboard 5V pin will cause brownouts and erratic behavior.

This guide breaks down how to select the right motor for your load, size it correctly, wire it to an ESP32, and troubleshoot the most common failure modes on the bench.

Servo vs. Stepper vs. DC: Which Motor Fits Your Load Profile?

A common mistake in embedded projects is treating servos and steppers as interchangeable. They are fundamentally different in how they deliver torque and how they demand to be driven. If you need to hold a heavy robotic arm at a specific angle without continuous power draw, a servo is your answer. If you need precise open-loop rotational positioning for a 3D printer axis, you need a stepper.

Motor Type Comparison for Embedded Projects
Motor Type Torque Curve Profile Control / Driver Needs Typical Cost (USD)
RC Servo (e.g., MG996R) Maximum torque at zero speed (stall). Drops off rapidly as speed increases. Built-in driver & potentiometer. Requires 50Hz PWM signal (1-2ms pulse). Closed-loop internally. $6 - $15
Stepper (e.g., NEMA 17) High holding torque at standstill. Torque drops significantly at high RPMs due to back-EMF. Requires external chopper driver (e.g., TMC2209, A4988) and step/dir pulse signals. Open-loop. $12 - $25 (plus driver)
Brushed DC Motor Maximum torque at stall (limited only by current). Torque decreases linearly as speed increases. Requires MOSFET or H-Bridge (e.g., L298N, DRV8871) for speed/direction control. Open-loop without encoder. $3 - $10 (plus driver)

Which motor fits your load profile? Choose a servo when you need high torque at low speeds, absolute positional accuracy within a limited arc (usually 180° or 270°), and want to avoid designing complex external driver circuits. The servo's internal control board handles the PID loop and H-bridge driving for you.

Sizing Your Servo: Torque, Load Profiles, and the 50% Rule

Servo torque is rated in kg·cm (kilogram-centimeters) or oz·in (ounce-inches). This is the stall torque—the absolute maximum force the motor can exert before it stops moving. Operating a servo continuously near its stall torque will strip the gears (even metal ones) and overheat the internal DC motor.

Callout Tip: The 50% Sizing Rule
For continuous dynamic loads, never size a servo for exactly the calculated load torque. Apply a 50% derating factor. Your calculated load should be no more than 50% of the servo's rated stall torque. This accounts for acceleration spikes, friction, and mechanical inefficiencies.

Worked Load Example: Robotic Forearm

Let's size a servo for a robotic arm lifting a payload. We will assume standard ambient temperature (25°C) and a standard 5V hobby servo.

  • Payload: 200g (0.2 kg) at the end of a 10 cm (0.1 m) forearm.
  • Arm Weight: 100g (0.1 kg) acting at its center of gravity, 5 cm (0.05 m) from the joint.

Step 1: Calculate Payload Torque
Force = mass × gravity = 0.2 kg × 9.81 m/s² = 1.96 N.
Torque = Force × distance = 1.96 N × 0.1 m = 0.196 Nm.
Converting to kg·cm (1 Nm ≈ 10.197 kg·cm): 2.0 kg·cm.

Step 2: Calculate Arm Torque
Force = 0.1 kg × 9.81 m/s² = 0.98 N.
Torque = 0.98 N × 0.05 m = 0.049 Nm ≈ 0.5 kg·cm.

Step 3: Total Load and Sizing
Total static load = 2.0 + 0.5 = 2.5 kg·cm.
Applying the 50% rule: Required servo rating = 2.5 kg·cm / 0.5 = 5.0 kg·cm.

Selection: A standard TowerPro SG90 (rated at 1.8 kg·cm) will instantly stall and overheat. You need a servo rated for at least 5 kg·cm. The TowerPro MG996R (rated at 10–13 kg·cm, typically $7) or the Feetech SCS15 (a smart serial servo, ~$18) are ideal choices here, providing the necessary overhead for acceleration and joint friction.

Wiring, PWM Signals, and Failure Signatures

Standard RC servos use a 3-wire interface. Misidentifying these terminals or underpowering them is the leading cause of fried microcontrollers and jittering mechanics.

Terminal Identification

Wire Color (Standard) Function Connection Destination
Brown or Black GND (Ground) Power Supply GND and ESP32 GND (Common ground is mandatory)
Red VCC (Power) Dedicated 5V or 6V Power Supply (Do NOT use ESP32 VIN/5V pin)
Orange, Yellow, or White Signal (PWM) ESP32 GPIO pin (e.g., GPIO 13)
Warning: The Common Ground Rule
When using an external power supply for the servo, you must connect the power supply's GND to the ESP32's GND. Without a common ground reference, the PWM signal voltage is floating relative to the servo's internal logic, resulting in erratic sweeping or total failure to respond.

ESP32 PWM Control Code

The ESP32 uses the LEDC (LED Control) peripheral for hardware PWM. While the standard Arduino Servo.h library works on some wrappers, the ESP32Servo library is the most reliable method for handling the hardware timer allocation in 2026. You can install it via the Arduino Library Manager.

#include <ESP32Servo.h>

Servo myServo;
const int servoPin = 13;

void setup() {
  // Allow allocation of all timers
  ESP32PWM::allocateTimer(0);
  ESP32PWM::allocateTimer(1);
  ESP32PWM::allocateTimer(2);
  ESP32PWM::allocateTimer(3);
  
  myServo.setPeriodHertz(50); // Standard 50hz servo
  myServo.attach(servoPin, 500, 2400); // Pulse width in microseconds (adjust for your specific servo)
}

void loop() {
  // Sweep from 0 to 180 degrees
  for (int pos = 0; pos <= 180; pos += 1) {
    myServo.write(pos);
    delay(15);
  }
  // Sweep back
  for (int pos = 180; pos >= 0; pos -= 1) {
    myServo.write(pos);
    delay(15);
  }
}

Note on Pulse Widths: While the theoretical standard is 1000µs to 2000µs for 0-180°, most modern digital servos (like the MG996R) actually respond to a wider range, typically 500µs to 2500µs. If your servo only sweeps 90 degrees, widen the microsecond limits in the attach() function. For deeper hardware-level timer configuration, refer to the official Espressif LEDC API documentation.

Diagnosing Failure Signatures

Servos fail in highly specific ways. Recognizing these signatures saves hours of debugging:

  • The "Hum" or Jitter: The servo vibrates audibly while holding position. Cause: Usually a dirty internal potentiometer, mechanical backlash in the gear train, or 50/60Hz mains noise coupling into the signal wire. Fix: Route signal wires away from AC mains, add a 100µF electrolytic capacitor across the servo's VCC and GND terminals, or implement a software deadband.
  • Overheat: The servo casing becomes too hot to touch within 30 seconds. Cause: The servo is mechanically stalled against an end-stop or an immovable load, causing it to draw maximum stall current (often >2.5A on an MG996R). Fix: Reduce the mechanical load, check for physical binding, or ensure your software limits prevent commanding positions beyond the physical travel limits.
  • Stall / Reset: The servo moves erratically, and the ESP32 reboots. Cause: Voltage sag. When the servo starts moving, it draws a massive inrush current. If powered from a weak supply (or the ESP32's USB 5V line), the voltage drops below 4.8V, resetting the servo's internal logic and triggering a brownout on the ESP32. Fix: Use a dedicated 5V 3A switching power supply.

Frequently Asked Questions: Control Servo with PWM

Why does my ESP32 reboot when I try to control a servo with PWM?

This is almost always a brownout caused by insufficient current. An ESP32 DevKit v1 powered via USB can typically only supply about 500mA total. A standard servo like the MG996R can draw 1.5A to 2.5A during startup or under load. This massive current draw collapses the 5V rail, triggering the ESP32's brownout detector (BOD) and forcing a reboot. Always power the servo from a dedicated external power supply, ensuring the GND is shared with the ESP32.

Can I control a servo with PWM directly from a Raspberry Pi GPIO?

Yes, but with a major caveat regarding signal jitter. The Raspberry Pi runs a non-real-time Linux OS. If you use software-based PWM (like standard Python GPIO libraries), background OS tasks will interrupt the pulse timing, causing the servo to twitch or jitter violently. To control a servo smoothly on a Pi, you must use hardware PWM pins (like GPIO 12 or 13) via the pigpio or gpiozero hardware PWM classes, or use an I2C PWM driver board like the PCA9685, which handles the precise timing independently of the Pi's CPU.

What is the exact PWM frequency and pulse width required to control a standard RC servo?

The standard protocol dictates a 50 Hz frequency (one pulse every 20 milliseconds). The position is determined by the high-time (pulse width) of that signal. A 1.0ms (1000µs) pulse typically commands 0°, a 1.5ms (1500µs) pulse commands the 90° center position, and a 2.0ms (2000µs) pulse commands 180°. As noted in the code section, many modern servos accept a wider 500µs to 2500µs range for extended travel.

How do I stop a servo from jittering when it is supposed to be holding a steady position?

Jitter at a standstill is usually caused by one of three things: power supply ripple, signal noise, or a worn internal potentiometer. First, solder a 100µF to 470µF electrolytic capacitor directly across the VCC and GND wires at the servo connector to smooth out voltage dips. Second, ensure your PWM signal wire is not routed parallel to high-current motor wires. If the jitter persists, the carbon track inside the servo's feedback potentiometer is likely worn or dirty, and the servo should be replaced. For high-precision applications, consider upgrading to a digital servo or a smart bus servo (like Dynamixel or Feetech SCS series) which use digital encoders instead of analog potentiometers. For more on RC servo internals and maintenance, see the Pololu RC Servo Guide.