The integral term
Removing the error that P leaves behind, and the windup that comes with it.
Do this lesson in the simulatorP responds to the error now. D responds to how it is changing. Neither has any memory, so neither can do anything about an error that is small, constant, and stubborn.
command = Kp * error + Ki * integral(error dt) + Kd * d(error)/dt
The integral adds up the error over time. A small error that refuses to go away accumulates, the I term grows, and eventually the command is large enough to move the robot. Then the error goes to zero, the integral stops growing, and it holds whatever value is needed to stay there.
That last sentence is the point: at steady state, the integral supplies the command and the proportional term supplies nothing. Which is what you want, because the proportional term can only supply something if the error is not zero.
In code
from bugbot import *
connect()
KP, KI = 1.6, 0.9
TARGET = 25.0
DT = 0.1
integral = 0.0
for tick in range(200):
error = distance() - TARGET
integral += error * DT
integral = max(-60, min(60, integral)) # clamped, see below
plot("error", error)
plot("i term", KI * integral)
drive(max(-60, min(60, KP * error + KI * integral)), 0, 0)
wait(DT)
stop()
print("ended", round(distance(), 1), "cm from the wall")
On the chart, the error comes down to a small offset and then, slowly, the last of it disappears as the I term climbs. P gets you close; I finishes the job.
Windup
The integral is a memory, and memories can be wrong.
Suppose the robot is asked for 25 cm from the wall, and something is in the way at 40 cm. The error stays at 15, the integral grows, and grows, and grows. Ten seconds later the obstacle is removed. The integral now holds an enormous number, the command is pinned at full speed, and the robot charges past the target and keeps going until the integral has been unwound by an equally large error in the other direction.
That is integral windup, and it produces the worst overshoot you will ever see from a controller that is otherwise correct.
Three defences, all standard, all one line:
- Clamp the integral to a sensible range, as above.
- Stop integrating while the output is saturated. If the actuator is already flat out, more integral cannot help, so do not accumulate it. This is called conditional integration, and it is the best of the three.
- Back-calculate: subtract the amount by which the command was clipped, scaled, from the integral.
U5.6 is that problem on its own.
Choosing Ki
Ki has units of percent per centimetre per second. A useful way to think about it: Kp / Ki is roughly the time the integral takes to contribute as much as the proportional term. Make it comparable with the settling time you want, and start with an integral that acts slower than the P term, not faster. Too much Ki is its own kind of oscillation: slow, large, and with a characteristic look of the robot sailing past the target and turning round.
Task: no offset left
Settle exactly 25 cm from the wall, within 3 cm, from fourteen seconds onwards. Proportional control alone will not do it, because this drive does nothing below about 15 percent. Plot error and i term.
from bugbot import *
connect()
KP, KI = 1.5, 0.8
TARGET = 25.0
DT = 0.1
integral = 0.0
Challenges
- Set
Ki = 0and report the offset the robot settles at. - Remove the clamp and hold the robot back by hand for a few seconds. Describe the result.
- Plot the P term and the I term separately and watch which one is carrying the command at the end.