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.
- 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.
- 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)
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
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:
- STEP: For each pulse we send here, the motor advances one step.
- DIR: If it is
HIGH, it turns one way; if it isLOW, 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)
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.