The planner describes where the tool must be. The motor decides how far its shaft must move to make that true.
Backlash is not a change to geometry. It is a difference between requested load position and required motor position.
A motor-level motion model for Smoothieware
Static slack and inertial lag, compensated live without corrupting Smoothie’s logical toolpath.
Begin with the invariantThe planner describes where the tool must be. The motor decides how far its shaft must move to make that true.
Backlash is not a change to geometry. It is a difference between requested load position and required motor position.
The kinematic and planner result. Exact, stable, and never polluted by compensation.
The cumulative STEP pulses emitted to the driver, including corrective motion.
The physical axis position inferred after removing the modeled offset from the shaft position.
Smoothie already separates geometry, planning, queue ownership, and pulse emission. The feature belongs at the final boundary.
Applies work offsets, compensation transforms, segmentation, inverse kinematics, per-axis rate limits, and per-actuator acceleration limits.
append_milestone() at line 1273Calls steps_to_target(), computes junction speed, performs reverse and forward look-ahead, and prepares trapezoids.
The planner produces at head_i, the interrupt consumes through isr_tail_i, and idle context reclaims through tail_i.
Each motor receives 2.62 fixed-point step phase, acceleration change, plateau rate, and the exact logical step count.
prepare() at line 315The timer interrupt advances a phase accumulator and emits a pulse once it crosses one whole step.
step_tick() at line 134This is the correct home for persistent, per-motor mechanical state. It is also where raw shaft position must become distinct from estimated load position.
step() at line 22Both effects make motor position differ from load position, but they have different causes, timescales, and state.
A dead zone caused by clearance in screws, belts, couplers, gears, or bearings. It appears most clearly when direction reverses.
A transient displacement caused by moving mass, drive stiffness, damping, friction, gravity, velocity, and acceleration.
The same motor coordinate can correspond to two load positions. The result depends on which mechanical flank is engaged.
Here q is logical actuator position, u is motor shaft command, and s is the currently engaged direction. Separate positive and negative offsets allow asymmetric hardware.
The shaft and load move together with a stable directional offset.
The first B steps cross clearance. The load has not moved yet.
Normal logical motion resumes once the new side carries force.
The compensator does not add a generic B steps to every block. It changes the persistent motor offset only when engagement changes. Tiny sign chatter should be rejected with an engagement threshold.
Belts stretch, screws twist, frames deflect, and bearings resist motion. Under force, the shaft must lead the requested load position.
m ẍ
Inertial force required to accelerate the carriage, gantry, tool, and coupled mechanics.
c(ẋ - u̇)
Velocity-dependent dissipation: bearing drag, belt damping, and structural losses.
k(x - u)
Elastic restoring force. Lower stiffness produces more positional deflection for the same force.
Fc sign(ẋ)
Coulomb friction or a consistent load bias. Gravity can add a direction-dependent term on vertical axes.
This is a useful conceptual plant model. Smoothie has no axis encoder in the normal configuration, so it cannot observe x directly. Compensation must therefore be bounded feed-forward unless feedback hardware is added.
A practical first implementation uses directly tunable coefficients. Velocity in steps/s times τv in seconds yields steps. Acceleration in steps/s² times τa² yields steps. Clamp, filter, and slew-limit the result.
Adjust an illustrative open-loop axis. The plot compares the requested path, uncompensated load estimate, and compensated load estimate.
Every input updates the same deterministic simulation. Units are explicit so the graph can be reasoned about.
The model intentionally exposes assumptions. It is a design instrument, not evidence that these default values match a real machine.
The policy is per actuator, persistent across Blocks, independent of kinematics, and deterministic inside the timer interrupt.
Produces actuator-space milestones from machine-space geometry.
Owns logical distance, look-ahead, junction velocity, and trapezoid timing.
Advances logical phase at the fixed timer rate and asks each motor for its physical pulse decision.
Owns engagement state, static offset, dynamic filter, limits, and estimated load position.
Owns DIR setup, STEP pulse, raw shaft count, enable state, and hardware pins.
struct MotionSample {
int64_t logical_position_fp; // 2.62 steps
int64_t logical_velocity_fp; // 2.62 steps per tick
int64_t logical_acceleration_fp; // 2.62 steps per tick squared
bool logical_motion_complete;
};
struct PulseDecision {
bool should_step;
bool direction;
bool requires_direction_setup_tick;
};
class StepperMotionCompensator {
public:
PulseDecision advance(const MotionSample& sample);
int32_t estimated_load_steps() const;
int32_t commanded_motor_steps() const;
bool has_settled() const;
void reset_at_known_load_position(int32_t logical_steps, Direction engaged);
void freeze_after_halt();
};
MotionSample
A signed, block-local logical state. It contains no mechanical policy and is safe to derive from existing fixed-point Block data.
PulseDecision
A bounded hardware action for this ticker interrupt. It can request zero or one pulse and explicitly handles DIR setup timing.
estimated_load_steps()
The position used for real-time machine coordinates and recovery after an abort. It is not the raw pulse counter.
reset_at_known_load_position()
The homing and manual-reset seam. It makes mechanical engagement explicit instead of silently assuming shaft equals load.
The logical phase remains the timing authority. The compensator converts its continuously changing target into a safe physical pulse stream.
Apply the existing acceleration change and trapezoid phase transitions.
Integrate signed steps per tick without assuming a pulse has already occurred.
Select directional free-play offset and update the bounded dynamic lead.
Quantize the difference into zero or one legal pulse for this interrupt.
Change direction first, then wait the configured setup interval before pulsing.
The logical profile is complete and the compensated physical target has settled.
// Existing Block math remains the logical trajectory generator.
MotionSample sample {
.logical_position_fp = tickinfo.logical_phase,
.logical_velocity_fp = signed_rate(block, tickinfo),
.logical_acceleration_fp = signed_acceleration(block, tickinfo),
.logical_motion_complete = tickinfo.logical_step_count ==
tickinfo.logical_steps_to_move
};
PulseDecision decision = motor[m]->advance_compensated_motion(sample);
if (decision.requires_direction_setup_tick) {
motor[m]->set_direction(decision.direction);
} else if (decision.should_step) {
motor[m]->step();
unstep.set(m);
}
if (sample.logical_motion_complete &&
motor[m]->compensation_has_settled()) {
motor[m]->stop_moving();
}
Direction is not merely a bit on the current Block. It is a remembered physical relationship between the shaft and the driven load.
Use b(+). Positive mechanical flank is carrying force.
Slew toward the opposite offset. Load motion is not inferred from these take-up pulses.
Use b(-). Negative mechanical flank is carrying force.
This hysteresis prevents tiny delta segments, rounding noise, or near-zero oscillation from commanding repeated full backlash take-up moves.
Smoothie’s arm solution already determines which motor motion realizes X, Y, and Z. Backlash must follow that same boundary.
A Cartesian X reversal can reverse both motors, one motor, or neither depending on the combined X/Y trajectory. Applying an X-axis backlash offset before inverse kinematics would model the wrong physical component.
The feature is incomplete unless reports, homing, manual steps, probes, limits, and emergency stops all agree on which coordinate they use.
M114.1 / M114.2
Forward kinematics must receive compensated load coordinates, not the raw pulse counter.
M114.3
Keep shaft position observable for calibration, diagnostics, and comparison with the load estimate.
ON_HALT
Do not issue a final correction. Recover logical machine position from the frozen load estimate.
Homing
Disable dynamic lead while probing a stop, then initialize static state from the final homing direction.
class StepperMotor : public Module {
public:
int32_t get_commanded_motor_step() const {
return commanded_motor_steps;
}
int32_t get_estimated_load_step() const {
return motion_compensator.estimated_load_steps();
}
float get_estimated_load_position() const {
return get_estimated_load_step() / steps_per_mm;
}
private:
volatile int32_t commanded_motor_steps;
StepperMotionCompensator motion_compensator;
};
Calibration belongs to each physical motor. Step units work for linear, rotary, and coupled kinematics without pretending every actuator is millimetric.
# Static free-play model
alpha_motion_compensation_enable true
alpha_backlash_positive_steps 12
alpha_backlash_negative_steps 14
alpha_backlash_engagement_min_steps 3
# Dynamic feed-forward model
alpha_dynamic_velocity_lead_ms 0.20
alpha_dynamic_acceleration_lead_ms2 0.015
alpha_dynamic_static_load_bias_steps 0
alpha_dynamic_max_lead_steps 24
alpha_dynamic_slew_steps_per_second 4000
# Pulse budget and safe state
alpha_compensation_step_rate_margin 0.10
alpha_homing_engagement_direction negative
The 100 kHz ticker can emit at most one pulse per motor per tick. Compensation needs real pulse-rate headroom. If the bound cannot be met, the planner-visible rate must be reduced before execution.
Static compensation establishes the architectural boundary, position semantics, and tests. Dynamic feed-forward then becomes one additional offset producer.
Establish state
Add the compensator shell, raw versus estimated position accessors, reset semantics, and deterministic fixed-point types. Compensation still returns zero.
Take up free play
Add engagement memory, asymmetric offsets, reversal thresholds, bounded pulse chasing, and static Cartesian verification.
Close every lifecycle seam
Prove that every path which currently reads or resets current_position_steps uses the intended coordinate after compensation is enabled.
Add dynamic lead
Start experimental and disabled. Add filtering, clamping, slew limits, telemetry, and measured calibration patterns before broad use.
int64_t StepperMotionCompensator::desired_motor_position_fp(
const MotionSample& sample
) {
const Direction engagement = update_engagement(sample);
const int64_t static_offset_fp = offset_for(engagement);
const int64_t raw_dynamic_offset_fp =
multiply_fp(velocity_lead, sample.logical_velocity_fp) +
multiply_fp(acceleration_lead, sample.logical_acceleration_fp) +
directional_load_bias_fp(engagement);
const int64_t dynamic_offset_fp = slew_and_clamp(raw_dynamic_offset_fp);
return sample.logical_position_fp +
static_offset_fp +
dynamic_offset_fp;
}
A green compiler is not evidence that a motion-control feature is safe. Each layer needs a different kind of proof.
Same-direction moves preserve offset. One reversal emits exactly the calibrated offset change. Tiny chatter does not re-engage. Asymmetric values work in both directions. Clamp and slew bounds hold for every tick.
No motor emits more than one pulse per ticker period. DIR setup time is respected. Logical trajectory duration remains deterministic. Block handoff has no missing or duplicate pulse.
Engagement persists across Blocks, queue starvation, continuous motion, and segmentation. The ISR still consumes only ready, unlocked Blocks, and idle cleanup remains allocation-safe.
M114 variants distinguish logical, estimated load, and shaft coordinates. Halt recovery does not shift machine coordinates by the active offset. Homing initializes a known engagement state.
Cartesian, CoreXY, and delta fixtures prove that compensation follows actuators rather than axis labels. Nonlinear segmentation must not create engagement chatter near a turning point.
Use dial-indicator reversal patterns at multiple positions and speeds. Validate low-speed static backlash first, then acceleration-dependent residuals. Keep the dynamic model disabled until measurements justify it.
The final metric is measured load error. Internal agreement between logical counters and emitted pulses is necessary, but it cannot validate an open-loop physical model by itself.
Keeping the boundary narrow is what makes the feature understandable, testable, and safe enough to tune.
StepperMotor::step()Smoothie keeps its exact logical trajectory. Each actuator supplies the extra shaft motion required by its own mechanics.
Static offset handles engagement. Dynamic lead handles modeled load. The pulse scheduler realizes u while every upstream coordinate remains q.