[!NOTE] This is mainly out of date now, and describes a pretty old snapshot of smoothie. left here for historical reasons.
This page is intended as a description of the current status of acceleration and stepping in Smoothie, and then a log of the process of moving to better acceleration and better ( maybe ) stepping.
These are notes to myself, please be nice :)
Here is the plan:
A summary of how we get smoothie to generate steps:
This is the core of what smoothie does.
Additionally, a painful thing we have to do, is to update the speed very often, to be able to accelerate, and decelerate.
This happens in a separate (lower priority) timer, at regular, but more rare intervals.
This behavior is inherited from grbl and is the source of many approximations/small problems.
You can end up with deceleration curves reaching zero, or near-zero speed a few steps too early, and instead of a direction change, you get a pause. This currently (Feb 2013) can lead, at high speeds, to «shocky» end of moves, and even missed steps.
Step generation is what Smoothie spends most of its time doing.
The reason for this is simple:
Basically, we want this to take as little time as possible in order to do it as often as possible.
In order to analyze how much various things we do take to execute, their cost, we can simply turn pins high at the beginning of things, low at the end of them, plug in a Logic Analyzer, and look at the graphs.
This has been extremely useful in the past to figure out where we spent too much time, what needed fixing, etc.
The width of the screen should represent 10 microseconds.
So, what is important to see here?
Now the very interesting stuff.
We are here to hunt waste. We want this to run as fast as possible. And looking at the curves, we clearly have a problem (not sure it has a solution).
The orange line is the time we spend doing useful work. The work we are here to do.
The brown line is the time it actually takes us, total, to do that work. As you can see, we spend much much time doing stuff that is not directly useful (though it may be necessary). That would be iterating over the active StepperMotors list, calling methods, etc… maintenance if you like.
We want that to last as little time as possible!
There are two situations here:
The additional duration of the second one compared to the first is due to the fact that we have a second timer interrupt, with a higher priority, to turn the step pin low after one microsecond. This interrupts this interrupt to turn the pin off, making this interrupt last longer. I think that’s the main cause of the longer execution time.
Now when we do not output a step signal, the total interrupt time is still much more than the useful (orange) time.
It should probably be interesting to look at the assembly to see how long we spend doing each thing.
This is schematic C++ representing what happens in your typical, short step interrupt.
// This is where work is done
extern "C" void TIMER0_IRQHandler (void){
// If no axes enabled, just ignore for now. This costs us a tiny bit of time
if( global_step_ticker->active_motor_bm == 0 ){ return; }
// We set the timer to a very high value so we don't overflow the timer if this takes too long
LPC_TIM0->MR0 = 2000000;
// This calls the tick method.
global_step_ticker->tick();
// Let's inline it below:
_isr_context = true;
int i;
uint32_t bm;
// This is your usual loop. I have no idea how costly it is. It seems like it is costly as we don't do much else.
for (i = 0, bm = 1; i < 12; i++, bm <<= 1){
if (this->active_motor_bm & bm){
// We call the tick() method for each StepperMotor.
this->active_motors[i]->tick();
// Let's inline this below:
void StepperMotor::tick(){
// Increase the (fixed point) counter by one tick
this->fx_counter += (uint64_t)((uint64_t)1<<32);
// If we are to step now.
if( this->fx_counter >= this->fx_ticks_per_step ){
// Here we don't care about the case where we do. We care about the time we waste before and after this.
}
}
}
}
// The iteration over the active StepperMotors is now finished, all useful work is done
_isr_context = false;
// Return to the main interrupt function
// If we did set a pin high, we want to set the other timer to set it low one microsecond from now
if( global_step_ticker->reset_step_pins ){
// But we don't care about the case where we did here. Still the check is expensive
LPC_TIM1->TCR = 3;
LPC_TIM1->TCR = 1;
global_step_ticker->reset_step_pins = false;
}
// If a move finished in this tick, we have to tell the actuator to act accordingly. This is not happening here either as we did not generate a step.
if( global_step_ticker->moves_finished ){ global_step_ticker->signal_moves_finished(); }
// If we spent too much time inside the interrupt. This should probably never happen if the previous condition was not true.
if( LPC_TIM0->TC > global_step_ticker->period ){
// We don't care, not happening
}
// This is just a security to make sure we never miss our match register
while( LPC_TIM0->TC > LPC_TIM0->MR0 ){
LPC_TIM0->MR0 += global_step_ticker->period;
}
}
// And that's it
Several optimizations found and applied:
These are optimizations that are most useful in the case we don’t do anything. Applying them gives us the following signals/durations:
Compared to the previous graph, we now spend significantly less time in the interrupt when no step is generated, and a bit less time when one or more steps are generated.