Hi Everyone,
So I am making this post mostly to talk myself (and others) through some common problems with precision timing. I do have an unsolved problem that I could use some help with, but to start off with I will discuss how to get extremely accurate timing from your Arduino. In theory.
I am working on a precision stepper motor library, using a STEP/DIR based motor driver. Microstepping at high speeds requires that STEP pin be toggled fairly rapidly and fairly accurately if you want to do things like precisely move objects with sub-mm accuracy and with accurate speeds.
For those who just want to learn from this post, when I say "motor", just read that as "something that I need to turn on and off with high accuracy".
There are two main problems to overcome here. The biggest and baddest is timing drift. Here's some code that causes it and also happens to be an amazingly common technique. Note, that lastStepTime and stepInterval (member variables of a class) are also unsigned longs (uint32_t).
// this causes timing error!
uint32_t curTime = micros()
if ((curTime - lastStepTime) >= stepInterval)
{
// do stuff to step the motor
lastStepTime = curTime; // this is the line of code that causes the problem
}
The micros() function is accurate, but it's not the problem. We're all doing other stuff in our sketches, so it's not like we have bare-bones loop() routines that can just check micros() every single microsecond to determine if it's time to do something. Instead, micros() is more likely to be sampled much less frequently, maybe once every 10 microseconds. So you might toggle a pin a few microseconds late, who cares? Well, if you look at the code above, you're not just going to toggle the pin late that one time. You're baking the error into lastStepTime and next time around it will be even worse. It will continue to get worse with every iteration. This is cumulative error, and in the real world it can get bad. It's not just something for nitpickers to worry about! Here's what it looks like when microstepping (1/8 steps) a stepper motor between 50 and 400 steps per second:
Okay, so the motor is running more slowly that desired. And it's bad - up to 10 steps/second off at higher speeds. You would not want to make a 3d printer with this type of problem.
The fix is, thankfully, amazingly easy.
// this is the correct way!
uint32_t curTime = micros();
if ((curTime - lastStepTime) >= stepInterval)
{
// do stuff to step the motor
lastStepTime += stepInterval; // all better now!
}
The caveat is that you need to be careful that you set lastStepTime = micros() at the start of a sequence of steps. If you let it lag behind, you're going to get very unwelcome high-speed pulses. For a stepper motor, it will simply stall and your position index will become completely off. For other devices, worse might happen. So take care, but it's an easy thing to do.
Here's what it looks like when we correct for the timing drift (and where a few folks might notice my pesky problem):
It's much better but now! Our worst-case error went down from being 10 steps/second off to being 1.7 steps/second off.
But we still have error and its appearance tells us something about why. We have the motor tending to always be faster than expected. The reason for that lies in integer math and the rounding error that results. When you calculate your timing interval, whatever digits might be after the decimal point just get thrown away. If stepInterval should be 400.9, you're going to get 400 from integer math. This rounding error will always err on the side of making things just a tad too fast and it will be more obvious at higher speeds. But the solution is NOT to use floats. They are slow.
Here's how my calculation of stepInterval looks:
uint32_t numer = 1000000UL;
uint32_t denom = (uint32_t)stepsPerSec << driveMode;
uint32_t stepInterval = numer / denom; // here comes the rounding error!
Absolutely slaying the rounding error is fairly easy. When you calculate stepInterval, determine the remainder (using modulus) of the division operation. And use that remainder to determine what proportion of steps should actually be delayed by 1 us. For example, if stepInterval should be 400.9 in a perfect world, then in the integer world we delay our steps by 1 us 90% of the time. On average, then, we get 400.9 even though we're using integer math.
Here's the code for how to do that when calculating stepInterval:
uint32_t numer = 1000000UL;
uint32_t denom = (uint32_t)stepsPerSec << driveMode;
uint32_t stepInterval = numer / denom;
uint8_t speedAdjustProbability = ((numer % denom) * 255) / denom;
And the code for putting that to use:
curTime = micros();
if ((curTime - lastStepTime) >= stepInterval)
{
// do stuff to step the motor
lastStepTime += stepInterval;
// increment lastStepTime if needed to fix rounding error
// Note: speedAdjustCounter is a byte (uint8_t)
if (speedAdjustCounter++ < speedAdjustProbability) lastStepTime++;
}
Nice and simple. Here is that final result of all these corrections:
And now our worst case error is 0.5 steps/second. Perhaps that is "problem solved" in many cases, but not in my case.
There is still cumulative error! And there shouldn't be. I cannot figure out why the error is rising with speed. I have tried everything I can think of to get rid of it. Is it caused by micros() itself? When running, micros() disables interrupts very briefly... and my code is efficient enough that this happens very frequently. Could calling micros() every 9 us or so actually cause timing problems by frequently disabling interrupts (including timer overflow interrupts)?
It's not the crystal, because I'm using the Arduino to diagnose itself. Even if the crystal were way off, it wouldn't notice.
Here's where it gets stranger still. I've timed the frequency of step pulses with my oscilloscope (Siglent SDS 1052DL) and it is reading accurate step timing, assuming I'm using it correctly. But I doubt myself just enough... could be code I'm using to measure my timing be at fault? Here it is:
I'm aware it probably doesn't make perfect sense without the rest of the sketch it is part of. If needed I can post the whole thing... but as an attachment no doubt.
void measureSpeed(uint32_t overDist)
{
int32_t startPos = mot.getPos();
int32_t targetPos = mot.getTarget();
int32_t testEndPos = (targetPos > startPos) ? (startPos + overDist) : (startPos - overDist);
uint32_t startTime, endTime;
uint16_t startSpeed = mot.getCurSpeed();
startTime = micros();
while (mot.work())
{
if ((targetPos > startPos) && (mot.getPos() >= testEndPos)) break;
else if ((targetPos < startPos) && (mot.getPos() <= testEndPos)) break;
}
endTime = micros();
uint16_t endSpeed = mot.getCurSpeed();
int32_t endPos = mot.getPos();
// calculate speed
int32_t dist = endPos - startPos; // microsteps
uint32_t time = endTime - startTime; // us
float measuredSpeed = (1000000.0 * dist) / (time * mot.fullStepVal);
Serial.print(String(measuredSpeed));
Serial.print(F(" st/s avg ("));
if (startSpeed == endSpeed)
Serial.print(String(startSpeed));
else
{
Serial.print(String(startSpeed));
Serial.print(F(" - "));
Serial.print(String(endSpeed));
}
Serial.println(F(" st/s)"));
}
It's not pretty but it's a testing sketch, I haven't quite taken the time to make it as elegant as it could be.
Anything stand out there as a problem? This is driving me nuts. There absolutely should be a bit of error, but it should oscillate around 0. Not have a tendency to rise.