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.