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:
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.
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:
We can use the data from our step test to calculate to Kp.
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.
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.
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):
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.
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.
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:
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:
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:
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
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.
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]