← Back to projects

4-Wire Stepper Motor Driver

Open-source precision stepper control for optical mirror mounts via Raspberry Pi GPIO

2024python, raspberry-pi, motor-control, quantum-optics

View on GitHub


The Problem

In quantum optics labs, precise positioning of optical components is critical. Mirror mounts, beam steering optics, and alignment stages all require sub-micron positioning accuracy. The industry standard solution is piezoelectric actuators—and they're expensive.

A typical piezo-driven mirror mount costs $2,000-$5,000. For a lab with dozens of optical elements requiring motorized adjustment, this adds up quickly. Our lab faced a $30,000+ budget for motorized mirror mounts alone.

The question: Can we achieve comparable precision with commodity stepper motors at a fraction of the cost?


The Solution

I developed an open-source stepper motor driver package that:

  • Controls any 4-wire bidirectional stepper motor via Raspberry Pi GPIO
  • Implements microstepping for smooth, precise movement
  • Exposes motor control via NDSP for remote operation
  • Maintains position state across power cycles
  • Supports saved positions for repeatable alignments

Cost comparison:

SolutionPer-unit cost12 units
Piezo actuators~$2,500$30,000
Stepper + driver + Pi~$50$600
Savings$29,400

The trade-off is speed (steppers are slower) and ultimate resolution (piezos can achieve nanometer precision). For our application—beam alignment that changes infrequently—this was acceptable.


System Architecture

System Architecture Diagram

Hardware Stack

[ARTIQ Master] --Network--> [Raspberry Pi] --GPIO--> [Stepper Driver] ---> [Motor]

Components:

  • Raspberry Pi 4 — Runs the NDSP server, handles GPIO
  • A4988/DRV8825 stepper driver — Current control, microstepping
  • NEMA 17 stepper motor — Standard 1.8° step angle (200 steps/rev)
  • Optical mirror mount — Modified to accept stepper actuation

Software Architecture

stepper_motor_driver/
├── src/
│   ├── stepper_motor_control.py   # Core motor control class
│   ├── aqctl_stepper_motor.py     # NDSP server entry point
│   └── gpio_interface.py          # Raspberry Pi GPIO abstraction
├── config/
│   └── motor_config.json          # Pin mappings, microstepping modes
└── data/
    └── positions.json             # Persistent position storage

Motor Control Implementation

Stepper Motor Basics

A 4-wire stepper motor has two coils. By energizing them in sequence, we rotate the motor in discrete steps. The standard sequence for full-stepping:

StepCoil ACoil B
1+10
20+1
3-10
40-1

This gives 200 steps per revolution (1.8° per step). For precision optics, this isn't fine enough.

Microstepping

Microstepping divides each full step into smaller increments by varying the current in each coil. Instead of binary on/off, we use PWM to create intermediate current levels.

Microstepping modes supported:

ModeSteps/revResolution
Full step2001.8°
Half step4000.9°
1/4 step8000.45°
1/8 step1,6000.225°
1/16 step3,2000.1125°
1/32 step6,4000.056°

At 1/32 microstepping with a fine-pitch lead screw (0.5mm pitch), we achieve:

Linear resolution = 0.5mm / 6400 steps = 0.078 µm per step

This approaches piezo-level precision for a fraction of the cost.

Implementation

The core stepping function:

def step_motor(self, steps, delay=0.02):
    """
    Perform step pulses with position tracking.
Args:
    steps: Number of steps (positive = forward, negative = reverse)
    delay: Time between steps in seconds
"""
# Set direction
direction = 1 if steps > 0 else -1
GPIO.output(self.DIR_PIN, GPIO.HIGH if direction > 0 else GPIO.LOW)
# Pulse the step pin
for _ in range(abs(steps)):
    GPIO.output(self.STEP_PIN, GPIO.HIGH)
    time.sleep(delay / 2)
    GPIO.output(self.STEP_PIN, GPIO.LOW)
    time.sleep(delay / 2)
self.current_position += direction
# Persist position to survive power cycles
self.save_positions()

Key design decisions:

DecisionRationale
GPIO-based controlSimpler than SPI/I2C, sufficient for step rates <10kHz
Position persistenceOptical alignments must survive power cycles
Configurable delayTrade speed vs. torque for different loads
Direction before steppingEnsures clean direction changes

Position Management

The Optical Alignment Problem

When aligning optics, you find an optimal position through iterative adjustment. Once found, you need to:

  • Remember it — So you can return after moving away
  • Name it — Different positions for different experiments
  • Survive reboots — Power cycles shouldn't lose alignment

Implementation

Position state is stored in JSON:

{
  "current_position": 15234,
  "home_position": 0,
  "saved_positions": {
    "beam_aligned": 15234,
    "maintenance": 0,
    "experiment_A": 12500,
    "experiment_B": 18700
  }
}

API for position management:

# Set current position as "home"
motor.set_home()
# Save current position with a name
motor.save_position("beam_aligned")
# Return to a saved position
motor.go_to_saved_position("beam_aligned")
# Move to absolute position
motor.move_to_absolute(15234)
# Move relative to current position
motor.move_relative(100)  # Move 100 steps forward

Network Integration (NDSP)

Why Network Control?

Optical tables are often in shielded rooms or clean rooms where direct access is limited. Operators need to adjust optics from the control room.

NDSP Server

