PID Controllers 101

My first introduction to PID controllers was in an article about a robotics dead-reckoning competition which the author won using a Lego Mindstorms creation called Peeves.

Image68

Peeves 2001

This was back in 2001. It’s an interesting read. I had only recently purchased my own Mindstorms set and so was very impressed with the “toy” robot beating all the more “serious” entries.

The key of course was with the software, not the hardware. All physical systems have error and while a Lego creation might have more error than most – being made from inexpensive mass-produced parts – in the end is not so much the amount of error but how you handle it that makes the difference. In Peeves’s case the author used a PID-based closed-loop system to achieve a winning result. (I’m guessing the other club members didn’t “do” software.)

PID (Proportional-Integral-Derivative) controllers are used in many applications. They are very simple to implement and understand, but when googling the subject it can seem anything but simple. I thought I’d document my experience setting up and using a PID controller for my own WallBot project.

In my case I’m trying to get a robot to travel in a straight line.

640_1401_IMG_3030

My bot is just like Peeves; a two wheeler – with a rear caster for stability – that maneuvers using differential steering.

Wheels turning at different rates maybe great for steering but not so good if you want to go straight. No two motors can turn at the exact same same speed. Over time the differences in the motors will add up.

Closed-Loop System

A simple approach to dealing with this problem is to measure how much each wheel actually turns and adjust the motor speeds dynamically to correct for the differences.

This is referred to as a closed-loop system.

Screen Shot 2014-01-29 at 7.56.33 PM

Adding quadrature encoders to the wheels provides one way to measure wheel rotation.

Using them I was able to created a function get_ticks_since_last() that fills out three variables:

  long leftTicks;
  long rightTicks;
  long ms;

  get_ticks_since_last( &leftTicks, &rightTicks, &ms)

leftTicks is the number of “ticks” the left wheel has turned since we last called the function. rightTicks is the right wheel count. ms is the number of milliseconds elapsed since we last called.

A tick represents a measure of wheel rotation. [In this particular case it’s 1/48th of a rotation.] A positive count means the wheel is turning forwards, negative means backwards.

[To simplify coding we are only going to consider forward motion, meaning tick counts will always be positive. We are also going to assume the the code is getting called at regular intervals otherwise we would have to factor for any variation in the time that has elapsed between calls.]

We can use the difference between left ticks and right ticks to tell in what way the bot is moving:

long diff = leftTicks - rightTicks;

if( diff > 0 )
{
  // left motor is turning faster
  Serial.println("Turning Right");
}
else if( diff < 0 )
{
  // right motor is turning faster
  Serial.println("Turning Left");
}
else
{
   Serial.println("Going straight");
}

Making Corrections

So to go straight we want there to be no difference between left and right tick counts.

Any difference is an “error” and we need to take some corrective action.

We can use this simple logic:

long error = leftTicks - rightTicks;

if( error > 0 )
{
  // left motor is turning faster
  adjustLMotor -= 1;
  adjustRMotor += 1;
}
else if( error < 0 )
{
  // right motor is turning faster
  adjustLMotor += 1;
  adjustRMotor -= 1;
}

adjustLMotor and adjustRMotor are two variables used to modify the current left and right motor speeds. We don’t know what the actual motor settings are but we do know that we need to increase the left motor and decrease the right motor if the error is negative, and vice versa if positive.

We can simplify the code more:


long adjust = (error!=0) ? (error>0 ? 1 : -1) : 0;

adjustLMotor -= adjust;
adjustRMotor += adjust;

adjust is either 1 if the error is positive , -1 if negative and 0 if no error.

Proportional Response

This reading of the ticks and adjusting the motors happens multiple times a second. Eventually the error will reduce to zero. However, the adjustments will happen at the same rate regardless of the size of the error.

We can improve the responsiveness of the system by making the adjustments proportional to the error; the bigger the error, the bigger the adjustment. We can do this by multiplying the error by some factor to give us a better adjustment value. This factor we will call Kp and represents the P (for Proportional) in PID:


float Kp = ??;

long adjust =  (Kp*error);

adjustLMotor -= adjust;
adjustRMotor += adjust;

So what value should this Kp factor be? Unfortunately there’s no simple answer to that question.

In my case I was seeing errors in the range of 4 to 10 ticks. My motor controller has a power range of 0 to 127 so I started with a Kp of 1.0. After experimenting with different values I ended up with a Kp of 1.5.

You want Kp to be large enough so the adjustments are responsive but not so large that it causes the corrections to overshoot the ideal setting. Once that happens the adjustments themselves will introduce error and the bot will never settle down to moving in a straight line. It will move more like a drunk driver.

We can refine the code somewhat more:


long Kp = 1500L;

long adjust =  (Kp*error)/1000L;

adjustLMotor -= adjust/2;
adjustRMotor += adjust/2;

I’ve replaced the use of floating point with pure integer math. Floating point numbers are expensive on the Arduino and should be avoided if possible.

I’ve also split the adjustment across the two motors to ensures that modifying the relative speeds of each motor does not effect the overall speed of the robot.

