Controlando un motor NEMA17 con ESP32 y MicroPython

Controlando un motor NEMA17 con ESP32 y MicroPython


JMT
JMT
Controlando un motor NEMA17 con ESP32 y...
Posted by JMT

Conectar un motor paso a paso (Stepper) a un microcontrolador usando MicroPython abre la puerta a infinidad de proyectos de robótica, CNC, impresoras 3D o automatización. En este artículo te cuento los pasos que he seguido para hacer funcionar un NEMA17 con un ESP32 y un driver A4988, acompañado del diseño de mi propia placa de control.

¿Qué son los Steppers?

Los motores paso a paso son un tipo especial de motor eléctrico que gira en pasos discretos en lugar de hacerlo de forma continua como un motor DC. Esto significa que podemos controlar con mucha precisión el ángulo de giro simplemente enviando impulsos de reloj (pulsos eléctricos). Cada pulso equivale a un paso.

Ventajas:

  • Control de posición muy preciso.
  • No necesitan feedback en aplicaciones simples.
  • Son fáciles de controlar con drivers dedicados.

¿Qué es un NEMA17?

NEMA 17

NEMA no es una marca, sino un estándar de dimensiones. El número indica el tamaño: en el caso del NEMA17, el frontal mide 1,7 pulgadas (~42 mm). Son los motores paso a paso más comunes en impresoras 3D y pequeños CNC. Suelen tener un par motor suficiente para mover ejes ligeros con correas o husillos.

Documentación NEMA 17

¿Cómo funciona un motor paso a paso?

Un stepper tiene dos bobinas independientes (A y B). Activando estas bobinas en secuencia, conseguimos que el rotor avance paso a paso.

  • Paso completo: se energizan las bobinas una tras otra.
  • Medio paso / microstepping: se usan combinaciones proporcionales de corriente, logrando movimientos más suaves y precisos.

Para no complicarnos con el control de corriente y la secuencia de fases, se usan drivers especializados como el A4988.

El Driver A4988

Driver A4988

El A4988 es uno de los drivers más populares para steppers bipolares. Su función es muy sencilla:

  • Recibe pulsos STEP → mueve el motor un paso.
  • El pin DIR marca la dirección de giro.
  • Regula la corriente máxima para proteger el motor de sobrecorriente, sobretemperatura y subtensión.
  • Soporta hasta 35 V y ±2 A de salida (1 A continuo sin disipador)

Puntos clave de conexión:

  • Condensador de desacoplo (100µF) en la entrada de 12 V (muy importante para evitar picos).
  • Conexión de bobinas: 1A–1B y 2A–2B. (Si no sabes cuáles son, basta medir continuidad con un multímetro).
  • Puente SLP–RST para mantener activo el driver.
  • Pines MS1, MS2, MS3 → seleccionan el modo de microstepping (1, 1/2, 1/4, 1/8, 1/16).

Pinout del A4988:

Pinout de A4988

Microstepping:

MS1MS2MS3Resolución Microstep
BajoBajoBajo1
AltoBajoBajo1/2
BajoAltoBajo1/4
AltoAltoBajo1/8
AltoAltoAlto1/16

Es importante tener en cuenta que estos pines tienen resistencias pull-down internas que los mantienen en BAJO por defecto. Esto significa que, si se dejan desconectados, el motor funcionará automáticamente en modo de paso completo.

Pines de control:

El pin STEP es la entrada de control principal para mover el motor. Cada vez que se envía una señal ALTA a este pin, el motor avanza un paso. La frecuencia de estos pulsos determina la velocidad de rotación del motor: los pulsos más rápidos resultan en una rotación más rápida.

El pin DIR controla la dirección de giro del motor. Al configurar este pin en ALTA, el motor gira en sentido horario, y al configurarlo en BAJA, gira en sentido antihorario.

Pines de salida al motor:

Normalmente, un motor paso a paso bipolar tiene dos pares de cables. Los pines 1A y 1B se conectan a la primera bobina del motor, mientras que los pines 2A y 2B se conectan a la segunda.

