Raspberry PIO stepper library (pio_stepper_lib) is a C++ library that runs stepper motors in PIO state machines. It's intended to be easy to integrate and use in Pico projects.
In this post: create ramps when motor starts, stops or changes direction.
The stepper motor is controlled by a PIO state machine. The Pico ARM (or RISC) cores are free at that time. While PIO is handling a command, we can use the cores to calculate if the next command is part of a direction change (or stop). And then slow down the motor for a smooth switch. The ramp logic is part of your firmware. It's not a stepper library function. It shows how you can use the library in complex scenarios, and use the cores to calculate the ideal path while PIO looks after the motor. |
Ramp Logic
A stepper motor, when under load or at high speed, performs best if you ramp its stepping speed up and down. A motor will only be able to perform at its maximum, if you give it a little bump to reach. If you don't ramp, your motor may skip steps, overstep, or not step at all. Ramping up and down also makes movements less jerky.
When is a ramp advised?
- motor start
- motor changes direction
- motor stop
Because the stepper library can handle a batch of commands, we can look forward in the command stack to see if we are dealing with a direction change. And because the controller is doing nothing while a motor is running, we can do this exercise for the next command, while we're running the current command. (even if the motor has to do a single step, there's ample time to do this)
video: a linear stepper motor executing the example code on repeat. It ramps up and down at start, end, and at direction switching
Border conditions:
We can see inside a stack of commands if we are involved in a direction change, but we don't know how a previous batch ended, or how a new batch will start. That's why the logic has two parameters for first and last commands. If you pass true, the motor will ramp up / down. If you pass false, it will start or end at its high speed.
Software
This is the stack of commands that the motor has to perform:
std::array<stepper::command, 12> cmd{{ {10 * microstep_x, false}, {40 * microstep_x, false}, {10 * microstep_x, false}, {10 * microstep_x, true}, {100 * microstep_x, true}, {10 * microstep_x, true}, {10 * microstep_x, false}, {100 * microstep_x, false}, {10 * microstep_x, false}, {10 * microstep_x, true}, {40 * microstep_x, true}, {10 * microstep_x, true}}};
I've prepared them for this exercise. The firmware should run every first and last command in a particular direction, at the low (ramp) speed. Commands that are in between other commands in the same direction, should run at high speed. I wrote down the direction of each command, and tried to find a pattern that's easily translated into software.
- x: involved in direction change (previous, next, or both different direction
- blank: same direction as previous and next
- !: start or stop: we do what the programmer asks us
I chose this algorithm: If the flags of the before, current and after direction are added up for the next command, and the sum is 0 or 3, there is no direction change. Else there is.
Translated into code:
void run_with_ramp(const commands_t & cmd, uint32_t slow_delay, uint32_t fast_delay, bool start_slow, bool end_slow) { bool ramp = start_slow; bool next_ramp_found = false; unsigned int bits = 0U; for (size_t i = 0U; i < cmd.size(); i++) { motor1.set_delay(ramp ? slow_delay : fast_delay); motor1.take_steps(cmd[i]); while (motor1.commands() < 1) { // motor has to complete before we can send another delay command // use motor run time to calculate next speed if (! next_ramp_found) { ramp = end_slow; // by default, think it's the last command if (i < cmd.size() - 2) { // if not, find if dir is changed or will change // check if next command has different direction bits = (cmd[i] & 1) + (cmd[i + 1] & 1) + (cmd[i + 2] & 1); // compare dir bits if (! (bits % 3)) { // in a dir change cycle, unless prev, cur and next command identical ramp = false; // go fast } } next_ramp_found = true; } } next_ramp_found = false; motor1.reset_commands(); } }
When you look at the code, take in mind that it always tries to figure out if the next command needs to be ramped.
In that context, cmd[i] (the one that's currently being executed by PIO) is the previous step, cmd[i + 1] is the one we are investigating, and cmd[i + 2] is going to be the step after that.
To run the batch of commands:
run_with_ramp(cmd, 7000, 4300, true, true);
When you add this line in the loop, when the evaluation is done:
printf("next step: %d, checksum: %d, ramp: %s\n", i + 1, bits, bits %3 ? "yes" : "no");
You get this output (step 0 and 11 are determined by the start_slow and end_slow parameters. They aren't shown below):
delays: slow = 7000, fast = 4300
next step: 1, checksum: 3, ramp: no
next step: 2, checksum: 2, ramp: yes
next step: 3, checksum: 1, ramp: yes
next step: 4, checksum: 0, ramp: no
next step: 5, checksum: 1, ramp: yes
next step: 6, checksum: 2, ramp: yes
next step: 7, checksum: 3, ramp: no
next step: 8, checksum: 2, ramp: yes
next step: 9, checksum: 1, ramp: yes
next step: 10, checksum: 0, ramp: no
The oscilloscope capture of DIR (yellow) and STEP (blue) shows the behaviour at a direction change:
the black line before the last quarter of the image above is a 1ms pause I intentionally put there in my test code, to get a hint when a batch starts
zoom in to a direction change with slow-down before and after. The firmware defines how many steps both part of the ramp last.
This example uses a rough ramp-up: slow speed and high speed. You can define how many slow steps it takes. In many cases that's enough to get the motor going. But there's enough core time available to construct more advanced up- and down-ramps. You could even write an algorithm that pre-analyses a set of commands, and compiles them into a new set with ideal (for your scenario) ramp patterns.
CMake project, with precompiled .uf2 file: pio_a4988_stepper_ramp_20250511.zip
Top Comments