Integral Response

Right now we have code that can detect and correct for errors in order to travel in a straight line. However, if we were to use it as is, the robot will travel in a straight line but we’ll notice that a lot of the time it does not go in the same direction the robot was initially facing, but rather at a slight angle from its origin:

Screen Shot 2014-01-29 at 5.04.21 PM

This is the behavior I was seeing with my bot.

While the proportional response will achieve a reasonably straight line motion, it will take some amount of time to do so and during that time the robot will have turned a little.

The reality is that the proportion response works to minimize the error that is about happened, based on the current error that has just happened, but it does not try to correct for that error. Eventually all these errors will add up.

To correct for the errors that have happened we need to keep a running tally of them and use that total to adjust the motors still more so that, over time, the total accumulation of errors becomes zero.

And in code we would write it so:


long Kp = 1500L;
long Ki = ??L;

totalErrors += error;

long adjust =  (Kp*error)/1000L
             + (Ki*totalErrors)/1000L;

adjustLMotor -= (adjust+1)/2;
adjustRMotor += adjust/2;

Again we apply some scaling factor to this total so we can tune the responsiveness of this new correction. The constant we call Ki and it represents the I (Integral) in PID. You can simply think of integral as meaning “the sum of” in mathematics.

For my bot I ended up with a value of 0.9 (or 900 in the integer math). After adding this the bot maintained very straight lines indeed. My only remaining issue is to stop the motors spinning up too quickly from a dead stop as it causes loss of traction and the bot can turn slightly. The lost of traction will not show up as error via the encoders so there can be no correcting for it.

Derivative Response

The final part is the derivative response; the D in PID. The derivative means the rate of change of something. So in our case the rate of change of the error. This is useful for responding to disturbances to the system – like a gust of wind if you were a flying bot. In my bot’s case there are no such external disturbances to deal with.

I did implement the derivative in code:


long Kp = 1500L;
long Ki = 900L;
long Kd = ???L;

totalErrors += error;
long dError = error - lastError;

long adjust =     (Kp*error)/1000L
                + (Ki*totalErrors)/1000L
                + (Kd*dError)/1000L;

adjustLMotor -= (adjust+1)/2;
adjustRMotor += adjust/2;

lastError = error;

But I ended up with setting Kd to zero. My understanding is that the vast majority of PID controllers are actually PI controllers. Hopefully I have situations where I need to use the derivative. I’d like to learn more about it.

Summing Up

I’ve quickly come to realize that having a good understanding of PID-based closed-loop systems is essential to doing robotics. Dealing with the real world means dealing with error and PID is a super tool for doing that.

It was also a lot of fun to implement and I’m surprised how easy it is to use. It does take time to tune the coefficients but the results are worth it.

The drawback is that you have to re-tune the PID anytime you make changes to the system, i.e. using a different motor controller, new motors, bigger wheels and so on. Even the battery getting low can change performance, so it is very much a touchy-feely system that needs continual attention.

There are also a lot of ways to refine the controller to make it perform better for each particular situation. What I’ve done so far is pretty crude. A useful breakdown of PID controller refinements and tweaks is given by Brett Beauregard on his blog. He is the author of the Arduino PID library. I’d recommend the PID library as a starting point for your own PID controller experiments.

I hope I have more opportunities to use PID in my applications. I’m thinking I will.

Follow Up

I’ve been testing my WallBot more thoroughly and noticed some anomalies. While the PID controller has vastly improved the straightness of the bot’s forward motion, there can be some significant deviation from the center line.

The deviations are random. The data from the PID control looks good:

Screen Shot 2014-01-30 at 1.30.43 AM

The error oscillates between 1 and 2 ticks, same with the integral. Not sure what the problem is yet.

Advertisements

3 comments

  1. This is the best novice introduction to PID control I have seen so far. Very complete stepwise understandable explanation, and well-written to boot. I might borrow some of your work for my own DIY 2-wheel drive bot, which will hopefully move straight soon (and turn at exact angles) with the help of a compass unit. Thank you for this!

  2. RoboRichio · · Reply

    There is still one thing I don’t understand. The second part of the PID gives the following formula:

    totalErrors += error;

    long adjust = (Kp*error)/1000L
    + (Ki*totalErrors)/1000L;

    The problem is that totalErrors will keep on growing larger and larger. When this happends, and it will, the adjustment wil grow as well. Not just grow, but it is getting realy big in my case.

    Wat do I not understand at this part?

    Thanks!

    1. Remember that “error” itself should oscillate between positive and negative values. So while “totalErrors” might grown (negatively or positively) over the short term, over the long term it should stay close to zero. Of course it is totally possible to have PID coefficients that “amplify” the error rather than “dampen” it. In that case error (and totalErrors) will oscillate wildly about zero and grow rapidly in magnitude. In those cases we would be “over correcting” for he error and thus adding error to the system.

      Look at the graph at the end of the article to see the relationship between “errors” (ERR) and “totalErrors” (SUM). It shows a section of the PID controller where it has reached a steady state (i.e. it’s working).

Comments welcome

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s