micropython-motores-paso-a-paso-stepper

Stepper Motor Control with MicroPython

  • 6 min

A stepper motor is a motor that advances in small, controllable increments called steps.

When we need a robot to move exactly 10 millimeters, or a 3D printer to deposit plastic at the exact coordinate, DC motors fall short (they are imprecise) and servos sometimes don’t have enough range (they only rotate 180°).

This is where Stepper Motors shine.

Unlike a normal motor that rotates continuously when powered, a stepper divides a full rotation into a number of discrete steps. It’s like the second hand of an analog clock: it advances “tick-tock”, step by step.

This allows us absolute control of position and speed without needing external sensors (encoders). We simply count the steps we send.

Motor types: unipolar and bipolar

Before connecting anything, we must identify what “animal” we have in our hands, because they are controlled very differently.

  1. Unipolar (5 or 6 wires): The most famous is the small 28BYJ-48. They are cheap, have little power, and are easy to control with a ULN2003 driver.
  2. Bipolar (4 wires): The industrial standard, like the NEMA 17 found in 3D printers. They have a lot of power (torque) but require more complex drivers like the A4988 or DRV8825.

Let’s see how to handle both in MicroPython.

Unipolar motor 28BYJ-48

This motor is ubiquitous in starter kits. It has an internal gearbox and works by energizing its 4 internal coils in a specific sequence.

To move it, simply setting a pin to HIGH is not enough. We need to perform a “dance” of turning on and off the 4 control pins (IN1, IN2, IN3, IN4).

The step sequence

There are several ways to energize them, but the smoothest and most precise is Half-Step, which has an 8-state sequence:

StepperULN2003 Class

Let’s encapsulate this logic in a class. We will connect the ULN2003 driver to any 4 GPIO pins.

import time
from machine import Pin

class StepperULN2003:
    def __init__(self, pins_list, delay=0.001):
        """
        :param pins_list: List of 4 Pin objects [IN1, IN2, IN3, IN4]
        :param delay: Time between steps (speed). Recommended minimum 0.001 (1ms)
        """
        self.pins = pins_list
        self.delay = delay
        
        # Half-step sequence (8 steps) for greater smoothness
        # Represents the state of [IN1, IN2, IN3, IN4]
        self.sequence = [
            [1, 0, 0, 0],
            [1, 1, 0, 0],
            [0, 1, 0, 0],
            [0, 1, 1, 0],
            [0, 0, 1, 0],
            [0, 0, 1, 1],
            [0, 0, 0, 1],
            [1, 0, 0, 1]
        ]

    def move(self, steps, direction=1):
        """
        Moves the motor n steps.
        :param steps: Number of steps to move
        :param direction: 1 (clockwise) or -1 (counter-clockwise)
        """
        for _ in range(steps):
            if direction > 0:
                # Advance through the sequence list
                step_pattern = self.sequence.pop(0)
                self.sequence.append(step_pattern)
            else:
                # Move backward through the list
                step_pattern = self.sequence.pop()
                self.sequence.insert(0, step_pattern)
            
            # Apply the pattern to the pins
            for i in range(4):
                self.pins[i].value(step_pattern[i])
            
            time.sleep(self.delay)

    def stop(self):
        """Turns off all coils to save energy and prevent overheating"""
        for pin in self.pins:
            pin.value(0)
Copied!

Usage Example

The 28BYJ-48 motor has a reduction ratio of approximately 64:1. In half-step mode, it needs 4096 steps for one full rotation (360°).

# Pins connected to IN1, IN2, IN3, IN4 of the ULN2003 driver
pins = [Pin(19, Pin.OUT), Pin(18, Pin.OUT), Pin(5, Pin.OUT), Pin(17, Pin.OUT)]

motor = StepperULN2003(pins)

print("Doing a full rotation...")
motor.move(4096, direction=1) # Clockwise

print("Going back half a turn...")
motor.move(2048, direction=-1) # Counter-clockwise

motor.stop() # Important to turn off when finished
Copied!

Bipolar motor NEMA 17 with A4988 driver

If you want to build a CNC or a serious robot, you will use bipolar motors. Here, we do NOT control the coils directly. A specialized driver like the A4988 takes care of that.

Our life is easier because the interface with the driver is reduced to two pins:

  1. STEP: For each pulse we send here, the motor advances one step.
  2. DIR: If it is HIGH, it turns one way; if it is LOW, it turns the other.

Danger of death (of the driver)! Never, under any circumstances, disconnect the motor wires while the driver is powered. The energy stored in the coils will have nowhere to go and will instantly fry the A4988 chip.

Control via STEP/DIR

Controlling this is as simple as turning an LED on and off (the STEP pin).

from machine import Pin
import time

class StepperDriver:
    def __init__(self, step_pin, dir_pin):
        self.step_pin = Pin(step_pin, Pin.OUT)
        self.dir_pin = Pin(dir_pin, Pin.OUT)
        self.step_pin.value(0)

    def move(self, steps, direction, delay=0.001):
        """
        :param steps: Steps to advance (a NEMA 17 usually has 200 steps/rev)
        :param direction: 1 or 0 for rotation direction
        :param delay: Speed (time between steps)
        """
        # Set the direction
        self.dir_pin.value(1 if direction > 0 else 0)
        
        for _ in range(steps):
            # Generate a pulse on STEP
            self.step_pin.value(1)
            time.sleep_us(10) # Very brief pulse
            self.step_pin.value(0)
            
            # Wait before the next step (speed control)
            time.sleep(delay)

# Usage Example
# STEP on GPIO 26, DIR on GPIO 27
nema = StepperDriver(26, 27)

# NEMA 17 motors are usually 1.8 degrees per step (200 steps = 1 revolution)
print("Fast rotation...")
nema.move(200, 1, delay=0.002) 

print("Slow rotation in opposite direction...")
nema.move(200, 0, delay=0.01)
Copied!

Microstepping for smoother movement

You may notice the motor vibrates or makes noise. Modern drivers allow a technique called Microstepping.

Using jumpers on the driver board (pins MS1, MS2, MS3), we can tell the A4988 to divide each physical step into microsteps (1/2, 1/4, 1/8, 1/16).

  • Advantage: Ultra-smooth and silent movement. Higher resolution.
  • Disadvantage: The motor loses a bit of torque, and we have to send pulses much faster (higher frequency).

Which motor to choose

  • Use the 28BYJ-48 (Unipolar) to learn, to move light things (like a small curtain or a toy), and because it is very cheap.
  • Use a NEMA 17 (Bipolar) if you need precision, speed, and real power (robots, 3D printers).

Now that we control exact position, in the next section we will see how to read sensors so our system knows what is happening around it.