You are viewing an older revision! See the latest version

PID

A proportional-integral-derivative controller (PID controller) is a generic loop feedback mechanism. It measures a process variable (e.g. temperature), and checks it against a desired set point (e.g. 100 degrees celsius); it then attempts to adjust the process variable by changing a controller output (e.g. PWM signal to an electric heater) which will bring the process variable closer to the desired set point.

The wikipedia article on PID controllers is an excellent place to start in understanding the basic concepts. The controlguru website also contains a wealth of easy to digest information on how to implement and tune a PID controller.

As these resources can already explain the fundamentals and implementation of a PID controller, this page will detail the PID library and then present an example of putting it into practice.

Software

The PID software runs a loop at a set interval, and performs the following calculation:

http://mbed.org/media/uploads/aberk/_scaled_pidequation.jpg

Where

  • CO is the controller output
  • CObias is an optional, user set bias for the controller output
  • Kc is a proportional tuning constant
  • e(t) is the error at time t
  • Ti is an integral tuning constant
  • Td is a derivative tuning constant
  • PV is the process variable
  • dt is the rate the loop runs at

The controller works in percentages during the calculations and then scales relevant outputs back into real world values.

Initialization

When a PID object is created, the three tuning constants (Kc, Ti, Td) and an interval time are passed as parameters. By default the controller starts in manual mode - whenever the controller is changed to auto mode, the working variables are reset (according to the current limits) which allows for "bumpless" transfer between manual and auto mode. The input and output limits are set as 0.0-3.3 [volts], and the tuning constants are slightly modified to make things easier during calculations. Finally, appropriate variables (such as the controller output and process variable) are initialized to zero before the main loop method is attached to a Ticker which runs at the rate passed in by the user.

Hello World!

In order to set up a PID object for use in a specific application, a typical initialization and loop might look like this.

Import program

00001 #include "PID.h"
00002 
00003 #define RATE 0.1
00004 
00005 //Kc, Ti, Td, interval
00006 PID controller(1.0, 0.0, 0.0, RATE);
00007 AnalogIn pv(p15);
00008 PwmOut   co(p26);
00009 
00010 int main(){
00011 
00012   //Analog input from 0.0 to 3.3V
00013   controller.setInputLimits(0.0, 3.3);
00014   //Pwm output from 0.0 to 1.0
00015   controller.setOutputLimits(0.0, 1.0);
00016   //If there's a bias.
00017   controller.setBias(0.3);
00018   controller.setMode(AUTO_MODE);
00019   //We want the process variable to be 1.7V
00020   controller.setSetPoint(1.7);
00021 
00022   while(1){
00023     //Update the process variable.
00024     controller.setProcessValue(pv.read());
00025     //Set the new output.
00026     co = controller.compute();
00027     //Wait for another loop calculation.
00028     wait(RATE);
00029   }
00030 
00031 }

API

[Not found]

Example: Velocity Control

This example will show how to use the PID controller library and a brushed DC motor with a quadrature encoder and H-bridge to perform velocity control.

We can calculate the velocity of the motor by taking two samples of the quadrature encoder's pulse count in a short interval and then dividing the difference between them by the length of the interval to get the number of pulses per second.

We could turn pulses per second into a more familiar unit, such as metres per second, but we want to try and choose a process variable which is as closely related to what we're measuring as possible; and since what we're measuring (pulses per second) is directly proportional to the velocity, it should provide a much better value to work with during our PID calculations.

Our process variable will therefore be the number pulses per second we've read, and our controller output will be the PWM signal's duty cycle to the H-bridge.

Tuning Method

There are many ways to tune the constants in a PID controller, including simple trial and error; the method presented on controlguru involves fitting a simple first order plus dead time (FOPDT) dynamic model to process test data that we take - a lot easier than it sounds! This is the method we will follow, but it is not the only way.

Step Test

The first we need to do, is observe how our process variable changes with respect to controller output. We'll do this by performing a step test - after setting our controller output to a specific value and observing our process variable, we will then "step" our controller output to a new value and watch what happens to our process variable.

Here are the results.

600

The number of counts per second were observed while the PWM duty cycle was 70%, and after it was stepped to 60%.

Process Gain Constant

The process gain constant or Kp describes how the process variable changes when the controller output changes. It is calculated in the following way:

337

We can use the data from our step test to calculate to Kp.

600

dPV = 1000, and dCO = -0.1; when talking about the controller output, we will use how far "on" or "off" it is as a percentage in our calculations to make things easier. Therefore dCO = -10%.

Kp = dPV / dCO = 1000 / -10% = -100 counts per second/%

Process Time Constant

The process time constant or Tp describes how fast the process variable changes when the controller output changes. It is calculated by measuring the time it takes from when the process variable first begins to respond to a step in the controller output, to 63% of the total change in the process variable from a step in the controller output. The 63% comes from solving the FODPT differential equation and is beyond the scope of this page.

Our dPV = 1000, so 63% would be when our process variable has increased 630 counts.

600

Since the ticks on the x-axis are 0.01s intervals we obtain:

Tp = 72 - 64 = 8 = 0.08s

Dead Time Constant

The dead time constant or [theta]p describes the delay between a change in the controller output and when the process variable first starts to respond.

600

On our graph it appears the change is almost instantaneous - we should bear in mind that it's impossible for the dead time to be less than the sample time (in our case 0.01s), and also it's generally better to be conservative with the value.

From the graph:

[theta]p = 64 - 63 = 1 = 0.01s

Constants

We have now estimated the constants required for our controller; they are:

Kp (Process Gain)-100 counts per sec/%
Tp (Process Time)0.08s
[theta]p (Dead Time)0.01s

Proportional Controller

A proportional or P-Only controller has no integral or derivative action. A moderate controller gain, or Kc, can be calculated for a P-Only controller in the following way (see controlguru for details):

300

Giving Kc = -0.025... %/counts per second

We should be careful not to read too much into these constants; an increase in decimal places will probably not relate to an increase in performance.

Before we plug this into our controller, we have to change Kc into a dimensionless constant as the PID library works in the percentage domain.

http://mbed.org/media/uploads/aberk/_scaled_kcdimensionless.jpg

The minimum value for our process variable is 0 (when the motor is stationary), and it achieves approximately 10500 counts per second when run at full speed. Therefore we get:

Kc = -0.025 * (10500/100) = -0.025 * 105 = -2.6

We'll use the following code to test our controller.

Import program

00001 //****************************************************************************/
00002 // Includes
00003 //****************************************************************************/
00004 #include "PID.h"
00005 #include "QEI.h"
00006 
00007 //****************************************************************************/
00008 // Defines
00009 //****************************************************************************/
00010 #define RATE  0.01
00011 #define Kc   -2.6
00012 #define Ti    0.0
00013 #define Td    0.0
00014 
00015 //****************************************************************************/
00016 // Globals
00017 //****************************************************************************/
00018 //--------
00019 // Motors 
00020 //--------
00021 //Left motor.
00022 PwmOut leftMotor(p23);
00023 DigitalOut leftBrake(p19);
00024 DigitalOut leftDirection(p28);
00025 QEI leftQei(p29, p30, NC, 624);
00026 PID leftController(Kc, Ti, Td, RATE);
00027 //-------
00028 // Files
00029 //-------
00030 LocalFileSystem local("local");
00031 FILE* fp;
00032 //--------
00033 // Timers
00034 //--------
00035 Timer endTimer;
00036 //--------------------
00037 // Working variables.
00038 //--------------------
00039 volatile int leftPulses     = 0;
00040 volatile int leftPrevPulses = 0;
00041 volatile float leftPwmDuty  = 1.0;
00042 volatile float leftVelocity = 0.0;
00043 //Velocity to reach.
00044 int goal = 3000;
00045 
00046 //****************************************************************************/
00047 // Prototypes
00048 //****************************************************************************/
00049 //Set motors to go "forward", brake off, not moving.
00050 void initializeMotors(void);
00051 //Set up PID controllers with appropriate limits and biases.
00052 void initializePidControllers(void);
00053 
00054 void initializeMotors(void){
00055 
00056     leftMotor.period_us(50);
00057     leftMotor = 1.0;
00058     leftBrake = 0.0;
00059     leftDirection = 0;
00060 
00061 }
00062 
00063 void initializePidControllers(void){
00064 
00065     leftController.setInputLimits(0.0, 10500.0);
00066     leftController.setOutputLimits(0.0, 1.0);
00067     leftController.setBias(1.0);
00068     leftController.setMode(AUTO_MODE);
00069 
00070 }
00071 
00072 int main() {
00073 
00074     //Initialization.
00075     initializeMotors();
00076     initializePidControllers();
00077 
00078     //Open results file.
00079     fp = fopen("/local/pidtest.csv", "w");
00080     
00081     endTimer.start();
00082 
00083     //Set velocity set point.
00084     leftController.setSetPoint(goal);
00085 
00086     //Run for 3 seconds.
00087     while (endTimer.read() < 3.0){
00088         leftPulses = leftQei.getPulses();
00089         leftVelocity = (leftPulses - leftPrevPulses) / RATE;
00090         leftPrevPulses = leftPulses;
00091         leftController.setProcessValue(leftVelocity);
00092         leftPwmDuty = leftController.compute();
00093         leftMotor = leftPwmDuty;
00094         fprintf(fp, "%f,%f\n", leftVelocity, goal);
00095         wait(RATE);
00096     }
00097 
00098     //Stop motors.
00099     leftMotor  = 1.0;
00100     
00101     //Close results file.
00102     fclose(fp);
00103 
00104 }

