Motion Profile Simulator: Trapezoidal and S-Curve
Plan a move as a trapezoidal and a jerk-limited S-curve profile side by side, and watch position, velocity, acceleration and jerk change as you drag the limits.
- Category
- Control Systems
- Time
- 30–60 min
- Platform
- Browser · Arduino
01 / Start here
Introduction
Every motor command hides a schedule. Ask an axis to go somewhere and something has to decide how fast it gets up to speed, how long it cruises, and when it starts slowing down. This lab makes that schedule visible: set a move, set what the machine can do, and watch a trapezoidal and an S-curve profile plan it side by side — including the cases where your limits cannot all be met at once.
Live lab / Analytic profile planner
Motion profile simulator
Set a move and watch both profiles plan it. Drag the limits to push the move into a triangle, starve the jerk until acceleration never saturates, and read what the S-curve costs.
Four stacked plots share a time axis. Each is the derivative of the one above it, so a corner in velocity becomes a step in acceleration and an impulse in jerk. The trapezoidal profile is drawn in the accent colour, the S-curve alongside it.
- Trapezoidal
- 1.200 s
- S-curve
- 1.300 s
- Jerk cost
- +0.100 s
- Peak speed
- 100 mm/s
- Peak accel
- 500 mm/s²
- Cruise
- 0.700 s
Keyboard: focus the plots, then use Space to play/pause, B to rewind, R to reset, M to cycle which profiles are shown, and F for full screen.
Controls
Distance and top speed describe the move you want. Acceleration and jerk limit describe what the machine can actually do. The lab re-plans on every drag — there is nothing to press before the curves respond.
Show switches between both profiles and either one alone. Play runs a playhead across the plots in real time, which is the quickest way to feel that the S-curve finishes later.
The note under the limits tells you when a limit stops being reachable, which is the whole reason the sliders are worth dragging.
Theory
Four plots, stacked, each the derivative of the one above it. That stacking is the argument: a corner in velocity is a step in acceleration, and a step in acceleration is an impulse in jerk. The discontinuity never disappears as you differentiate — it moves down until it becomes unbounded.
A trapezoidal profile accelerates at a constant rate, cruises, then decelerates. Velocity is a trapezoid, acceleration is a square wave, and jerk is a set of impulses at the corners. Those impulses are what a machine hears as a knock and what a compliant drivetrain answers with ringing.
An S-curve profile limits jerk as well. Acceleration ramps in over t_j = a_max / j_max, holds, and ramps out, so velocity gains rounded corners. It costs exactly one jerk time and buys bounded shock.
Neither profile can always honour every limit. A move shorter than v²/a never reaches the top speed. An S-curve whose top speed is below a²/j never reaches the acceleration limit. Both are ordinary and both are visible here.
Algorithm
The lab solves the profile rather than integrating it. Each segment has a constant jerk, so within one segment:
a(τ) = a₀ + j·τ
v(τ) = v₀ + a₀·τ + j·τ²/2
x(τ) = x₀ + v₀·τ + a₀·τ²/2 + j·τ³/6
Those are exact, which is why the move lands on the target instead of near it, and why dragging a slider re-plans instantly.
Picking the peak velocity uses one identity: a velocity ramp is odd-symmetric about its midpoint, so it always covers exactly v_peak/2 × duration whatever shape it has. That turns “is there a cruise phase?” into a single comparison, and makes the distance-limited peak solvable in closed form — a quadratic while acceleration still saturates, a cube root once it does not.
Source code
A trapezoidal generator small enough to paste into a sketch. It returns the position your control loop should be chasing at time t:
// Target position t seconds into a trapezoidal move of `d` mm.
float profilePosition(float t, float d, float vMax, float a) {
float tAcc = vMax / a;
float dAcc = vMax * vMax / (2 * a);
if (2 * dAcc > d) { // too short to reach vMax
float tPeak = sqrtf(d / a); // triangular profile
if (t >= 2 * tPeak) return d;
if (t < tPeak) return 0.5f * a * t * t;
float left = 2 * tPeak - t;
return d - 0.5f * a * left * left;
}
float tCruise = (d - 2 * dAcc) / vMax;
float tTotal = 2 * tAcc + tCruise;
if (t >= tTotal) return d;
if (t < tAcc) return 0.5f * a * t * t;
if (t < tAcc + tCruise) return dAcc + vMax * (t - tAcc);
float left = tTotal - t;
return d - 0.5f * a * left * left;
}
Call it once per control period and feed the result to whatever closes the loop. AccelStepper does the equivalent internally for steppers; there is no S-curve option, so a jerk-limited profile is one you generate yourself.
Circuit diagram
There is no circuit unique to this lab — the profile is a layer of arithmetic, not a part. It sits between your command source and the loop you already have:
target position → PROFILE → setpoint → controller → driver → motor
↑ │
└────── encoder ─────────┘
The wiring is whatever your motor already needs: a driver sized for the peak current the ramp demands, and encoder feedback if you want the loop closed. What changes is only what the controller is asked to follow.
The full derivation, the Marlin “jerk” confusion, and the reason profiling often fixes a PID that looked badly tuned are in motion profiles: trapezoidal and S-curve acceleration.
Hardware checklist
Components
- A stepper or an encoder-equipped DC motor
- A driver sized for the peak current the profile asks for
- A controller with a timer stable enough to hold the sample rate
- A rigid mount — compliance is what turns jerk into ringing
Explore the graph
Where this simulator is used
The projects, learning paths, and tutorials that build on this lab.
Continue building
Download resources
Use these on-page references while working through the project. Downloadable project bundles will be added only after their source and version are published.
Common questions
Frequently asked questions
Why does my axis never reach the top speed I set?
Because the move is too short. Reaching a top speed takes a run-up of v²/2a and the same again to stop, so any move shorter than v²/a is over before the ramp finishes. The velocity profile becomes a triangle rather than a trapezoid, and its peak is √(a·d) regardless of what you configured. Raising the speed limit does nothing to those moves — only raising acceleration does. Drag the distance slider below the threshold and the lab will say so.
Is an S-curve always better than a trapezoidal profile?
No. It is always slower, and the machine is not always troubled by the thing it fixes. An S-curve limits jerk, which matters when the drivetrain has compliance — belts, long links, a heavy tool at the end of an arm — because a step in acceleration excites that compliance and it rings afterwards. On a short, stiff, direct-driven axis the ringing may be unmeasurable and the extra time is pure loss. Use the lab to see the cost, then decide whether your machine is paying for something it needs.
How much time does jerk limiting actually cost?
Exactly one jerk time, t_j = a_max / j_max, provided both profiles reach the same top speed and the same acceleration. Not two, and — the surprising part — independent of how far the move is. The S-curve's acceleration segment is longer, so it covers more distance before cruising, so the cruise phase shrinks and gives most of the time back. Only the jerk time itself is unrecoverable. Drag the distance slider and watch the jerk cost stay put.
Why does the acceleration never reach my acceleration limit?
Because the jerk limit is too low for it. Ramping into a_max and back out again takes a velocity change of a_max²/j_max, so if your top speed is below that, the acceleration itself becomes triangular and peaks at √(v·j) instead. It is the same 'ran out of room' story as a short move, one derivative up. The lab calls this out whenever it happens.
Where does the profile go in my code?
Before the controller, not inside it. The profile generates a setpoint — where the axis should be right now — and whatever you already had follows it: a step scheduler, a PID loop, a servo write. This is why profiling often fixes an axis that seemed badly tuned. A step command asks a PID for infinite output, so its error starts at the full move distance and the integral winds up while the output is saturated. Against a profiled target the error stays small and the same gains behave.
Further reading
References
Authoritative sources for going deeper than this simulator's bounded educational model.