This is how I do non-linear acceleration with AccelStepper

1,154 views
Skip to first unread message

Aaron Newsome

unread,
Aug 23, 2021, 5:33:13 PM8/23/21
to accelstepper
Use CUBIC curves!

Every since I built my first stepper motor based projects, it's always come up in every project I've built. How to get non-linear acceleration from the stepper motors? This is how I did it 12 years ago and it still serves me well.

It comes up so often, I wonder why most of the stepper libraries I've come across, don't include some way to do non-linear acceleration natively in the library.

Incidentally, I use this same method for not only controlling the acceleration, but really anything. Using cubic curves, I can set the maxSpeed of the stepper based on the points of the curve, or even set the position of the stepper in time based on the points along the curve.

This allows smooth  stepper movements that can gracefully ease in and of positions very nicely.

Now, most projects probably don't need to go through this level of trouble to do stepper motor moves. Linear acceleration will probably serve 99% of projects well. I've found that MOVING A CAMERA with stepper motors can really benefit from non-linear acceleration.

An added benefit is that once you setup your program to use cubic curves to define speed, acceleration or stepper position, you'll be able to easily execute stepper moves defined by time.

Meaning, you can execute a move like: "move from step 0 to step 25,000 and take 10 seconds to do it".

Again very handy for moving a camera with stepper motor. So handy, again, I wonder why most stepper libraries don't include some way to execute stepper moves based on time. From memory, most of the libraries I've tried either don't provide a way to do it, OR they don't easily allow a way to do it with multiple steppers simultaneously with different accelerations for each stepper.

By now you're probably wondering when am I actually going to explain HOW to do this. Consider the attached image. A screenshot of a little HTML/JS page I made to play around with cubic curves. 

The curve is drawn on a grid with lower left x,y being 0,0 and upper right x,y being 200,200. For this example, the curve start is anchored in the lower left at 0,0 and the curve end is anchored in the upper right at 200,200. Control points for the curve are at 180,40 and 40,180.

You could probably use either the x or y axes to represent whatever you want, but this is how I apply the curve to stepper movements for setting stepper SPEED "ramp UP".

The x axis (left to right) represents TIME. That is, a stepper speed ramp for the move starts at the left at 0,0. And ends at the right. The time can be arbitrary. But lets says, x=200 is 5 seconds.

The y axis (down to up) represents SPEED. That is, the stepper speed all the way at the left is 0. All the way at the right, the curve ends at 100 in the Y axis. If the max speed is set for, say 2750. That means that during this curve represents the following:

The stepper will start it's move at SPEED=0, TIME=0. At the end of 5 seconds, the stepper speed will be set to 2750. To use the curve to actually set the speed, you do a lookup inside your loop that does the run() for the steppers. That is, if in your loop the elapsed time of your move is 1.764 seconds, then you lookup that point on the curve, that is, the Y value at 1.764 seconds and you set your stepper motor speed to that value.

That's it. That's the whole explanation. If you change the control points so that the line between points is is straight, you'll get linear acceleration. If drag the control points down and to the right, up and to the left, you get a more pronounced S shape with more easing at the end and aggressive acceleration through the middle.

In your actual program, which is probably running on a microcontroller, you're not dragging control points. You define the control points in code. I've done this in Java, C, C++, javascript, Python, whatever. Concepts are the same across them all. We'll copy and paste some C++ from my latest project:

First I define a struct that holds a Point, an x,y value of numbers.

struct Point { long x; long y; };

Again, a cubic curve can be defined with FOUR of these Points. That is start, cp1, cp2 and end. Each point gets an x,y.

Then I define the curve as an actual class since I usually tuck some other functions in there for dealing with curves. Here's a simplified version of my Curve class:

class Curve {
  public:
    Point start;
    Point cp1;
    Point cp2;
    Point end; 
};

I use it like this:

Curve accelCurve;
accelCurve.start = {0.0};
accelCurve.cp1   = {180,40};
accelCurve.cp2   = {40,180};
accelCurve.end   =  {200,200};

Again, from your loop that does the run() for the steppers, you lookup the value for the speed, position or whatever the curve control. for example: My example code doesn't lookup the value directly, it computes the percent complete/remaining and then uses that. Since this example uses 5 seconds as the total time for the ramp up, you can use the elapsed time of the moved to figure out how far along into the ramp we are. at 2.5 seconds, the ramp would be 50% complete. In my loop that moves the steppers, I do something like:

long elapsedTime = 0;
long durationInMs = 5 * 1000 // ramp up speed is 5 seconds
long startTime = millis(); // millis() is specific to my MCU use whatever your platform has
while (millis() - startTime < durationInMs)  {
    elapsedTime = millis() - startTime;
    float percentComplete = (float)elapsedTime / (float)durationInMs; // your platform probably doesn't need the cast to float
    float percentRemaining = 1 - percentComplete; 
    float value = getPoint(percentRemaining, accelCurve.start, accelCurve.cp1, accelCurve.cp2, accelCurve.end);
   ... i then map the value returned using min/max speed of the stepper, set the speed, move the stepper, etc.
}

getPoint() defined as below:

float BZ1(float t) {  return t*t*t;  }
float BZ2(float t) { return 3*t*t*(1-t); }
float BZ3(float t) { return 3*t*(1-t)*(1-t); }
float BZ4(float t) { return (1-t)*(1-t)*(1-t); }
float getPoint(float percent, Point start, Point cp1, Point cp2, Point end) {
  BezierPointOne = start.x * BZ1(percent) + cp1.x * BZ2(percent) + cp2.x * BZ3(percent) + end.x * BZ4(percent);
  BezierPointTwo = start.y * BZ1(percent) + cp1.y * BZ2(percent) + cp2.y * BZ3(percent) + end.y * BZ4(percent);
  return BezierPointTwo;
}
You'll notice I only return BezeirPointTwo from getPoint(). For this example, I only care about the Y value (speed) at the given time.