The graph below shows our results.

600

As seen, our P-Only controller is experiencing "offset"; that is, the set point it reaches is offset from the actual set point we want it to reach. This problem can be solved by using a PI controller which introduces an integral term that updates the output based on the sum of the previous errors.

As we increase the proportional constant, the offset is reduced but at the cost of overshoot (going further than our set point before dropping down to the right value) and also increasingly wild oscillations. A proportional constant four times the size of a moderate value will give results that look like this:

600

PI Controller

We'll use the same constants we calculated before to work out two tuning parameters; the first will be the one we met before in the P-Only controller, that is the controller gain Kc, and the second will be the reset time Ti.

For the PI controller:

http://mbed.org/media/uploads/aberk/_scaled_kcpicontroller.jpg

Where

  • Kp = Process gain, calculated earlier from step test
  • Tp = Process time, calculated earlier from step test
  • [theta]p = Dead time, calculated earlier from step test
  • Tc (moderate) = max(1 * Tp, 8 * [theta]p)
  • Reset time Ti = Tp

This gives us the following values for the tuning constants:

  • Tc = max(0.08, 0.08) = 0.08
  • Kc = (1 / -100) * (0.08 / (0.01 + 0.08)) = -0.009
  • Kc (dimensionless) = -0.009 * (10500/100) = -0.9
  • Ti = 0.08

Plugging these into our controller gives the following results:

600

Not bad at all! No overshoot, and only a little delay in reach the set point which it stays at comfortably.

Let's see what happens if we choose more conservative, or aggressive tuning parameters. Ti will remain equal to Tp but we'll use the following values for Kc.

  • Aggressive tuning
    • Tc = max(0.1 * Tp, 0.8 * [theta]p) = max(0.008, 0.008) = 0.008
    • Kc = (1 / -100) * (0.08 / (0.01 + 0.008)) = -0.04
    • Kc (dimensionless) = -0.04 * (10500/100) = -4.7
  • Conservative tuning
    • Tc = max(10 * Tp, 80 * [theta]p) = max(0.8, 0.8) = 0.8
    • Kc = (1 / -100) * (0.08 / (0.01 + 0.8)) = -0.001
    • Kc (dimensionless) = -0.001 * (10500/100) = -0.105

600

While the aggressive tuning has a faster response, we also see a big overshoot and lots of oscillation which takes a long time to settle. The conservative tuning doesn't overshoot but takes a lot longer to get closer to the set point.

The moderate tuning gives adequate performance and was tested on a range of changing set points.

600

With just a little overhead, we've managed to select very favourable tuning constants for our PID controller. Even better values could be selected with the appropriate software, but this example has shown you can do it by eye on simple graphs.

Adding the derivative component

For most applications, a PI controller might be sufficient for control, as shown in this example.

Adding the derivative component to the controller is much like adding the integral component; there is simply an extra tuning constant to calculate from our original three values for Kp, Tp, and [theta]p.

More information including some common pitfalls when adding the derivative can be found on the controlguru website.

Library

[Not found]

Reference


All wikipages