Getting a Raspberry Pi Pico to reliably drive a stepper motor requires more than just toggling a GPIO pin. The 3.3V logic level of the RP2040 microcontroller rules out several legacy industrial drivers, and missing a single decoupling capacitor can permanently brick your silicon. This guide provides a decision-forward framework to select the correct pico driver for your application, followed by a complete, debuggable MicroPython implementation using the industry-standard DRV8825.
The Pico Driver Decision Matrix
Do not buy a driver until you have passed your project requirements through this decision tree. The Raspberry Pi Pico outputs 3.3V logic. Many older drivers (like the TB6600 or A4988 in some configurations) expect 5V logic, which leads to missed steps or complete failure to trigger.
| Project Requirement | Recommended Driver | Pico 3.3V Compatibility | Est. Cost (2026) |
|---|---|---|---|
| Silent operation + dynamic current tuning via UART | TMC2209 | Native (Logic high > 2.2V) | $8.00 |
| High torque (>3A per phase) + industrial loads | TB6600 | Poor (Requires 5V level shifter) | $12.00 |
| Standard 2.5A limit, low cost, basic positioning | DRV8825 | Native (Logic high > 2.2V) | $4.00 |
Parts List and Pin Mapping
Using exact variants prevents the 'it works on my bench but not in the enclosure' problem. The Pico W is specified here for its identical pinout to the base Pico, allowing future WiFi telemetry upgrades without rewiring.
| Component | Exact Variant / Spec | Notes |
|---|---|---|
| Microcontroller | Raspberry Pi Pico W (RP2040, 2MB Flash) | Ensure headers are soldered if using a breadboard. |
| Stepper Driver | DRV8825 (Pololu #2133 or equivalent) | Generic clones often use 0.1Ω sense resistors; Pololu uses 0.05Ω. Check yours. |
| Stepper Motor | NEMA 17 (17HS4401, 1.5A/phase, 42Ncm) | Bipolar, 4-wire. Do not use 5-wire unipolar motors. |
| Power Supply | 12V 3A DC (Mean Well GST60A12 or similar) | Must provide at least 2x the motor's rated current. |
| Decoupling Cap | 100µF 25V Electrolytic | Mandatory. Prevents VMOT voltage spikes from killing the driver. |
Pin Mapping Table
Wire the logic side first. Keep the high-power motor wires separated from the logic wires to prevent inductive noise from resetting the Pico.
| Pico Pin | Function | DRV8825 Pin | Recommended Wire Color |
|---|---|---|---|
| GP2 (Pin 4) | Step Pulse | STEP | Orange |
| GP3 (Pin 5) | Direction | DIR | Yellow |
| GND (Pin 8) | Logic Ground | GND | Black |
| 3V3(OUT) (Pin 36) | Logic VCC | VDD | Red |
Wiring the DRV8825 Pico Driver
- Set the Current Limit (Vref): Before wiring, power the DRV8825 logic only (connect VDD and GND to Pico 3.3V and GND). Use a multimeter to measure the voltage between the GND pin and the potentiometer wiper. For a 1.5A motor with a generic 0.1Ω sense resistor, target Vref = 0.75V. Adjust the pot with a ceramic screwdriver.
- Install the Decoupling Capacitor: Solder or plug the 100µF capacitor across the DRV8825's VMOT and GND pins on the power side. Observe polarity (stripe to GND).
- Wire the Logic: Connect the Pico GP2, GP3, GND, and 3V3(OUT) to the corresponding DRV8825 pins as per the table above.
- Identify Motor Coils: Use your multimeter in continuity mode. Probe the 4 motor wires. Two wires will beep (Coil A), and the other two will beep (Coil B). Connect Coil A to 1A and 1B, and Coil B to 2A and 2B on the driver.
- Apply High Power: Connect your 12V power supply positive to VMOT and negative to the power GND pin. Double-check that the logic GND and power GND are tied together on the driver board.
MicroPython Code with Error Handling
This firmware targets the Raspberry Pi Pico W (RP2040) running MicroPython v1.22+. It includes explicit pin definitions, CPU frequency locking to prevent step-timing jitter, and a custom exception handler for stalled motors or user interrupts.
from machine import Pin, freq
import time
# Lock CPU to 125MHz for deterministic microsecond timing
freq(125000000)
# Pin Definitions
STEP_PIN_NUM = 2
DIR_PIN_NUM = 3
# Motor Parameters
STEPS_PER_REV = 200 # 1.8 degree motor
MICROSTEPS = 16 # Set by MS1/MS2/MS3 jumpers
class StepperController:
def __init__(self, step_pin, dir_pin):
try:
self.step = Pin(step_pin, Pin.OUT)
self.dir = Pin(dir_pin, Pin.OUT)
self.step.value(0)
self.dir.value(0)
except ValueError as e:
raise RuntimeError(f'Hardware config failed: {e}')
def move(self, revolutions, clockwise=True, rpm=60):
self.dir.value(1 if clockwise else 0)
total_steps = int(revolutions * STEPS_PER_REV * MICROSTEPS)
# Calculate delay between steps in microseconds
# 60 sec / (rpm * total_steps_per_rev) * 1,000,000
step_delay_us = int((60.0 / (rpm * STEPS_PER_REV * MICROSTEPS)) * 1000000)
if step_delay_us < 50:
raise ValueError(f'Calculated step delay ({step_delay_us}us) is too fast for software stepping.')
print(f'Moving {revolutions} revs at {rpm} RPM. Delay: {step_delay_us}us')
try:
for _ in range(total_steps):
self.step.value(1)
time.sleep_us(10) # DRV8825 requires min 1.9us high pulse
self.step.value(0)
time.sleep_us(step_delay_us - 10)
except KeyboardInterrupt:
print('\n[HALT] User interrupted. Motor de-energized.')
self.step.value(0)
except Exception as e:
print(f'[FAULT] Stepping aborted: {e}')
self.step.value(0)
if __name__ == '__main__':
try:
motor = StepperController(STEP_PIN_NUM, DIR_PIN_NUM)
# Move 5 revolutions clockwise at 30 RPM
motor.move(revolutions=5, clockwise=True, rpm=30)
time.sleep(1)
# Move 2 revolutions counter-clockwise at 60 RPM
motor.move(revolutions=2, clockwise=False, rpm=60)
except RuntimeError as e:
print(f'System Error: {e}')
Debugging: First Three Things to Check
When the motor fails to spin, vibrates violently, or the Pico throws an exception, follow this ranked diagnostic path. Do not swap parts until you have verified these three baseline conditions.
1. The Exact Error String: ValueError: Pin(36) doesn't exist
If your script crashes immediately on initialization with ValueError: Pin(36) doesn't exist (or similar pin numbers like 37, 38, 40), you have confused physical board pins with GPIO numbers.
- Cause A (Most Likely): You mapped the STEP or DIR variable to a physical pin number (e.g., Physical Pin 36 is 3V3 OUT) instead of the GPIO number (GP2 is Physical Pin 4). Fix: Use the GP numbers in the Pin() constructor.
- Cause B: You are using an ESP32 pinout reference by mistake. Fix: Verify against the official Pico pinout PDF.
2. Motor Vibrates but Doesn't Rotate
This is a hardware phasing issue. The driver is energizing the coils out of sequence.
- Cause: Coil pairs are mixed. You have one wire from Coil A and one from Coil B paired together.
- Fix: Unplug power. Use a multimeter to find the two pairs of wires that show continuity (usually 1-3 ohms). Keep the pairs intact when plugging them into 1A/1B and 2A/2B.
3. Motor Skips Steps Under Load
The software is commanding steps faster than the magnetic field can collapse, or the current limit is too low.
- Cause: Vref is set too low, causing the driver to cut power before the step completes.
- Fix: Re-measure Vref. If your board has a 0.1Ω sense resistor, Vref = Current / 2. For a 1.5A motor, ensure Vref is exactly 0.75V. If it's at 0.3V, turn the pot clockwise slightly.
Extending and Simplifying the Build
Once the baseline pico driver circuit is proven, you can adapt it to your specific mechanical constraints.
How to Simplify (For Basic Actuators)
If you are building a simple linear actuator or a conveyor belt where precision and noise don't matter, drop the microstepping. Tie the MS1, MS2, and MS3 pins on the DRV8825 directly to GND. This forces the driver into full-step mode. You must then update the MICROSTEPS variable in the MicroPython code to 1. This drastically reduces CPU overhead and allows for higher RPMs before the Pico's software loop bottlenecks.
How to Extend (For CNC and 3D Printing)
Software-stepping (using time.sleep_us) blocks the Pico's main thread, meaning you cannot read WiFi sensors or update a display while the motor moves. To extend this build for multi-axis CNC:
- Use Hardware PIO: The RP2040 features Programmable I/O (PIO) blocks. You can offload the step-pulse generation to a PIO state machine, freeing the main CPU. Look into the rp2.PIO MicroPython documentation for non-blocking pulse generation.
- Add Limit Switches: Wire mechanical limit switches to GP4 and GP5 with internal pull-ups enabled (
Pin.PULL_UP). UsePin.irq()to trigger a hardware interrupt that instantly sets the STEP pin low, preventing mechanical crashes without polling delays. - Upgrade to UART: If acoustic noise becomes an issue in an enclosure, swap the DRV8825 for a TMC2209. The TMC2209 uses the same step/dir pins but adds a UART interface to the Pico's TX/RX pins, allowing you to dynamically adjust the motor current and enable 'StealthChop' mode via serial commands.