Uno de los puntos más importantes es la posición de los pines del cable que va desde la placa al motor, para ello es necesario “detectar” las 2 bobinas. Aquí tienes 3 métodos sencillos para identificarlas:

  1. La primera opción es consultar la documentación de tu motor, en muchas ocasiones incluyen el diagrama las bobinas y el pinout del mismo:
  2. El segundo método consiste en girar el eje del motor manualmente: debería girar libremente. Ahora, toma dos cables del motor y conéctalos brevemente. Manteniendo los cables conectados, intenta girar el eje de nuevo. Si el eje se vuelve repentinamente mucho más difícil de girar, has encontrado una fase. Si no cambia nada, prueba con otra combinación de cables.
  3. Otro método es usar un multímetro en modo de continuidad. Toca dos cables a la vez con las puntas del multímetro. Si el multímetro emite un pitido o muestra continuidad, esos cables pertenecen a la misma fase. Si no detecta ninguna señal, prueba con otro par.

Una vez que identifiques ambos pares, conecta un par a 1A y 1B y el otro a 2A y 2B; no te preocupes por el orden.

El Microcontrolador: ESP32

He elegido un ESP32 por varias razones:

  • Tiene WiFi y Bluetooth integrados, lo que nos habilita a realizar control remoto.
  • Tiene más potencia que un Arduino UNO.
  • Soporta MicroPython.
  • Incorpora un periférico llamado RMT, ideal para generar trenes de pulsos de forma precisa.

Diagrama de conexión

Este es mi diseño de la Microstepper Board que integra el ESP32, el A4988 y dos conectores para el motor NEMA17 (pines para pruebas y conector estándar XH2.54):

El resultado sería el que sigue:

Microstepper Board v1.0 soldada

Una vez montada la placa queda bastante compacta:

Microstepper Board v1.0 con todas las placas

MicroPython con el RMT del ESP32

Con MicroPython podemos controlar el motor fácilmente, pero para obtener un movimiento fluido necesitamos generar pulsos regulares. Ahí entra el RMT del ESP32. El módulo RMT (Remote Control) del ESP32 está pensado para protocolos como IR o WS2812, pero es perfecto para mandar trenes de pulsos estables al pin STEP.

Demostración del ejemplo básico en MicroPython usando Microstepper Board v1.0

