4-Wire Stepper Motor Driver
Open-source precision stepper control for optical mirror mounts via Raspberry Pi GPIO
2024 • python, raspberry-pi, motor-control, quantum-optics
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:
| Solution | Per-unit cost | 12 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

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 storageMotor 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:
| Step | Coil A | Coil B |
|---|---|---|
| 1 | +1 | 0 |
| 2 | 0 | +1 |
| 3 | -1 | 0 |
| 4 | 0 | -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:
| Mode | Steps/rev | Resolution |
|---|---|---|
| Full step | 200 | 1.8° |
| Half step | 400 | 0.9° |
| 1/4 step | 800 | 0.45° |
| 1/8 step | 1,600 | 0.225° |
| 1/16 step | 3,200 | 0.1125° |
| 1/32 step | 6,400 | 0.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 stepThis 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:
| Decision | Rationale |
|---|---|
| GPIO-based control | Simpler than SPI/I2C, sufficient for step rates <10kHz |
| Position persistence | Optical alignments must survive power cycles |
| Configurable delay | Trade speed vs. torque for different loads |
| Direction before stepping | Ensures 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 forwardNetwork 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 3252Remote 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_alignedARTIQ 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 driverTrade-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
| Parameter | Value |
|---|---|
| Resolution | 0.078 µm (at 1/32 microstepping) |
| Repeatability | ±2 µm |
| Max speed | 500 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,300Lessons 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
| Component | Specification |
|---|---|
| Platform | Raspberry Pi 4 (any Pi with GPIO) |
| Language | Python 3.7+ |
| Motor type | 4-wire bipolar stepper (NEMA 17 tested) |
| Driver | A4988, DRV8825, or compatible |
| Microstepping | Up to 1/32 (driver dependent) |
| Network | ARTIQ NDSP over TCP/IP |
| Dependencies | RPi.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.