Next, PID controller. I doubt you’ll really want to go here, unless you like to fiddle. If you like to fiddle, it’s a fiddler’s dream, if you can set up the real-time programming environment to where you can run the algorithm. (That’s usually the hard part, but no doubt today there are systems set up to make this easy.)
First, the setup:
The goal is to move an actuator to a desired position. You have one input, which is the current location. Ideally, this is linear – that is, it’s in inches or centimeters etc. If it’s not, the controller can still work sometimes, but less optimally. In any case, this is not an optimal controller, it’s just a simple, effective, and fairly robust one.
You also have an output that controls a force. Again, ideally the force is proportional to the output value. (One way to do that, closely enough, is a duty-cycle controller, which would be ‘downstream’ of the PID’s output and feeding the actuator motor’s control circuit. It’s beyond the scope of the course today.)
The setup requires (like the LP filter above) that you have a real-time OS that allows you to schedule a function to get run periodically (say, 1000 times a second, or even just 100 times a second).
The motor controls the position of the actuator, but acts against some force, which varies. A PID controller works well as long as the “counter force” is not strongly periodic. If it is strongly periodic, you can work around that by tweaking the sample/control period and the three tweak constants which I’ll describe below. But for any apparently well-tweaked controller, there are periodic counterforces that’ll totally mess it up. With a strong and fast enough motor, though, it shouldn’t be a serious problem.
Theory: You need to output a force that matches the current counter-force, AND puts you at the right location. We arrive at this force by applying three component forces, each for a different purpose.
First, if you’re below the target, you need to increase the force. If you’re above it, decrease the force. To keep it simple and also get where you want to be as fast as possible, just apply a force in the correct direction that’s PROPORTIONAL to the distance you need to go:
// current is where we are; target is where we want to be
output_force = K1 * (target - current);
Simple enough, right? If there were no force and the actuator were massless, eventually target and current would be zero, and we’d be right at the sweet spot! K1 is just a tweak constant. The higher it is, the faster we get where we’re going (up to the limits of the actuator motor.) No, infinity is not best.
Think of K1 as “gain”.
But remember there’s some unknown force pushing against us (which could be going in either direction, pushing down or up … no matter!) Let’s say that force is pushing down and it’s constant. We’d stabilize where K1 * (target - current) matches the external force. To correct for this, we add an INTEGRAL term:
accumulator = accumulator + (target - current);
output_force = output_force + K2 * accumulator;
As with “last_val” above, “accumulator” has to be a value that is retained between different calls to the function. Every time the function is called, if we’re below the target, the accumulator value grows, increasing the force we apply. Eventually, this force matches the external force, and the actuator is in the target location.
So, we’re done, right? Oops, not quite. One problem with the above is that when we hit the target, the accumulator is nonzero, so we overshoot. Then it starts to grow negative, and we move down, and hit the target, but again the accumulator is nonzero (negative) so we overshoot. That is, we oscillate. We’d also oscillate if the actuator has significant inertia (e.g., mass).
To fix that, we add a DIFFERENTIAL component, providing damping:
diff = current_loc - last_loc;
output_force = output_force - K3 * diff;
last_loc = current_loc;
Again, “last_loc” is a variable that keeps its value between function calls. K3 is the “damping factor”. I forget the usual name for K2, but it’s essentially the “external force compensation” factor.
Notice that this time, we’re subtracting the term. This slows down the movement: the faster we’re going up, the less force we want to apply. (Likewise, the faster we’re going down, the more force we want to apply.)
Digital friction. Gotta love it. And that’s it! We’re done!
Here’s the lot, arranged a bit differently, in a function intended to be called periodically to compute the output force. As before, I’m using floats, even though inputs and outputs are usually integers. Using integers, you have to take care to avoid losing precision in intermediates, though you can often just gloss over that, since this controller is more like a Ford Model A engine than a Ferrari.
float last_loc;
float accumulator;
float K1, K2, K3; // set these tweak constants from outside: set-and-forget
// called periodically. Returns the force for the actuator motor
float pid(float target_loc, float current_loc)
{
float p, i, d;
float discrepancy = target_loc - current_loc;
float delta_loc = current_loc - last_loc;
accumulator = accumulator + discrepancy;
last_loc = current_loc;
p = K1 * discrepancy;
i = K2 * accumulator;
d = K3 * delta;
return (p + i - d);
}
Note that I’d use “static” for last_loc, etc., but I’m trying to avoid anything that would complicate translating to another language for someone who’s not C-literate.
Someone who’s a better engineer than I am could probably describe the tweak constants in engineering terms, with units, conversions, etc., and based on the polling period. I just fiddle them until they work well.
IIRC, good starting values are 1.0, 0.1, and 0.1, respectively.
If you try to implement this with fixed point, you need to rescale some intermediates (especially the accumulator calculation) to avoid losing precision. Unless you have a good reason to need to use integers, or you’re familiar with fixed-point coding, I recommend sticking with floating point. In the bad ol’ days we needed integer math to get the sample rates high (and our processors didn’t have FPUs.)