Tuning the gain
Sluggish, good, oscillating. Loop speed and driving speed.
Do this lesson in the simulatorPrint it: worksheet (PDF) · answers (PDF) · as a web page
Guide: PID control explained
The gain is one number, and it decides whether a controller is sluggish, good, or shaking itself to pieces. Engineers spend real time on it. In this lesson you measure what it does.
Two numbers describe a run: the worst error seen, and how many times the error changed sign. A sign change means the robot crossed the target and went the other way. A good controller crosses once or not at all.
Too little
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
worst, flips, last = 0, 0, 0
while position()[1] < 60:
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# gain 1
drive(70, 0, error * 1)
# pause 0.1 s (the robot keeps doing what it was told)
wait(0.1)
# all motors off
stop()
# where am I? (cm from where I started)
x, y = position()
print(f"gain 1: worst error {round(worst)} deg, {flips} sign changes, ended at x={x}")
With a gain of 1 the correction is too weak to hold the curving robot: the error grows to 15 degrees before the controller matches it, and the robot ends nearly 10 cm off the line. It never crosses the target at all. A controller that is too weak is called sluggish.
Too much
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
worst, flips, last = 0, 0, 0
while position()[1] < 60:
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# gain 40
drive(70, 0, error * 40)
# pause 0.1 s (the robot keeps doing what it was told)
wait(0.1)
# all motors off
stop()
# where am I? (cm from where I started)
x, y = position()
print(f"gain 40: worst error {round(worst)} deg, {flips} sign changes, ended at x={x}")
Watch the robot: it snakes. A tiny error becomes a huge rotation, which overshoots, which becomes a huge rotation the other way, over a dozen times in four seconds. That is oscillation, and in a real machine it wears parts out. The rotation is also capped at 100, so at gain 40 anything over 2.5 degrees is just bang-bang again.
Why the loop speed matters
Between one look and the next, the robot keeps doing whatever you last said. A high gain with a slow loop is the worst combination: a big correction that runs for too long.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
worst, flips, last = 0, 0, 0
while position()[1] < 60:
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(70, 0, error * 40)
# a slow loop
wait(0.3)
# all motors off
stop()
print(f"gain 40, slow loop: worst error {round(worst)} deg, {flips} sign changes")
The worst error is now 22 degrees: worse than the sluggish gain of 1 (15), and not far off no controller at all (26 degrees in lesson 3.2). On a real robot the loop runs over a radio link, so the delay is never zero. That alone limits how high a gain can go.
Measure the sweep
The sweep takes 14 seconds, longer than the fast task allows, so this cell runs on the lesson 3.2 mat. Same robot, same fault.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
speed = 70
for gain in [1, 2, 4, 8, 16, 32]:
# this way is now heading 0
reset_heading()
worst, flips, last = 0, 0, 0
# two seconds of driving for each gain
for tick in range(20):
error = wrapped(0 - heading())
worst = max(worst, abs(error))
if error * last < 0:
flips += 1
last = error
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(speed, 0, error * gain)
# pause 0.1 s (the robot keeps doing what it was told)
wait(0.1)
# all motors off
stop()
# pause 0.3 s (the robot keeps doing what it was told)
wait(0.3)
# the next run goes back the way this one came, so the robot stays on the mat
speed = -speed
print(f"gain {gain}: worst error {round(worst, 1)} deg, {flips} sign changes")
The worst error falls as the gain rises, from 12.4 degrees at gain 1 to 1.3 at gain 16, with no sign changes. At gain 32 the sign changes jump to 5. The good gain is just before that: about 16 here.
Speed and gain together
Faster driving means the fault acts faster too: this robot turns about twice as hard at speed 100 as at 50. A gain that is fine at 50 may be sluggish at 100, and a loop that was fast enough may not be.
Task: hold the heading, fast
The zone is narrower and you have six seconds. The starter's gain of 40 wobbles and wastes time, and its speed is too low. Tune both.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def wrapped(h):
return (h + 180) % 360 - 180
while position()[1] < 72:
error = wrapped(0 - heading())
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(60, 0, error * 40)
# pause 0.05 s (the robot keeps doing what it was told)
wait(0.05)
# all motors off
stop()
Challenges
- Find the largest gain that gives at most one sign change at speed 100.
- Make the gain depend on the speed:
gain = 4 * speed / 70. - Log the error every tick into a list and print the list at the end. Where does it peak?