micropython-servomotores-pwm

Control de servomotores con MicroPython y PWM

  • 5 min

Un servomotor es un actuador que puede girar hasta una posición concreta y mantenerla.

Si queremos que nuestro proyecto se mueva, abra una puerta, gire una cámara o mueva un brazo robótico, necesitamos servomotores.

A diferencia de un motor DC normal (que gira como loco en cuanto le das energía), un servomotor es un dispositivo inteligente que mantiene la posición que le indiquemos. Le decimos “ponte a 90 grados”, y él va allí y se queda quieto haciendo fuerza.

En este artículo vamos a aprender a controlarlos usando las señales PWM que ya conocemos, pero con una configuración muy específica.

La teoría del pulso (50Hz)

Para controlar un servo analógico estándar (como los típicos SG90 azules o los MG996R), no vale cualquier PWM. Necesitamos seguir una regla estricta:

  1. La frecuencia debe ser de 50 Hz. Esto significa que se envía un pulso cada 20 milisegundos.
  2. La “anchura” de ese pulso determina el ángulo:
  • Un pulso de ~1 ms pone el servo en .
  • Un pulso de ~1.5 ms lo pone en el centro 90°.
  • Un pulso de ~2 ms lo pone en 180°.

¡Cuidado con los límites! Estos valores teóricos (1ms a 2ms) son el estándar “seguro”. Sin embargo, muchos servos modernos aceptan rangos más amplios (0.5ms a 2.5ms) para alcanzar los 180° completos. Si te pasas del límite físico de tu servo, escucharás un zumbido desagradable y el motor se calentará. Si zumba, desconecta.

Conexión eléctrica

Un servo tiene 3 cables. El código de colores suele ser:

  • Marrón o Negro: GND (Tierra).
  • Rojo: VCC (Normalmente 5V).
  • Naranja o Amarillo: Señal (PWM).

No alimentes servos grandes desde la placa Un servo pequeño (SG90) moviéndose en vacío puede alimentarse desde el pin 5V (VBUS) de tu ESP32/Pico. Pero si usas servos grandes o varios a la vez, necesitas una fuente externa. Los servos consumen picos de corriente que pueden resetear tu microcontrolador (Brownout).

Control básico con machine.PWM

En MicroPython, el manejo del ciclo de trabajo (duty) se hace con una resolución de 16 bits (0 a 65535). Esto complica un poco las cosas, porque tenemos que traducir milisegundos a este número de 0 a 65535.

Hagamos las matemáticas:

  • Periodo total (50Hz) = 20 ms.
  • Valor máximo del contador (65535) = 20 ms.
  • Entonces, 1 ms equivale a 1 / 20 * 65535 = 3276.
  • Y 2 ms equivale a 2 / 20 * 65535 = 6553.

Vamos a mover un servo al centro (1.5ms):

from machine import Pin, PWM
import time

# Configuramos el PWM en el pin 15
servo = PWM(Pin(15))

# Frecuencia estándar para servos
servo.freq(50)

# Mover a 0 grados (aprox 1ms -> duty 3276)
servo.duty_u16(3276)
time.sleep(1)

# Mover a 90 grados (aprox 1.5ms -> duty 4915)
servo.duty_u16(4915)
time.sleep(1)

# Mover a 180 grados (aprox 2ms -> duty 6553)
servo.duty_u16(6553)
Copied!

Creando nuestra propia clase Servo

Estar calculando 3276 o 6553 todo el rato es un dolor de cabeza y hace el código ilegible. Además, cada servo es un mundo y a veces necesitamos calibrar esos valores mínimos y máximos.

Vamos a escribir una clase cómoda que nos permita hacer simplemente servo.write(90).

from machine import Pin, PWM

class Servo:
    def __init__(self, pin_id, min_us=500, max_us=2500, max_degree=180):
        """
        Constructor de la clase Servo.
        :param pin_id: Número del pin o objeto Pin.
        :param min_us: Duración del pulso para 0 grados (en microsegundos).
        :param max_us: Duración del pulso para max_degree (en microsegundos).
        :param max_degree: Rango máximo de giro (normalmente 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
        
        # Constante para convertir microsegundos a duty_u16
        # Periodo 20ms = 20000us
        # 65535 / 20000 = 3.27675
        self._us_to_duty = 65535 / 20000

    def write(self, degree):
        """Mueve el servo al ángulo especificado."""
        # Limitamos el ángulo entre 0 y el máximo
        degree = max(0, min(degree, self.max_degree))
        
        # Mapeamos el ángulo a microsegundos
        # Formula de la recta: y = mx + n
        slope = (self.max_us - self.min_us) / self.max_degree
        pulse_us = self.min_us + (slope * degree)
        
        # Convertimos a duty_u16 y aplicamos
        duty = int(pulse_us * self._us_to_duty)
        self.pwm.duty_u16(duty)

    def close(self):
        """Detiene el PWM para liberar el pin."""
        self.pwm.deinit()
Copied!

Hemos cambiado los valores por defecto a 500us y 2500us. Esto suele cubrir mejor el rango completo de los servos SG90 baratos. Si ves que tu servo golpea el tope, ajusta estos valores en el constructor.

Ejemplo de uso con la clase

Mira qué limpio queda ahora el código principal:

import time
# Suponiendo que hemos guardado la clase anterior en servo.py
# from servo import Servo 

# Instanciamos el servo en el GPIO 15
mi_servo = Servo(15)

while True:
    # Barrido de 0 a 180 grados
    for i in range(0, 181, 10):
        mi_servo.write(i)
        time.sleep(0.05)
    
    # Barrido de vuelta
    for i in range(180, -1, -10):
        mi_servo.write(i)
        time.sleep(0.05)
Copied!

Desactivar el servo

Los servos analógicos a veces hacen un pequeño ruido (“humming”) incluso cuando están quietos, intentando mantener la posición. Si en tu proyecto no hay fuerza externa que mueva el brazo, puedes “apagar” el servo dejando de enviar PWM.

En nuestra clase podríamos añadir:

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

Al poner el duty a 0, el motor se relaja y deja de consumir energía, aunque también pierde su fuerza de retención (puedes moverlo con la mano).

Con esto ya tenemos el control total. Ya sea para abrir la tapa de una caja fuerte o para hacer saludar a un robot, el principio es siempre el mismo.