In this post, we explain how to discretize and implement a Proportional Integral Derivative (PID), controller. To demonstrate the PID controller implementation, we use a ball beam system and an Arduino microcontroller.
In the s-domain the PID controller has the following form
(1)
where
(2)
where
In the time domain, the PID controller 1, has the following form
(3)
where
(4)
To obtain 3 from 1, we used the following Laplace transform pairs (by neglecting constants):
(5)
The controller 3 is the continious-time domain. However, to implement the controller using microcrontrollers we need to discretize the controller. There are several ways for discretizing the controller. In this post, we will use the simplest method (by the author’s opinion). To implement the controller, we first differentiate the equation 3:
(6)
Next, using the finite difference method we approximate the first and second derivatives in 6. Due to favorable stability properties, we use backward differences. The first derivative of the control input is approximated as follows
(7)
where
Similarly, we approximate the first derivative of the control error
(8)
The second derivative of the control error is approximated as follows
(9)
By substituting \eqref{firstDerivativeApproximationError} for the time indices
(10)
By substituting the approximations 7, 8, and 10 in 6, we obtain the final expression for the control signal at the time instant
(11)
where the constants
(12)
From 11 we see that the control action at the time instant
for k=0,1,2,3,…
1. Obtain sensor measurements.
2. Compute the control signal
3. Send the control signal
4. Wait for
where
The discretization step is chosen on the basis of how fast the system response is. Namely, we can apply a step control input
In the sequel, we present the code that implements the PID controller. The code can be found here. We use the ball-beam system that is used in the following video to demonstrate the PID controller tuning.
//sensor parameters
int distanceSensorPin = A0; // distance sensor pin
float Vr=5.0; // reference voltage for A/D conversion
float sensorValue = 0; // raw sensor reading
float sensorVoltage = 0; // sensor value converted to volts
float k1=16.7647563; // sensor parameter fitted using the least-squares method
float k2=-0.85803107; // sensor parameter fitted using the least-squares method
float distance=0; // distance in cm
int noMeasurements=200; // number of measurements for averaging the distance sensor measurements
float sumSensor; // sum for computing the average raw sensor value
// motor parameters
#include <Servo.h>
Servo servo_motor;
int servoMotorPin = 9; // the servo motor is attached to the 9th Pulse Width Modulation (PWM)Arduino output
// control parameters
float desiredPosition=35; // desired position of the ball
float errorK; // position error at the time instant k
float errorKm1=0; // position error at the time instant k-1
float errorKm2=0; // position error at the time instant k-2
float controlK=0; // control signal at the time instant k
float controlKm1=0; // control signal at the time instant k-1
int delayValue=0; // additional delay in [ms]
float Kp=0.2; // proportional control
float Ki=10; // integral control
float Kd=0.4; // derivative control
float h=(delayValue+32)*0.001; // discretization constant, that is equal to the total control loop delay, the number 32 is obtained by measuring the time it takes to execute a single loop iteration
float keK=Kp*(1+h/Ki+Kd/h); // parameter that multiplies the error at the time instant k
float keKm1=-Kp*(1+2*Kd/h); // parameter that multiplies the error at the time instant k-1
float keKm2=Kp*Kd/h; // parameter that multiplies the error at the time instant k-2
void setup()
{
Serial.begin(9600);
servo_motor.attach(servoMotorPin);
}
void loop()
{
unsigned long startTime = micros(); // this is used to measure the time it takes for the code to execute
// obtain the sensor measurements
sumSensor=0;
// this loop is used to average the measurement noise
for (int i=0; i<noMeasurements; i++)
{
sumSensor=sumSensor+float(analogRead(distanceSensorPin));
}
sensorValue=sumSensor/noMeasurements;
sensorVoltage=sensorValue*Vr/1024;
distance = pow(sensorVoltage*(1/k1), 1/k2); // final value of the distance measurement
errorK=desiredPosition-distance; // error at the time instant k;
// compute the control signal
controlK=controlKm1+keK*errorK+keKm1*errorKm1+keKm2*errorKm2;
// update the values for the next iteration
controlKm1=controlK;
errorKm2=errorKm1;
errorKm1=errorK;
servo_motor.write(94+controlK); // the number 94 is the control action necessary to keep the ball in the horizontal position
// Serial.println((String)"Control:"+controlK+(String)"---Error:"+errorK);
// these three lines are used to plot the data using the Arduino serial plotter
Serial.print(errorK);
Serial.print(" ");
Serial.println(controlK);
unsigned long endTime = micros();
unsigned long deltaTime=endTime-startTime;
// Serial.println(deltaTime);
// delay(delayValue); // uncomment this to introduce an additional delay
}
A few comments are in order. The code lines 3-11 are used to define the sensor parameters. We are using a SHARP infrared distance sensor to measure the ball position on the beam. The procedure for calibrating the distance sensor is given in another post, and explained in the video below.
The code lines 14-16 are used to define the servo-motor parameters. The code lines 20-35 are used to define the controller parameters. The code lines 20-25 define respectively variables for
The code lines 44 and 77 are used to measure the time it takes for the code to execute. This is necessary for obtaining the exact value of the constant