micropython-servomotores-pwm

Servomotor Control with MicroPython and PWM

  • 5 min

A servomotor is an actuator that can rotate to a specific position and hold it.

If we want our project to move, open a door, rotate a camera, or move a robotic arm, we need servomotors.

Unlike a normal DC motor (which spins wildly as soon as you apply power), a servomotor is an intelligent device that maintains the position we tell it. We say “go to 90 degrees”, and it goes there and stays still, exerting force.

In this article, we will learn to control them using the PWM signals we already know, but with a very specific configuration.

The pulse theory (50Hz)

To control a standard analog servo (like the typical blue SG90 or the MG996R), not just any PWM will do. We need to follow a strict rule:

  1. The frequency must be 50 Hz. This means a pulse is sent every 20 milliseconds.
  2. The “width” of that pulse determines the angle:
  • A pulse of ~1 ms sets the servo to .
  • A pulse of ~1.5 ms sets it to the center 90°.
  • A pulse of ~2 ms sets it to 180°.

Watch your limits! These theoretical values (1ms to 2ms) are the “safe” standard. However, many modern servos accept wider ranges (0.5ms to 2.5ms) to achieve the full 180°. If you exceed your servo’s physical limit, you’ll hear an unpleasant buzzing and the motor will overheat. If it buzzes, disconnect it.

Electrical connection

A servo has 3 wires. The color code is usually:

  • Brown or Black: GND.
  • Red: VCC (Normally 5V).
  • Orange or Yellow: Signal (PWM).

Don’t power large servos from the board A small servo (SG90) moving with no load can be powered from the 5V (VBUS) pin of your ESP32/Pico. But if you use large servos or several at once, you need an external power source. Servos draw current spikes that can reset your microcontroller (Brownout).

Basic control with machine.PWM

In MicroPython, handling the duty cycle is done with 16-bit resolution (0 to 65535). This complicates things a bit because we have to translate milliseconds into this number from 0 to 65535.

Let’s do the math:

  • Total period (50Hz) = 20 ms.
  • Maximum counter value (65535) = 20 ms.
  • Therefore, 1 ms is equivalent to 1 / 20 * 65535 = 3276.
  • And 2 ms is equivalent to 2 / 20 * 65535 = 6553.

Let’s move a servo to the center (1.5ms):

from machine import Pin, PWM
import time

# Configure PWM on pin 15
servo = PWM(Pin(15))

# Standard frequency for servos
servo.freq(50)

# Move to 0 degrees (approx 1ms -> duty 3276)
servo.duty_u16(3276)
time.sleep(1)

# Move to 90 degrees (approx 1.5ms -> duty 4915)
servo.duty_u16(4915)
time.sleep(1)

# Move to 180 degrees (approx 2ms -> duty 6553)
servo.duty_u16(6553)
Copied!

Creating our own Servo class

Constantly calculating 3276 or 6553 is a headache and makes the code illegible. Also, every servo is different, and sometimes we need to calibrate these minimum and maximum values.

Let’s write a convenient class that allows us to simply do servo.write(90).

from machine import Pin, PWM

class Servo:
    def __init__(self, pin_id, min_us=500, max_us=2500, max_degree=180):
        """
        Servo class constructor.
        :param pin_id: Pin number or Pin object.
        :param min_us: Pulse duration for 0 degrees (in microseconds).
        :param max_us: Pulse duration for max_degree (in microseconds).
        :param max_degree: Maximum rotation range (usually 180).
        """
        self.pwm = PWM(Pin(pin_id))
        self.pwm.freq(50)
        self.min_us = min_us
        self.max_us = max_us
        self.max_degree = max_degree
        
        # Constant to convert microseconds to duty_u16
        # Period 20ms = 20000us
        # 65535 / 20000 = 3.27675
        self._us_to_duty = 65535 / 20000

    def write(self, degree):
        """Moves the servo to the specified angle."""
        # Limiting the angle between 0 and the maximum
        degree = max(0, min(degree, self.max_degree))
        
        # Mapping the angle to microseconds
        # Linear formula: y = mx + n
        slope = (self.max_us - self.min_us) / self.max_degree
        pulse_us = self.min_us + (slope * degree)
        
        # Convert to duty_u16 and apply
        duty = int(pulse_us * self._us_to_duty)
        self.pwm.duty_u16(duty)

    def close(self):
        """Stops the PWM to release the pin."""
        self.pwm.deinit()
Copied!

We changed the default values to 500us and 2500us. This usually covers the full range of cheap SG90 servos better. If you see your servo hitting its mechanical stop, adjust these values in the constructor.

Usage example with the class

See how clean the main code looks now:

import time
# Assuming we saved the previous class in servo.py
# from servo import Servo 

# Instantiate the servo on GPIO 15
mi_servo = Servo(15)

while True:
    # Sweep from 0 to 180 degrees
    for i in range(0, 181, 10):
        mi_servo.write(i)
        time.sleep(0.05)
    
    # Sweep back
    for i in range(180, -1, -10):
        mi_servo.write(i)
        time.sleep(0.05)
Copied!

Deactivating the servo

Analog servos sometimes make a small noise (“humming”) even when stationary, trying to maintain position. If in your project there is no external force moving the arm, you can “turn off” the servo by stopping the PWM signal.

In our class, we could add:

    def release(self):
        self.pwm.duty_u16(0)
Copied!

By setting the duty to 0, the motor relaxes and stops consuming power, although it also loses its holding force (you can move it by hand).

With this, we have complete control. Whether it’s to open the lid of a safe or to make a robot wave, the principle is always the same.