A configuration-driven stepper motor motion control library for Rust, designed for embedded systems with no_std support.
- 📁 Configuration-Driven: Define motor parameters and trajectories in TOML files
- 🔧 Embedded-Ready: Full
no_stdsupport withembedded-hal 1.0integration - ⚡ Asymmetric Motion Profiles: Independent acceleration and deceleration rates
- 🎯 Named Trajectories: Execute movements by name with registry-based lookup
- 📐 Type-Safe Units: Physical quantities with compile-time unit checking
- 🛡️ Mechanical Constraints: Automatic validation against hardware limits
- 📍 Absolute Position Tracking: i64 step-based position management
- 🔄 Backlash Compensation: Configurable mechanical play compensation
Add to your Cargo.toml:
[dependencies]
stepper-motion = "0.1"| Feature | Default | Description |
|---|---|---|
std |
✓ | Standard library support, TOML file loading |
alloc |
Heap allocation without full std | |
defmt |
defmt formatting for embedded debugging |
|
async |
Async executor support (planned) |
For no_std environments:
[dependencies]
stepper-motion = { version = "0.1", default-features = false, features = ["alloc"] }# motion.toml
[motors.pan_axis]
name = "Pan Axis"
steps_per_revolution = 200
microsteps = 16
gear_ratio = 4.0
max_velocity_deg_per_sec = 180.0
max_acceleration_deg_per_sec2 = 360.0
invert_direction = false
backlash_compensation_deg = 0.5
[motors.pan_axis.limits]
min_degrees = -180.0
max_degrees = 180.0
policy = "reject"
[trajectories.home]
motor = "pan_axis"
target_degrees = 0.0
velocity_percent = 50
[trajectories.quarter_turn]
motor = "pan_axis"
target_degrees = 90.0
velocity_percent = 100
acceleration_deg_per_sec2 = 360.0
deceleration_deg_per_sec2 = 180.0 # Asymmetric: slower deceluse stepper_motion::{
SystemConfig,
motor::StepperMotorBuilder,
trajectory::TrajectoryRegistry,
config::units::Degrees,
};
// Load configuration (requires `std` feature)
fn main() -> Result<(), stepper_motion::Error> {
// Parse TOML configuration
let config: SystemConfig = toml::from_str(include_str!("motion.toml"))?;
// Get motor configuration
let motor_config = config.motor("pan_axis").expect("Motor not found");
// Your hardware pins (implement embedded-hal 1.0 traits)
let step_pin = MyStepPin::new();
let dir_pin = MyDirPin::new();
let delay = MyDelay::new();
// Build motor from configuration
let motor = StepperMotorBuilder::new()
.step_pin(step_pin)
.dir_pin(dir_pin)
.delay(delay)
.from_motor_config(motor_config)
.build()?;
// Load trajectory registry for named lookups
let registry = TrajectoryRegistry::from_config(&config);
// Get trajectory by name
let trajectory = registry.get_or_error("quarter_turn")?;
println!("Target: {}°", trajectory.target_degrees.0);
Ok(())
}use stepper_motion::{
motor::StepperMotorBuilder,
config::units::{Degrees, DegreesPerSec, DegreesPerSecSquared, Microsteps},
};
// Create motor with explicit parameters
let motor = StepperMotorBuilder::new()
.name("demo_motor")
.step_pin(step_pin)
.dir_pin(dir_pin)
.delay(delay)
.steps_per_revolution(200)
.microsteps(Microsteps::SIXTEENTH)
.gear_ratio(1.0)
.max_velocity(DegreesPerSec(360.0))
.max_acceleration(DegreesPerSecSquared(720.0))
.backlash_steps(10) // Optional: backlash compensation
.build()?;
println!("Motor: {}", motor.name());
println!("Position: {} steps ({} degrees)",
motor.position_steps().0,
motor.position_degrees().0);
// Move to absolute position
let moving_motor = motor.move_to(Degrees(90.0))?;
// Execute step-by-step
while moving_motor.is_moving() {
moving_motor.step()?;
}
let idle_motor = moving_motor.finish();Use this when you need a constant-speed stream of steps until your application decides to stop.
use stepper_motion::{
config::units::DegreesPerSec,
HomingSwitches, StepperMotorBuilder, SwitchConfig, SwitchPolarity,
};
let motor = StepperMotorBuilder::new()
.step_pin(step_pin)
.dir_pin(dir_pin)
.delay(delay)
.steps_per_revolution(200)
.microsteps(stepper_motion::Microsteps::SIXTEENTH)
.max_velocity(stepper_motion::DegreesPerSec(360.0))
.max_acceleration(stepper_motion::DegreesPerSecSquared(720.0))
.build()?;
// 1) Start continuous motion in the desired direction
// let mut moving = motor.start_continuous_forward(DegreesPerSec(120.0))?;
let mut moving = motor.start_continuous_backward(DegreesPerSec(120.0))?;
// Optional physical switches (home/min/max)
let mut switches = HomingSwitches::new(
Some(&mut home_pin),
Some(SwitchConfig::new(SwitchPolarity::NO)),
Some(&mut min_pin),
Some(SwitchConfig::new(SwitchPolarity::NO)),
Some(&mut max_pin),
Some(SwitchConfig::new(SwitchPolarity::NO)),
);
loop {
// 2) Step with switch + soft limit checks
match moving.step_with_switch_checks(&mut switches) {
Ok(_) => {}
Err(stepper_motion::Error::Motor(
stepper_motion::error::MotorError::LimitExceeded { .. },
))
| Err(stepper_motion::Error::Motor(
stepper_motion::error::MotorError::HardwareLimitTriggered { .. },
)) => break,
Err(e) => return Err(e),
}
if should_stop() {
break;
}
}
// 3) Stop (returns Idle state)
let motor = moving.stop();For a one-call convenience API, you can also run until home directly:
let motor = motor.run_continuous_backward_until_home(DegreesPerSec(120.0), &mut switches)?;Async API parity (async feature):
AsyncStepperMotor::start_continuous_forward(...)AsyncStepperMotor::start_continuous_backward(...)AsyncStepperMotor::run_continuous_forward_until_home(...)AsyncStepperMotor::run_continuous_backward_until_home(...)AsyncStepperMotor<..., Moving>::step_async_with_switch_checks(...)AsyncStepperMotor<..., Moving>::stop()
┌─────────────────────────────────────────────────────┐
│ stepper-motion │
├─────────────────────────────────────────────────────┤
│ config/ │ TOML parsing, validation │
│ ├── motor.rs │ MotorConfig, limits │
│ ├── trajectory.rs│ TrajectoryConfig (asymmetric) │
│ ├── mechanical.rs│ MechanicalConstraints │
│ ├── limits.rs │ SoftLimits, LimitPolicy │
│ └── units.rs │ Degrees, Steps, Microsteps │
├─────────────────────────────────────────────────────┤
│ motor/ │ Hardware abstraction │
│ ├── driver.rs │ StepperMotor<STEP,DIR,DELAY,S> │
│ ├── builder.rs │ Builder pattern construction │
│ ├── state.rs │ Type-state: Idle, Moving, etc. │
│ └── position.rs │ Position tracking (i64 steps) │
├─────────────────────────────────────────────────────┤
│ motion/ │ Motion planning │
│ ├── profile.rs │ MotionProfile (trapezoidal) │
│ └── executor.rs │ Step pulse generation │
├─────────────────────────────────────────────────────┤
│ trajectory/ │ Named trajectory management │
│ └── registry.rs │ TrajectoryRegistry │
└─────────────────────────────────────────────────────┘
velocity
▲
max ┤ ┌───────┐
│ / \
│ / \
└─/─────────────\────► time
accel cruise decel
(same rate for both)
velocity
▲
max ┤ ┌───────┐
│ /│ │\
│ / │ │ \
└─/──┴───────┴──\───► time
fast slow
accel decel
Set different rates in TOML:
[trajectories.gentle_stop]
motor = "pan_axis"
target_degrees = 90.0
velocity_percent = 100
acceleration_deg_per_sec2 = 720.0 # Fast acceleration
deceleration_deg_per_sec2 = 180.0 # Gentle decelerationOr using percent-based values (relative to motor max):
[trajectories.smooth_move]
motor = "pan_axis"
target_degrees = 45.0
velocity_percent = 75 # 75% of motor's max velocity
acceleration_percent = 100 # 100% of motor's max accelerationDefine hardware limits to prevent damage:
[motors.servo]
name = "Servo Axis"
steps_per_revolution = 200
microsteps = 32
gear_ratio = 5.0 # 5:1 reduction gearbox
max_velocity_deg_per_sec = 360.0
max_acceleration_deg_per_sec2 = 720.0
backlash_compensation_deg = 0.5 # Compensate 0.5° backlash on reversal
[motors.servo.limits]
min_degrees = -360.0
max_degrees = 360.0
policy = "reject" # or "clamp"reject: Return error if target position exceeds limitsclamp: Automatically constrain target to nearest limit
The library automatically handles conversions:
let constraints = motor.constraints();
// Configuration values → internal steps
println!("Steps/revolution: {}", constraints.steps_per_revolution);
println!("Steps/degree: {:.4}", constraints.steps_per_degree);
println!("Max velocity: {:.0} steps/s", constraints.max_velocity_steps_per_sec);Run the included examples:
# Basic motor control with mechanical constraints demonstration
cargo run --example basic_motor
# Configuration-driven operation with named trajectories
cargo run --example config_driven
# Multi-motor system demonstration
cargo run --example multi_motorAgent integration prompt for continuous mode:
docs/agent-continuous-motion-prompt.md
The motor uses Rust's type system to enforce valid state transitions:
// Motor starts in Idle state
let motor: StepperMotor<_, _, _, Idle> = builder.build()?;
// move_to() transitions to Moving state
let moving: StepperMotor<_, _, _, Moving> = motor.move_to(Degrees(90.0))?;
// Can only call step() or finish() on Moving motor
while moving.is_moving() {
moving.step()?;
}
// finish() transitions back to Idle
let motor: StepperMotor<_, _, _, Idle> = moving.finish();Rust 1.70.0 or later (required for embedded-hal 1.0).
For embedded systems without standard library:
#![no_std]
#![no_main]
use stepper_motion::{SystemConfig, motor::StepperMotorBuilder};
// Embed configuration at compile time
const CONFIG_TOML: &str = include_str!("../motion.toml");
fn setup() {
// Parse embedded configuration
let config: SystemConfig = toml::from_str(CONFIG_TOML).unwrap();
let motor_config = config.motor("servo").unwrap();
// Build motor with your embedded-hal pins
let motor = StepperMotorBuilder::new()
.step_pin(gpioa.pa0.into_push_pull_output())
.dir_pin(gpioa.pa1.into_push_pull_output())
.delay(timer.delay_us())
.from_motor_config(motor_config)
.build()
.unwrap();
}Contributions are welcome! Please read the CHANGELOG for version history.
# Run all tests (46 tests: 25 unit + 21 integration)
cargo test --all-features
# Check no_std compatibility
cargo build --no-default-features
cargo build --no-default-features --features alloc
# Run clippy
cargo clippy --all-features
# Format code
cargo fmt
# Run examples
cargo run --example basic_motor
cargo run --example config_driven
cargo run --example multi_motorLicensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.