Código fuente del video: ( más ejemplos en https://github.com/juanmitaboada/microstepper )

from machine import Pin
import esp32
import time

# -------------------------
# Configuración hardware
# -------------------------
en_pin  = Pin(5,  Pin.OUT, value=1)   # 1 = deshabilitado al inicio
dir_pin = Pin(16, Pin.OUT, value=0)
step_pin = Pin(17, Pin.OUT, value=0)
ms1 = Pin(27, Pin.OUT, value=0)
ms2 = Pin(26, Pin.OUT, value=0)
ms3 = Pin(25, Pin.OUT, value=0)

# RMT: clock_div=80 -> 1 tick ~ 1 µs
rmt = esp32.RMT(0, pin=step_pin, clock_div=80)

# -------------------------
# Parámetros motor
# -------------------------
MICROSTEP = 1          # 1 para full-step; si pones 2/4/8/16 adapta MS1..3 y este factor
STEPS_PER_REV = 200 * MICROSTEP

# -------------------------
# Estado
# -------------------------
_net_pos = 0  # pasos acumulados (positivos = adelante, negativos = atrás)

def enable():
    en_pin.value(0)  # ENABLE activo en bajo

def disable():
    step_pin.value(0)
    en_pin.value(1)

def step_motor_rmt(steps, delay_us=1000, direction=1):
    """
    Genera 'steps' pasos con RMT.
    Actualiza _net_pos para poder volver a 'home'.
    """
    global _net_pos
    if steps <= 0:
        return
    step_pin.value(0)
    dir_pin.value(direction)
    time.sleep_us(20)  # setup/hold de DIR antes del primer flanco
    pulses = [delay_us, delay_us] * steps
    rmt.write_pulses(pulses, 0)  # start en bajo -> primer evento es flanco de subida
    step_pin.value(0)
    _net_pos += steps if direction == 1 else -steps

def clock_sweep(ticks, interval_s, direction=1, delay_us=800):
    """
    Reparte exactamente STEPS_PER_REV pasos en 'ticks' saltos,
    distribuyendo el resto de forma uniforme (sin perder pasos).
    """
    base = STEPS_PER_REV // ticks          # pasos mínimos por tick
    rem  = STEPS_PER_REV %  ticks          # resto a repartir
    err = 0
    for _ in range(ticks):
        steps = base
        err += rem
        if err >= ticks:
            steps += 1
            err -= ticks
        step_motor_rmt(steps, delay_us=delay_us, direction=direction)
        time.sleep(interval_s)

def go_home(delay_us=800):
    """Vuelve a la posición original compensando la desviación acumulada."""
    global _net_pos
    if _net_pos == 0:
        return
    if _net_pos > 0:
        # nos hemos ido adelante -> volver atrás
        step_motor_rmt(_net_pos, delay_us=delay_us, direction=0)
    else:
        # nos hemos ido atrás -> volver adelante
        step_motor_rmt(-_net_pos, delay_us=delay_us, direction=1)

# -------------------------
# Demo básica
# -------------------------
try:
    enable()
    # Full-step por defecto
    ms1.value(0); ms2.value(0); ms3.value(0)

    # 1 vuelta completa hacia adelante
    step_motor_rmt(STEPS_PER_REV, delay_us=1000, direction=1)
    time.sleep(1)

    # 1 vuelta completa hacia atrás
    step_motor_rmt(STEPS_PER_REV, delay_us=1000, direction=0)
    time.sleep(1)

    # 1/2 vuelta adelante
    step_motor_rmt(STEPS_PER_REV // 2, delay_us=1000, direction=1)
    time.sleep(1)

    # 1/2 vuelta atrás
    step_motor_rmt(STEPS_PER_REV // 2, delay_us=1000, direction=0)
    time.sleep(1)

    # 1/4 vuelta adelante
    step_motor_rmt(STEPS_PER_REV // 4, delay_us=1000, direction=1)
    time.sleep(1)

    # 1/4 vuelta atrás
    step_motor_rmt(STEPS_PER_REV // 4, delay_us=1000, direction=0)
    time.sleep(1)

    # 1 vuelta (360º) en 12 pasos adelante (1 salto/segundo) -> SIN perder pasos
    clock_sweep(ticks=12, interval_s=1.0, direction=1, delay_us=800)
    time.sleep(1)

    # 1 vuelta (360º) en 60 pasos atrás (1 salto/0.1 s) -> SIN perder pasos
    clock_sweep(ticks=60, interval_s=0.1, direction=0, delay_us=800)
    time.sleep(1)

    # Siempre volver a la posición original
    go_home(delay_us=800)

finally:
    disable()


def ramp_motion(total_steps, accel_steps, cruise_steps, delay_us_start=2000, delay_us_min=500, direction=1):
    """
    Movimiento con rampa de aceleración + velocidad constante + desaceleración.
    - total_steps: número total de pasos a cubrir
    - accel_steps: pasos dedicados a acelerar
    - cruise_steps: pasos dedicados a velocidad constante
    - delay_us_start: retardo inicial (velocidad baja)
    - delay_us_min: retardo mínimo (velocidad máxima)
    """
    global _net_pos

    # Calcular pasos de aceleración y deceleración
    accel_range = delay_us_start - delay_us_min
    step_decrement = accel_range / accel_steps

    # Aceleración
    current_delay = delay_us_start
    for _ in range(accel_steps):
        step_motor_rmt(1, delay_us=int(current_delay), direction=direction)
        current_delay -= step_decrement

    # Crucero
    for _ in range(cruise_steps):
        step_motor_rmt(1, delay_us=delay_us_min, direction=direction)

    # Deceleración
    current_delay = delay_us_min
    for _ in range(accel_steps):
        current_delay += step_decrement
        step_motor_rmt(1, delay_us=int(current_delay), direction=direction)


# ----------------------
# Demo con rampas
# ----------------------
try:
    enable()

    print("Rampa trapezoidal: 2 vueltas acelerando, 5 vueltas constantes, 2 vueltas decelerando")

    # 2 vueltas acelerando (200*2 = 400 pasos)
    # 5 vueltas constantes (1000 pasos)
    # 2 vueltas decelerando (400 pasos)
    ramp_motion(
        total_steps=1800,       # total (para referencia)
        accel_steps=STEPS_PER_REV * 2,
        cruise_steps=STEPS_PER_REV * 5,
        delay_us_start=2000,    # arranque lento
        delay_us_min=500,       # velocidad máxima
        direction=1
    )

finally:
    disable()
    print("Secuencia terminada.")

Limitación de corriente del A4988:

Una forma de maximizar el rendimiento de un motor paso a paso es suministrarle un voltaje superior al nominal. En particular, usar un voltaje más alto generalmente permite velocidades de paso más altas y un mayor par de paso.

Ejemplo: supongamos que utilizamos un motor paso a paso con una corriente nominal máxima de 1 A y una resistencia de bobina de 5 Ω. Esto indica una alimentación máxima del motor de 5 V (1 A × 5 Ω = 5 V). Usar un motor de este tipo con 12 V permitiría velocidades de paso más altas, pero el problema es que, si se conectan 12 V directamente, el motor intentará consumir demasiada corriente, muy por encima del límite seguro de 1 A. Cuando esto sucede, las bobinas internas del motor pueden sobrecalentarse, lo que puede dañarlo permanentemente.

Para usar de forma segura un voltaje superior al voltaje nominal del motor, es necesario limitar la corriente que fluye a través de las bobinas del motor. Precisamente por eso, el controlador de motor paso a paso A4988 incluye un pequeño potenciómetro limitador de corriente. Este permite ajustar la corriente máxima que fluye a través de las bobinas del motor.

Potenciómetro limitador de corriente A4988

Al ajustar el potenciómetro correctamente, se asegura de que el motor nunca consuma más corriente de la que puede manejar con seguridad, lo que ayuda a prevenir el sobrecalentamiento y lo mantiene seguro.

Para ajustar el límite de corriente en el A4988, activaremos el controlador en modo de paso completo y se usará un multímetro para medir directamente la corriente que fluye a través de una de las bobinas mientras se ajusta el potenciómetro limitador de corriente.

Primero, consulte la documentación de su motor paso a paso para conocer su corriente nominal. Por ejemplo, supongamos que utiliza un motor paso a paso NEMA 17 con 200 pasos por revolución y una potencia nominal de 1,5 A por bobina.

En el programa MicroPython ponga los pines STEP y DIR en ALTA, esto mantiene el motor energizado y en una posición fija. Establece MS1, MS2 y MS3 en BAJA para configurar el controlador en modo paso completo.

A continuación, coloca el amperímetro (o el multímetro en modo corriente) en serie con una de las bobinas del motor para que la corriente fluya desde el amperímetro hasta la bobina. Para ello, desconecta un cable del terminal 1A o 1B del controlador. Conecta una sonda del amperímetro a ese terminal y la otra al cable suelto del motor.

Con todo conectado y encendido, deberías ver una lectura de corriente en el multímetro. Ahora, gira con cuidado el potenciómetro de límite de corriente con un destornillador pequeño mientras observas la lectura del multímetro. Ajústalo hasta que la corriente coincida con el valor nominal del motor, en este caso 1,5 A.

Tenga en cuenta que la corriente que está midiendo es solo el 70% del límite de corriente real configurado, ya que ambas bobinas están siempre encendidas y limitadas a este valor en el modo de paso completo, por lo que si luego habilita los modos de micropasos, la corriente a través de las bobinas podrá superar esta corriente de paso completo medida en un 40% (1/0,7) en ciertos pasos; tenga esto en cuenta cuando utilice este método para establecer el límite de corriente.

Recuerda que el objetivo principal de establecer un límite de corriente es proteger tu motor paso a paso. Hacer funcionar un motor con demasiada corriente puede generar más par a corto plazo, pero también provoca que se caliente más, lo que puede acortar su vida útil. Por otro lado, si configuras la corriente demasiado baja, el motor no se dañará, pero podría no tener suficiente potencia para realizar su función correctamente. La clave está en encontrar el punto óptimo donde tu motor tenga suficiente potencia para realizar sus tareas sin calentarse y con una larga vida útil.

Recomendaciones

  • Motor NEMA17:
    • Cada motor paso a paso tiene un par-velocidad característico.
    • A bajas velocidades tiene mucho par, a medida que subes la frecuencia de pasos, el par disponible cae.
    • Un NEMA17 típico puede moverse cómodamente en el rango 1000–1500 pasos/s en full step con A4988 sin perder pasos (depende del modelo exacto, tensión y carga mecánica).
    • Con microstepping puedes subir más en frecuencia, pero el par por micro-paso es menor.
    • Motores típicos: 1.8°/paso = 200 pasos/vuelta en full step.
  • Driver A4988:
    • A mayor voltaje de alimentación (12 V vs 24 V), más rápida es la subida de corriente en las bobinas → permite más velocidad.
    • El ajuste de corriente (Vref) también influye:
      • demasiado bajo = pierde pasos antes
      • demasiado alto = calienta el driver.
    • Disipador en el chip y ventilación si vas a trabajar con corrientes altas.
    • El A4988 suele manejar hasta unos pocos kHz de frecuencia de pasos (decenas de miles de pasos/s), pero el límite práctico lo pone el motor.
    • Une SLEEP y RESET para mantener el driver activo.
    • ENABLE (activo en bajo): úsalo para desactivar el motor cuando no lo uses y así ahorrar energía.
    • Ajusta microstepping (MS1–3) según lo que necesites:
      • Full step → más par, menos resolución.
      • 1/16 step → más suave, menos par por micro–paso.
  • La inercia y la carga mecánica:
    • Si tu eje mueve poca carga, el motor podrá girar más rápido antes de perder pasos.
    • En un eje pesado (ejemplo: CNC, impresora 3D con cama grande) la velocidad máxima baja bastante.
    • Cuando intentes pruebas de velocidad máxima, el punto donde empieza a resonar o saltarse pasos está cerca de su límite.
    • El par cae con la velocidad → no intentes ir demasiado rápido.
    • Siempre usa rampas de aceleración/desaceleración → evita perder pasos en arranques/paradas bruscas.
  • Programación en MicroPython + ESP32:
    • Usa el RMT del ESP32 → trenes de pulsos estables sin cargar la CPU.
    • Controla los pasos acumulados → así puedes implementar un go_home() virtual.
    • Si necesitas máxima suavidad, considera interpolar la rampa (ejemplo: perfil trapezoidal o seno).
    • Haz pruebas incrementando la velocidad poco a poco para encontrar el límite seguro de tu motor.
  • Pruebas y depuración:
    • Empieza siempre con velocidades bajas.
    • Revisa con el multímetro la continuidad de las bobinas: 2 pares independientes.
    • Si el motor tiembla en su sitio → bobinas mal conectadas.
    • Si el motor gira solo en un sentido → revisar señal DIR y que esté estable antes del STEP.
    • Si el motor se calienta mucho → baja el límite de corriente.
    • Para proyectos críticos: añade un endstop / sensor de referencia para saber posición real.

Conclusiones

El A4988 simplifica enormemente el control de un motor NEMA17.

El ESP32 con MicroPython y el RMT permiten generar pulsos muy precisos sin preocuparnos por temporizadores manuales.

Ahora ya puedes desarrollar sistemas más complejos como impresoras 3D, CNC, actuadores lineales o robots controlados por WiFi.

Microstepper Board v1.0

Comments

Related Articles

Arduino

Alioli ROV Submarine Drone Software Framework for Arduino

In this post, I describe how my own Arduino Framework for Alioli ROV Submarine Drone works. In my last post about Alioli ROV Submarine Drone, I wrote, “Learn how to Build an...

Posted on by Juanmi Taboada
Arduino

Finishing the frame for an Underwater ROV

In my last post, “How I designed the frame for my Underwater ROV“, I gave all details about the design I used for the frame for Alioli Underwater ROV. In this post, I...

Posted on by Juanmi Taboada