Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
My bot won't drive straight! Such is a common problem in robotics. Unfortunately, two
motors will never be identical, two surfaces will never have the same coefficient of friction,
and two wheels will never be exactly the same diameter. So what is a roboticist to do? We
need to incorporate a form of feedback into our motor control. Encoders are one possible
solution to the problem of a bot that won't drive straight, as well as these other problems:
// this code will print the # of encoder ticks each 1/2 second
volatile int counter = 0;
void setup(){
attachInterrupt(0, count, RISING);
serial.begin(19200);
}
void loop(){
counter=0;
delay(500);
serial.print(counter);
serial.print("\n");
}
void count(){
counter++;
}
and B is logic high, and B was logic high last time, we've gone clock-wise, but
we've actually gone through a full wave cycle of the A channel. This is actually
twice as much travel as if we had reversed direction.
and B is logic high, but B was logic low last time, we've still gone clock-wise, but
we've reversed direction, and thus gone only 1 click in distance.
and B is logic low, we've gone counter-clockwise. Based on the previous B value,
we can decide if we've gone 1 or 2 steps.
Using this, we've doubled our resolution. As they say on TV though, but wait, there's more.
If we interrupt on both the rising and falling edges of A, and do a similar decoding, we can
double our resolution again.
Of course, since I'm using these Nubotics sensors, it's somewhat academic to decode the
A/B signals, since these awesome encoders provide an alternative Clock + Direction output
that is already decoded as described above. But, we should at least discuss this, since not all
encoders are quite as nice.
Figure 1 - Quadrature Encoder output and truth table. A quadrature encoder has two channels, A and B,
which are some distance out of sync with each other. If we have an interrupt routine that runs on the rising
edge of A, we can determine the direction of rotation by reading B. When our encoder turns in one direction,
the interrupt will be triggered on the red dots, times at which the B channel is low. If our encoder turns in the
opposite direction, it will trigger our interrupt on the green dots, when B is high.
Figure 2 - A typical diagram of a feedback loop. Our set point and feedback go into a summation node, the
output of which is our error, and is sent to our controller, which produces an output.
When you drive down the road, the speed limit sign is your set point. You are a controller
that adjusts the car's speed, based on the actual value you read on your speedometer. If your
car has cruise control -- you've already used a closed-loop feedback device to regulate the
speed of a motor!
There are of course caveats with this. We have several things that may arise that can be
detrimental or downright destructive to our system. First and foremost, a poorly configured
feedback loop can easily go into oscillation, if it is really bad the oscillation could build to
infinity. It will be seen that this type of situation often happens if we try to adjust too
quickly. On the other hand, if we adjust too slowly, we will probably never get our actual
value to equal our set point value. There are also a multitude of physical issues that will
plague feedback loops. Motors don't typically have linear speed control, especially on their
low-end speeds. Friction on wheels, inclines, and other properties of the environment cause
spurious changes in the actual value -- these spurious changes can also set off oscillation in
the controller. For all of these reasons, creating the function or controller can be difficult,
time-consuming, and involve quite a bit of tuning.
Figure 3 - Diagrams of: (A) overshoot and oscillation, (B) undershoot and residual offset, (C) near-perfect
convergence.
Proportional term: this term adds a proportion of our error to the output, it is
typically what a novice would use for their entire feedback function. Typically just
Kp * error.
Integral term: this is used to adjust out any residual offset we might have. If our
controller typically approaches the value we want, but typically never actually
makes it all the way to the value, an integral term may be used. Many controllers are
actually just P-D controllers, they omit an Integral term. Typically just Ki *
sum(errors).
We will examine the control of a motor's speed, however the same process can be applied
to anything really: position control, temperature of an oven, etc. When using an encoder, we
have to count very delicate and often high-speed pulses, and thus we really need a
dedicated controller that is running in real-time. This is best achieved using a
microcontroller with interrupts to count pulses, and some hardware timer to set your time
intervals if doing speed control. Our example will use an Arduino, due to it's low cost, and
easy availability.
For this example, I am using an Arduino-based robot, the controller is actually an AVRRA
Mini board configured as an Arduino. I'm using GM8 motors and the associated wheels,
powered by an SN754410 motor driver. My encoders are the Nubotics ones designed
specifically for these motors. The encoders need just a simple 5V supply, and have both an
A/B style output or a clock+direction style output. They give 128 counts per rotation when
using the clock+direction output.
// motor speeds
int lSpeed;
int rSpeed;
// PID tuning parameters
int kP;
int trial = 0;
// enable our interrupts
void setup(){
Serial.begin(38400);
// Encoder clock output is tied to Digital2, call ISR function named
left.
attachInterrupt(0, left, RISING);
lSet = 20;
// we'll try to go about 50rpm
last = millis();
lSpeed = 0;
kP = 20;
// trial 0 - kP too low, residual offset
}
// main loop
void loop(){
while(millis() < last + FRAME_LEN);
// find error, and then reset counter...
int error = lSet - lCount;
lCount = 0;
last = millis();
// do our update
int nextSpeed = (kP * error)/100 + lSpeed;
drive.set(nextSpeed,0);
lSpeed = nextSpeed;
// output some data
Serial.print(nextSpeed);
Serial.print(",");
Serial.println(error);