The motor controller runs as an NDSP server on the Raspberry Pi:

python3 -m src.aqctl_stepper_motor --port 3252

Remote clients can then issue commands:

# List available methods
sipyco_rpctool 192.168.1.50 3252 list-methods
# Move motor 100 steps
sipyco_rpctool 192.168.1.50 3252 call step_motor 100
# Move to absolute position
sipyco_rpctool 192.168.1.50 3252 call move_to_absolute 15234 0.01
# Go to saved position
sipyco_rpctool 192.168.1.50 3252 call go_to_saved_position beam_aligned

ARTIQ Integration

In the ARTIQ device database:

device_db = {
    "mirror_x": {
        "type": "controller",
        "host": "192.168.1.50",
        "port": 3252,
        "command": "python3 -m src.aqctl_stepper_motor"
    }
}

In experiments:

class AlignmentExperiment(EnvExperiment):
    def build(self):
        self.setattr_device("mirror_x")
def run(self):
    # Move to alignment position before taking data
    self.mirror_x.go_to_saved_position("beam_aligned")
    delay(1*s)  # Wait for mechanical settling
    # ... run experiment ...

Challenges and Solutions

Challenge 1: Missed Steps

Problem: At high speeds or under load, stepper motors can miss steps. The software thinks it's at position X, but the motor is actually at position X-N.

Solution:

  • Characterize maximum reliable step rate for each motor/load combination
  • Implement acceleration ramps to avoid sudden speed changes
  • Add homing routine with limit switch for absolute reference
def accelerated_move(self, steps, max_delay=0.02, min_delay=0.005):
    """Move with acceleration ramp to prevent missed steps."""
    accel_steps = min(abs(steps) // 4, 100)
for i in range(accel_steps):
    delay = max_delay - (max_delay - min_delay) * (i / accel_steps)
    self.step_motor(1 if steps > 0 else -1, delay)
# Cruise at max speed
cruise_steps = abs(steps) - 2 * accel_steps
self.step_motor(cruise_steps if steps > 0 else -cruise_steps, min_delay)
# Decelerate
for i in range(accel_steps):
    delay = min_delay + (max_delay - min_delay) * (i / accel_steps)
    self.step_motor(1 if steps > 0 else -1, delay)

Challenge 2: Vibration at Rest

Problem: Steppers hold position by energizing coils, causing a 60Hz hum and micro-vibrations. For sensitive optical measurements, this is unacceptable.

Solution: Implement "disable after move" option that de-energizes the motor after reaching position. The motor's detent torque (magnetic cogging) holds position without power.

def move_and_disable(self, target_position, delay=0.02):
    """Move to position then disable to eliminate vibration."""
    self.move_to_absolute(target_position, delay)
    time.sleep(0.1)  # Brief settle time
    GPIO.output(self.ENABLE_PIN, GPIO.HIGH)  # Disable driver

Trade-off: Without holding torque, external forces can move the motor. For our fixed optical mounts, this was acceptable.

Challenge 3: Position Loss on Power Cycle

Problem: Unlike encoders, steppers have no absolute position feedback. Power loss means position is unknown.

Solution:

  • Persist position to file after every move
  • On startup, load last known position from file
  • Provide manual homing routine for recovery

Results

Performance Achieved

ParameterValue
Resolution0.078 µm (at 1/32 microstepping)
Repeatability±2 µm
Max speed500 steps/sec
Position range±50mm (limited by lead screw)
Latency (network)<50ms

Deployment

Currently deployed on:

  • 8 beam steering mirrors
  • 4 translation stages
  • 2 rotation mounts

Cost Savings

Piezo solution: 14 units × $2,500 = $35,000
Stepper solution: 14 units × $50 = $700
Development time: ~40 hours
Net savings: $34,300

Lessons Learned

What Worked

  • NDSP abstraction — Same remote control pattern as Thorlabs drivers
  • Position persistence — Critical for optical work; JSON is simple and human-readable
  • Microstepping — Achieved sub-micron resolution with commodity hardware

What I'd Improve

  • Add encoder feedback — For critical applications, closed-loop would eliminate missed step concerns
  • Implement acceleration profiles — S-curve acceleration for smoother motion
  • Better configuration management — Currently hardcoded pins; should use config files
  • Add web interface — For operators unfamiliar with command-line tools

Technical Specifications

ComponentSpecification
PlatformRaspberry Pi 4 (any Pi with GPIO)
LanguagePython 3.7+
Motor type4-wire bipolar stepper (NEMA 17 tested)
DriverA4988, DRV8825, or compatible
MicrosteppingUp to 1/32 (driver dependent)
NetworkARTIQ NDSP over TCP/IP
DependenciesRPi.GPIO, sipyco

Conclusion

This project demonstrated that precision optical positioning doesn't require expensive piezoelectric actuators. By combining commodity stepper motors with microstepping drivers and careful software design, we achieved sub-micron positioning at 2% of the commercial solution cost.

The key engineering decisions—microstepping for resolution, position persistence for reliability, and NDSP for remote access—addressed the real requirements of an optics lab. The system has been in production use for months with no position-related failures.

More broadly, this project reinforced that understanding the application constraints is as important as the technical implementation. We didn't need nanometer precision or millisecond response times. By right-sizing the solution, we saved $34,000 while meeting all actual requirements.