That's it. That's how I use cubic curves to super smooth non-liner acceleration. Even on my modest ESP32 processor, I can get 20kHz of stepper pulses even with all the floats happening there.

Screenshot_2021-08-23_13.24.57.png

Aaron Newsome

unread,
Aug 23, 2021, 5:39:19 PM8/23/21
to accelstepper
Oh I should also mention that given my example explanation, I should have probably written getPoint(percentComplete ... rather than getPoint(percentRemaining. 

I get confused sometimes depending on if I'm doing a speed ramp UP or DOWN but also I work in grid systems that lower left is 0,0 and sometimes upper left is 0,0. Just verify your results to make sure you're using the correct form.

gjgsm...@gmail.com

unread,
Aug 24, 2021, 3:53:22 AM8/24/21
to accelstepper
Hey Aaron,

Thanks for the detailed explanation, very interesting. This is something I will be having a closer look at in the near future.

Cheers

Geoff

Aaron Newsome

unread,
Aug 24, 2021, 11:50:13 AM8/24/21
to accelstepper
Here's a more practical example of how I use cubic curves to control movement in my app. In the example above, I only discuss the RAMP UP to desired speed. In reality, you need to RAMP DOWN as well. To do that I flip the cubic curve and make it slope downward to the right. Also, in my current app, I base the RAMP down on number of steps to target. So just to be clear, I RAMP UP based on time and the defined curve and then use number of steps to target to determine what percentage of of the ramp down is complete.

I've also done projects where I ramp up/down based on time, rather than number of steps. I've also done projects where I base it on how many times I've executed the loop to move steppers.

Here's a screenshot of my app with the bezier curves for ramp up and ramp down. The config form stores the min,max and current speed for all 6 axis as well as a definition for the curves for ramp up ramp down. Actually, there's about 30 config items for each axis in the form including soft travel limits, steps per mm, etc. Saving the config form creates a JSON file on the SPIFFS file system on the ESP32. The ESP32 reads the JSON file on boot to configure all the stepper params for each motor. This form also allows reloading the config on the fly.
Screenshot_2021-08-24_08.39.15.png

gjgsm...@gmail.com

unread,
Aug 24, 2021, 7:12:01 PM8/24/21
to accels...@googlegroups.com

Does your app just graph the resulting curves or is it generating the numbers?

--
You received this message because you are subscribed to the Google Groups "accelstepper" group.
To unsubscribe from this group and stop receiving emails from it, send an email to accelstepper...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/accelstepper/6090a855-ca36-4679-9f22-d1355bb49ef1n%40googlegroups.com.

Aaron Newsome

unread,
Aug 24, 2021, 7:28:44 PM8/24/21
to accels...@googlegroups.com
The UI you see in the screenshot here has a save button. That save
button saves the state of those Bezier curves in the screenshot to a
JSON file. In the JSON file, the ramp up curve look something like
this:

{ "STARTX": 0,
"STARTY": 0,
"CP1X": 180,
"CP1Y": 40,
"CP2X": 40,
"CP2Y": 180,
"ENDX": 200,
"ENDY": 200
}

When the ESP32 boots, it reads the JSON file into a "Curve" object as
shown in the example C++ code in the first post. That code looks
something like this:

Curve accelCurve = readJSONFromFile("startRamp.json");

In my code I can now use accelCurve.start.x , accelCurve.start.y, etc
as arguments to getPoint() to control the acceleration of the stepper.
So to answer your question, no. My app is not graphing any numbers or
generating the points along the curve. What's happening can best be
described as a "lookup" inside the loop that moves the steppers.

In the loop that moves the stepper motors, I repeatedly call
"getPoint()" based on how far along in the stepper move the timestamp
is. getPoint(), as explained above, will return the Y point along the
curve that corresponds to how much time has elapsed since the move
started.

That's how you get non-linear acceleration, that follows the shape of
the cubic curve, defined by the FOUR X,Y pairs of points. Hopefully
that makes more sense to you.
> You received this message because you are subscribed to a topic in the Google Groups "accelstepper" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/accelstepper/t6ergGz30Lo/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to accelstepper...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/accelstepper/001401d7993d%246fc49d10%244f4dd730%24%40gmail.com.

Aaron Newsome

unread,
Aug 26, 2021, 10:06:22 PM8/26/21
to accelstepper
I created a quick 5 minute video sequence using cubic curves on the camera slider. Maybe this will make it a little clearer. At 00:29, you can see the Bezier curves in my app. You'll see start and stop ramp curves for all 6 stepper motors. When the forms is saved, those curve values are stored in a JSON file, which you see a few moments later when I click on File Manager. Again, the microcontroller reads the JSON file on boot and when it changes.


I should also point out that, while I do use cubic curves to control speed and sometimes acceleration, this video was not made with either. The cubic curves are controlling POSITION, not speed.

Also, if anyone is interested in the camera rig itself, I posted some pictures over at OpenBuilds:

Eddi Maevski

unread,
May 29, 2024, 3:13:41 AM5/29/24
to accelstepper
Aaron thanks a million for the detailed post and implementation skeleton.
Here is small excel I've used to get a feel of it. I needed a way to generate custom non linear velocity profiles which are continuous and smooth - your method nails it perfectly.

(For the ramp up Y - it seems I needed to use percent remaining per your convention)
Bezier Curve Cubic.xlsx
Reply all
Reply to author
Forward
0 new messages