July/August: Direct Drive Transport (part 2)
It took a few weeks of trial and error to come up with the following, but it was a lot of fun. You can see half a dozen blind-alleys, over-complications, and wrong answers in my notes before it all shook out into the simple form in the rest of this post.
Interleaved Steps
Like I mentioned last time, I’ve been trying to write my Arduino code with an emphasis on being able to read it straight through so I won’t have to do so much tricky reasoning about what state things are in or which interrupt should be firing next, etc. So, when it came time to step two motors simultaneously at different rates, rather than attempt to do it with PWM units in hardware (where I’m less confident about stopping them exactly at the right step count), I wondered if there might not be an easy way to do it manually.
I’ve got all the stepper motor’s “step” pins connected to the same AVR “port”, so they can be updated simultaneously just by setting a single 8-bit value in the code. Which motor(s) get stepped depends on which bits are set when you change the value. You end up with something like this:
PORTF = mask;
delayMicroseconds(d);
PORTF = 0;
delayMicroseconds(d);
Whichever motors are selected in your bitmask will get stepped, the rest will sit still. You can do this in a loop or with some other timing mechanism to handle acceleration and deceleration, but it always comes back to the decision of which motors to step and when.
So, starting with an example where we’ve determined we need to advance the supply reel 9 steps but we only need to advance the take-up reel 3 steps (say, to decrease our film tension a bit while nudging the center of the frame a little), you could imagine we could choose our bitmask for each successive step like this:
The motor with the larger number always gets stepped every cycle. And, ideally, you find the right spacing for the motor that needs to move fewer steps so that the move is as smooth as possible.
At first, I was worried that writing the code to do that would be full of special cases, keeping track of different sub-patterns, worrying about round-off, and all sorts of other details. I mean, when you look at the rest of the ways to fit every combination of fewer pulses into the same 9 steps, it seems like the code to generate all these (nice, symmetrical) patterns would be tricky:
And that’s to say nothing of the real world cases where you might be moving hundreds of steps at a time. (Every fifteenth pair, there is a group of three steps on the 242 line in this example):
While doodling these step patterns in my notes, I eventually noticed some similarities to modular arithmetic. Adding another multiple of the smaller number each cycle, dividing it by the larger, and producing a step anytime the rounded result increased by one started to produce these lovely shapes without any regard to keeping track of… anything else.
It was an easy answer that worked with floating point math and round-off, but the Arduino is an 8-bit processor that doesn’t like doing floating point math if it can help it (and you always have to worry about numerical stability with those sorts of solutions). So, I played with the idea a little more until I found an all-small-integer solution that ended up even simpler.
Here it is, for posterity:
unsigned short s = floor(larger / 2)
for 1..larger
s += smaller
if s >= larger
step both motors
s -= larger
else
step larger motor
That’s it. One addition, one comparison, and maybe one subtraction per cycle. For any sensible number of steps, all the math fits into 16-bit numbers, which avoids generating an onerous number of AVR instructions. And there are never any large numbers that might overflow.
So now we can make arbitrary simultaneous interleaved moves. Now the question is how many steps should we be moving?
Maintaining Tension While Moving
Advancing the supply reel reduces tension. Advancing the take-up reel increases tension. After moving S supply steps and T take-up steps we get some change in tension, Δ𝜏.
To relate these, let’s introduce constants a and b with units of “change in tension per step”, which lets us write:
Really,
a and
b vary slowly over the course of the entire reel, but are relatively constant across many successive frames. While advancing,
a is negative and
b is positive. (Again, supplying film reduces the tension and taking it up increases it.)
If we can find good (current) values for a and b, we should be able to move reliably while maintaining (or deliberately changing) the tension on the film. One solution might be to stop scanning every once in a while to do a recalibration: make a small move or two on both reels, one after the other, measure the tension change between moves, finding a and b directly. But, short moves are more susceptible to all the sources of noise.
The good news is that there is an easy way to get much better accuracy without even interrupting the film scanning.
Every time we move, we get another set { S,T, Δ𝜏 } which, ideally, we could use to help narrow down a and b. Because this is a noisy dataset, there will never be an exact solution. Instead, we estimate them by finding a least squares fit across all our recent measurements.
(Geometrically, this boils down to finding the best-fitting plane in three dimensions, with one point per recorded motor move. Each point’s distance from the plane is an “error” term. The error is squared to–among other things–always make it positive. Then, the algorithm finds the best plane that minimizes the combined squared error of all the points.)
Least squares fitting is a built-in operation in OpenCV, so long as we can get our problem into the usual Ax=b form:
The only mildly interesting part here from a code standpoint is interleaving the
S’s and
T’s in an OpenCV Mat with two columns. Once you’ve got everything stored in the correct size
cv::Mat
, you just call:
cv::solve(A, b, x, cv::DECOMP_SVD);
Then, read the a and b coefficients straight out of the x
Mat.
Despite a lot of noise in the measurements from all the non-linearities and imperfections in the ReelSlow8’s construction, the best fit against the previous dozen or so moves produces remarkably stable estimates for a and b.
With those in hand, we can now make the next move. We start with however many desired supply reel steps S (say, our guess for what it’ll take to reach the next frame), the current tension 𝜏0, and the desired tension after the move completes 𝜏1. Using the latter two, we can find the desired change in tension:
In the ideal case (where a move always lands exactly at the same tension as before), this would always be zero. But, in the presence of system noise and integer stepping, there are usually a few grams of under- or over-shoot that we also need to correct for.
Now we can solve equation (1) for T and find the number of take-up reel steps required to move how far we want while maintaining the tension we want:
In practice this works well over distances up to whole frames with the tension barely fluctuating over the course of the move!
That said, there is a danger: the estimates are so stable and the results so consistent, that it’s important not to make the same move over and over (say, moving exactly the length of one frame).
An example is illustrative: if it takes 750 supply reel steps and 700 take-up reel steps (in this particular area of the reel) to advance one frame without the tension changing (besides the usual noise sources), once your history buffer wraps around, you end up trying to solve:
Even though you have many equations and only two unknowns, the redundancy between them leads to a degenerate (or singular) matrix. All of those points that are being fit to a plane fall on the same line, which means many different planes could intersect the line equally well.
In this case, the coefficients could be -5.5 and 5.9. Or they could be -11.0 and 11.8. Or any other multiple. There is no way to tell! The coefficient estimates can become suddenly unstable once the initial “bad” points have rolled off the end of the list of historical points.
The solution is easy: just vary something. Instead of moving directly to the next frame, get there in two moves and vary the desired tension between them. If your working tension for image capture is 190g, have the intermediate move target somewhere around 140g, just to get a little “contrast” in the readings. Just about anything different will keep that best-fit plane from spinning around the degenerate line.
Once you take that caveat into account, the estimates provided by this model land the moves (at 1/32nd micro-stepping, directly driving 3" reels) within a few grams of the target tension after any number of steps you like, every time.
The best part is that it’s completely adaptive: over the course of the reel, the coefficients will slowly shift as the amount of film changes on each. The only real question is how large should the history buffer be? Too short (3 or 4 samples?) and things will be more susceptible to noise. Too long (60 samples?) and it might adapt slower than the coefficients are actually changing. A dozen or two movement history data points has worked well.
One last detail: there is a bit of a chicken-and-the-egg problem with the coefficients. How do you start gathering move data without knowing how far it’s safe to move? The easiest answer is to just use the weaker direct measuring scheme mentioned above, once, at the start. While longer moves provide more steps to average the tension changes across, it only takes a couple very small moves to get a reasonable basis to start the least-squares operation from. I have my app do an { S=5, T=0 } move followed by an { S=0, T=5 } move (disregarding any desired tension, just recording what happened after each). Because those are orthogonal as far as the solutions to the linear equation are concerned, it has worked as a good starting point. From there I use the estimates to walk a dozen supply reel steps, then fifty, and by that point the least-squares solution has locked on pretty closely and you can move however you like without fear.
What’s Next
With good sprocket hole estimates and the ability to move smoothly while maintaining tension, the last step before this machine could be said to actually do something useful is figuring out the number of steps to center the next frame in the camera viewport.
My guess is that an adaptive, best-fit scheme will work for that, too! We’ll find out soon.