Second-order input shaping for axis crosstalk

Updated 2026-06-11
CoreOne

What input shaping is

Input shaping is a pre-filter applied to the stepper motor velocity commands before they reach the motor. The goal is to suppress mechanical vibrations excited by motion. The technique was developed by Singer and Seering (MIT, 1990) and is now standard in 3D printer firmwares.

The signal chain

The motion planner outputs a velocity step: the stepper ISR receives a target velocity and must reach it instantly. In the ideal case, the motor follows the command perfectly. In reality, the mechanical system — belt, pulley, frame — has a resonant frequency. A velocity step excites that resonance, producing ringing (ghosting) on the printed part.

The input shaper sits between the planner and the step generator. It convolves the velocity step with a short FIR pulse train (the "shaper"):

vshaped(t)=i=0n1aivcommand(tti)v_{\text{shaped}}(t) = \sum_{i=0}^{n-1} a_i \cdot v_{\text{command}}(t - t_i)

The pulse amplitudes aia_i and times tit_i are chosen so that the residual vibration at the target frequency is exactly zero. The filter is causal (t0=0t_0 = 0) and normalized (ai=1\sum a_i = 1), so the total displacement is unchanged — the move simply takes tn1t_{n-1} longer to complete.

Zero-vibration condition

A second-order mechanical resonance responds to an impulse with a decaying sinusoid. Two impulses, spaced by exactly half the resonant period and weighted appropriately, produce sinusoids that cancel at the resonant frequency. A three-impulse shaper (ZVD, MZV, EI) adds a third pulse to make the cancellation robust against damping ratio uncertainty.

Shaper Pulses Time spread Notch depth Damping robustness
ZV 2 0.5 periods Deep Low
MZV 3 1.0 periods Deep Moderate
ZVD 3 1.0 periods Deeper Moderate
EI 3 1.5 periods Broad, deep High

The deeper the notch, the lower the remaining vibration at the target frequency. The broader the notch, the more frequencies it covers — useful when the resonance is not a sharp peak. The cost is a longer shaped move.

In the firmware

The shaper is not a continuous-time filter. It operates in the step ISR, which runs every step event. The shaper stores a queue of upcoming step timestamps; each incoming step is delayed and amplitude-scaled according to the shaper's pulse train. The step ISR overhead is proportional to the number of pulses. The Core One firmware limits each axis to 10 pulses.

Single-stage filter per axis

The stock firmware applies one input shaper per logical axis. On the Core One, the factory defaults are MZV at 60 Hz for X and MZV at 48 Hz for Y. Each shaper is a 3-pulse FIR filter that places a single deep notch at its target frequency.

Motor sweep measurements on a Core One show two distinct resonance clusters:

Configuration Frequency Remaining vibrations Suppression
Factory MZV X@60, Y@48 0.209 79.1%
Best single-shaper (ZVD@57) 57 Hz 0.159 84.1%
Best single-shaper (EI@56) 56 Hz 0.144 85.6%

A single shaper cannot place deep notches at both clusters simultaneously. A ZVD or EI shaper tuned between the two peaks partially suppresses both but leaves significant residual at each.

Crosstalk in CoreXY kinematics

In CoreXY, the kinematic mixing matrix maps logical axis moves to motor steps:

Move direction Motor A steps Motor B steps
+X +1 −1
+Y +1 +1

Every logical X move drives both motors with opposite sign; every Y move drives both with the same sign. A vibration excited on motor A projects onto both X and Y. A vibration excited on motor B also projects onto both axes. The firmware cannot isolate a single motor's vibration to a single logical axis.

Motor sweep data — driving one motor at a time while measuring X and Y response — shows the crosstalk directly:

Motor A sweep (driving motor A, measuring X/Y response):

Motor B sweep (driving motor B, measuring X/Y response):

