A PID(Proportional-Integral-Derivative) Controller is a closed loop controller that is effective and extremely simple to implement. The controller continuously calculates an error function as the difference between the current value and the setpoint(the desired target for our system), and tries to minimize this by adjustment of the control variable, to a new value based on a weighted sum.

The advantage of a PID controller lies in the fact that the information about the process itself is not required, allowing this controller to be implemented in many situations.

The disadvantages include the fact that while the controller performs well if tuned properly, it does not provide optimal control. It is one of the best controllers without knowledge of the process itself, and is this reliant on reaction and compromise. A controller with knowledge of the process is almost always better.

Controller

The weighted sum is as follows:

where Kp, Ki, and Kd are positive constants denoting the coefficients for proportional, integral, and derivative terms respectively.

  • Proportional accounts for instantaneous deviation from the setpoint.
  • Integral accumulates error and increases effort if the control effort has been less effective.
  • Derivative accounts for future error, and is based on current rate of change.

Some implementations of this controller may involve only one or two of these terms to provide the necessary control. See the tuning section for more.

Implementation

Following is a C code snippet which implements the PID controller. Note the versatility of the controller, requiring nothing but the measured value, and a setpoint.

pid.c
#include <stdio.h>
 
int main(int argc, char **argv) {
  double kp, ki, kd;
  // TODO Initialize kp, ki and kd as required
  double error = 0;
  double integral = 0;
  double previous_error = 0;
  double derivative = 0;
  double dt;
  // TODO Initialize dt to time taken for each iteration of the following loop
  double setpoint;
  // TODO Initialize setpoint
  for (;;) {
    // TODO Get measured_value from sensor
    error = setpoint - measured_value;
    integral = integral + error * dt;
    derivative = (error - previous_error) / dt;
    output = (kp * error) + (ki * integral) + (kd * derivative);
    // TODO Use output to adjust control variable
    previous_error = error;
  }
  return 0;
}

In the above, the dt in both integral and derivative can be absorbed in the ki and kd if required. This allows the integral term to be a simple summation of all the errors. See the example below for more.

Tuning

Tuning the controller involves determination of Kp, Ki, and Kd that best suit our needs. While there are a multi-fold of methods to tune the controller, such as Zeigler-Nichols method or the Tyreus Luyben method, the simplest method is to manually tune the controller. While time consuming at first, with a little practice and experience this method becomes quick and effective.

The process of tuning is roughly as follows:

  1. Set ki and kd to zero, and try to make a proportional controller by increasing kp till the system converges to the setpoint relatively quickly, without much overshoot. If the system behaves good enough, there is no need to set ki or kd.
  2. If the proportional controller was not good enough, first set kp to half the value of kp at which the system oscillates, then increase ki till the process rises quickly enough and oscillates about the setpoint. If the oscillations die out fast enough, there is no need to set kd.
  3. Increase kd till the oscillations die out quickly.

Further fine tuning can be done by increasing or decreasing kp, ki and kd by small amounts, keeping the following in mind:

  • Increasing kp makes the controller more aggressive, it reduces the settling time, but also increases the oscillations, overshoot and settling time.
  • Increasing ki helps in eliminating steady-state error, but increases oscillations and overshoot.
  • Increasing kd smoothens out oscillations and decreases overshoot, but a high kd can make the system unstable.

The following are some other tips that are useful while tuning:

  • Saturation of integral term - Saturation of total error, ie capping it at a certain value is useful when the integral term has a tendency to dominate in certain situations, but reduction of ki is not possible without disturbing the controller.
  • Setpoint ramping - systems than have drastically and suddenly changing setpoints may cause problems such as excess overshoot, and sometimes even divergence from the setpoint. To fix this behavior, the setpoint can be changed from its old value to its new value slowly, for example linearly instead of abruptly. It is necessary to note that this slows the settling time after the change drastically.

Example of PID controller for line follow

Following is Arduino code for a line following bot using a two wheel differential drive using an 8 sensor array. Data is taken from the 8 sensors and a deviation is calculated about zero, and is used as the input for the PID Controller. The speed of the bot is split into two parts, linear and angular. The output of the controller is used to change both the linear and angular parts of the speed, which are then later added and written to the motors.

The analog data from the sensor is made binary - 1 indicates that the sensor is on the line and 0 indicates the sensor is not. The sensors are given weights starting from 0 to 7. The deviation is then the sum of the weights of the sensors outputting 1, divided by the total sensors outputting 1. This is the deviation about 4, and thus 4 is subtracted to make the deviation about 0. The above method of finding deviation is simple and gives an increasing deviation from one side to the other, and automatically corrects for varying number of sensors on the line.

linefollow.ino
int sensor[8],x[8];
int i = 0;
long int sensor_output;
 
float deviation = 0, correction = 0, total_error = 0, previous_deviation = 0;
float v, w;
const float kp = 9;
const float ki = 0.06;
const float kd = 3;
const int V = 80;  // Linear speed at 0 deviation
const int W = 0;  // Angular speed at 0 deviation
const int pwm = 80;
 
void setup() {
 
  Serial.begin(9600);
  pinMode (8, OUTPUT);
  pinMode (9, OUTPUT);
 
}
 
 
void loop() {
 
  sensor_output = 0;
  for (i = 0; i < 8; i++) {
    sensor[7-i] = analogRead(i);  // Data from sensor
  }
  for(i=0;i<8;i++) {
    if(sensor[i]<=400) {  // Convert to digital
      x[i]=1;
    } else {
      x[i]=0;
    }
    deviation += x[i] * i;
    count += x[i];
  }
  deviation = deviation / count; // Calculate deviation about 4
  deviation = deviation - 4;  // Make deviation about 0
 
  correction = kp * deviation + (ki) * (total_error) + kd * (deviation - previous_deviation);
 
  if (((total_error <= 600) && (total_error >= (-600))) || (total_error * correction < 0)) {  
     total_error += correction;  // Saturate total_error term
  }
  previous_deviation = deviation;
 
  v = V - 0.4 * abs(correction);  // Larger correction, slower the linear speed to turn better
  w = W - 1 * correction;  // Larger angular speed to turn faster
 
  analogWrite(8, int(v - w));
  analogWrite(9, int(v + w));
 
  delay(10);
}
References