Driving motor A excites both axes at ~65 Hz. Driving motor B excites both axes at ~42 Hz. The stock firmware's single-shaper-per-axis approach damps one motor's resonance on one axis but leaves the coupled resonance from the other motor largely unfiltered.

The charts show the motor sweep data. Motor A's primary resonance cluster sits at 55–67 Hz. Motor B's primary resonance cluster sits at 33–45 Hz. On both motors, the "wrong-axis" response (crosstalk) is roughly 40–50% of the "correct-axis" response at peak.

A single filter spanning both peaks

A single shaper tuned between the two peaks covers both clusters with a broader, shallower notch. The chart below shows the vibration reduction (VR) transfer function for each shaper — the factor by which the shaper attenuates vibration at each frequency. A deep notch reaches near zero; a shallow notch does not.

The factory configuration uses MZV at 60 Hz (X) and 48 Hz (Y). Because CoreXY mixes both motors into both axes, using different frequencies on X and Y creates direction-dependent residuals — a 45° move excites both motors and sees neither shaper at full strength.

A same-shaper approach — one frequency for both axes — eliminates direction dependence. The best single shaper found from the sweep data is ZVD at 57 Hz or EI at 56 Hz:

Shaper 42 Hz VR 65 Hz VR Worst-case residual
Factory MZV X@60 0.339 0.064 0.339
Factory MZV Y@48 0.126 0.145 0.145
ZVD@57 (same both axes) 0.143 0.040 0.143
EI@56 (same both axes) 0.092 0.014 0.092

The broader EI notch performs better than ZVD, but a single shaper cannot reach the deep suppression of a notch centered on each peak. At 42 Hz — motor B's primary resonance — even the best single shaper leaves 9% of the vibration. At 65 Hz — motor A's primary resonance — it leaves 1.4%. The 42 Hz peak is the limiting factor.

The chart below shows the shaped spectrum: the raw vibration gain multiplied by the shaper's VR at each frequency. The shaded areas are the vibrations that remain after shaping.

The fundamental problem: a single FIR shaper places one deep notch. Two distinct resonance clusters require two notches.

Cascaded second-order shaper

The community firmware adds a cascaded (second-order) input shaper: each axis gets a primary filter for one resonance cluster and a cascade filter for the other cluster. The two filters are convolved into a single FIR pulse train, so the step ISR overhead is unchanged.

The cascade search over the measured data finds:

Configuration Pulses Remaining vibrations Suppression
Factory MZV 60/48 3+3 0.209 79.1%
Best single-shaper 3 0.144 85.6%
ZV@40 + ZVD@64 6 0.058 94.2%
ZVD@37 + EI@61 9 0.032 96.8%

The cascaded shaper places independent notches at both resonance clusters (~42 Hz from motor B and ~65 Hz from motor A). The chart below compares the shaped spectrum for the factory configuration, the best single shaper, and the cascaded shaper. The 9-pulse ZVD+EI cascade reduces remaining vibrations by a factor of 6.5 compared to the factory configuration, and by a factor of 4.5 compared to the best single-shaper configuration.

Each shaper type has a fixed pulse count and total duration:

Shaper type Pulses Total duration (× period) Description
ZV 2 0.5 Minimal, narrow notch
MZV 3 1.0 Default Prusa, moderate notch
ZVD 3 1.0 Deeper notch than MZV
EI 3 1.5 Broader notch, longer duration

A cascade of two shapers multiplies the pulse count: ZVD (3) × EI (3) = 9 pulses. The Core One firmware allows up to 10 pulses per axis, so a 9-pulse cascade fits within the budget. A 6-pulse ZV+ZVD cascade (2 × 3) leaves more headroom.

The implementation convolves two shapers into one pulse train:

void create_cascaded_input_shaper_pulses(
    input_shaper_pulses_t &is_pulses,
    const input_shaper::AxisConfig &primary_config,
    const std::optional<input_shaper::AxisConfig> &cascade_config) {

    input_shaper::Shaper shaper1 = input_shaper::get(
        primary_config.damping_ratio, primary_config.frequency,
        primary_config.vibration_reduction, primary_config.type);

    if (!cascade_config.has_value() || cascade_config->type == Type::null) {
        // No cascade — use the primary shaper alone
        init_input_shaper_pulses(shaper1.a, shaper1.t, shaper1.num_pulses, &is_pulses);
        return;
    }

    input_shaper::Shaper shaper2 = input_shaper::get(
        cascade_config->damping_ratio, cascade_config->frequency,
        cascade_config->vibration_reduction, cascade_config->type);

    // Convolve: combined pulse at (t1_i + t2_j) has amplitude (a1_i * a2_j)
    for (int i = 0; i < n1; ++i) {
        for (int j = 0; j < n2; ++j) {
            combined_a[idx] = shaper1.a[i] * shaper2.a[j];
            combined_t[idx] = shaper1.t[i] + shaper2.t[j];
            ++idx;
        }
    }
}

The cascade configuration is stored alongside the primary configuration in input_shaper::Config:

struct Config {
    std::optional<AxisConfig> axis[3];
    std::optional<AxisConfig> cascade[3]; ///< Cascade second shaper per axis
    std::optional<WeightAdjustConfig> weight_adjust_y;
};

And set via the M593 gcode with new cascade parameters (ST, SF, SD, SV):

M593 X ST ZVD SF 45.0 ST ZVD SF 51.2 ; X primary 45 Hz, cascade 51.2 Hz

The axis swap bug (now fixed upstream)

For over a year, input shaping on the Core One was effectively broken due to a bug in set_logical_axis_config_internal(): the X and Y shaper parameters were swapped. The second call (for Y) overwrote the results of the first call with a reversed axis mapping, causing the X shaper to be applied to Y and vice versa.

This was fixed in v6.5.3 (commit 97f968a8c), and a related calibration bug (M1959 vibrating A/B motors instead of X/Y) was fixed in v6.5.6 (commit 434fd8f67, BFW-8536).

Manual resonance finder

The PR includes a "Motor Vibration" wizard (screen_motor_vibration.cpp) accessible from the Control menu. It drives the stepper motors at a user-selected frequency and amplitude while an attached accelerometer measures the response. This lets users manually sweep frequencies to find both the primary and coupled resonances for each axis — the data needed to configure the cascade filters.

History and status

References

Research Notes

Motor sweep measurements were performed by driving motor A or motor B at a fixed frequency and measuring the X/Y accelerometer response. Two measurement sets exist: one from the center of the bed (2026-06-09), one from a validated position (raw_validated01). Both show the same two-cluster structure: motor A dominant at ~65 Hz, motor B dominant at ~42 Hz.

The "remaining vibrations" metric is a raw weighted sum: sum of shaped gains divided by sum of original gains, pessimized over damping ratios ζ = [0.05, 0.10, 0.15]. This differs from the stock Klipper/Prusa metric which uses a thresholded sum (ignoring gains below a floor). The raw metric is more informative for cascaded shapers because deep cascades push all peaks below typical thresholds, making thresholded metrics uninformative.

The 6-pulse ZV+ZVD cascade is the most compact viable option (≤ MAX_PULSES = 10 per axis, but 6 pulses leaves more headroom). The 9-pulse ZVD+EI cascade gives the best suppression but uses more of the pulse budget.

The factory configuration uses different frequencies for X and Y (60/48 Hz). Because CoreXY kinematics couple both motors to both axes, using different shapers on X and Y creates direction-dependent residuals. The same-shaper approach (one frequency for both axes) eliminates this direction-dependence.

Prusa Research has not publicly addressed the crosstalk problem. The layer shift issues (#4732, #4573) were partially mitigated by the axis swap fix in v6.5.3, but the underlying crosstalk from two distinct motor resonance clusters remains unaddressed in upstream firmware.