Tuning a PID Controller
If you’ve ever tried to control the speed of a motor, balance a robot, or maintain a stable temperature with an Arduino or any other micro-controller, chances are you’ve come across something called a PID controller. At first, PID might look complicated with all its math and jargon—but its actually a simple concept! Today, I will break it down for you in simple terms so that you can quickly learn how to implement it in your own projects.
What is a PID Controller?
PID stands for:
- P: Proportional
- I: Integral
- D: Derivative
Think of it like this:
- P is how hard you push toward your goal.
- I keeps track of how far you’ve been off the goal over time and nudges you to fix it.
- D watches how fast you’re approaching the goal and prevents you from overshooting.
All three work together to reach your target smoothly and accurately.
Implementing PID Controller in Code
Writing the PID logic in code has to be the easiest part of all. There are only few thing to keep in mind.
- Constant main loop
unsigned long loop_timer;
loop_timer = micros();
void loop() {
/* Read sensor value */
/* PID code */
while (micros() - loop_timer < 4000);
loop_timer = micros();
}
- Calculate error
float error, prev_error;
error = set_point - sensor_value;
- Calculate p value
float p;
p = kp * error;
- Calculate d value
float d;
d = kd * (prev_error - error);
- Calculate i value
float i;
i = i + ki * error;
- Total PID value
float output;
output = p + i - d;
Tuning the PID Constants
Now all you have to do is change the values of kp, ki and kd such that you get
desired output from your system.
-
Start with only one constant
kp, likekp = 2.0, ki = 0, kd = 0and observe the output. Tweak this value so that you get optimum possible outcome. -
Next, start assuming
kd,yeahkdnotki. We secondly tunekdbecause, it dampens the over and under shoot created bykp. You might need to go back tokpand tweak it to tune this value properly. -
Finally, tune the
ki. For the many systems you might not even needki. You have to think slightly differently for constantkibecause if you see above mathematical expression for the value associated withkiwhich is—i, depends on previous values ofimeaning it is integrating all the values over many loops whereas fordyou only need what previouserrorvalue was, meaning it is accounting for how large the difference inerroris between the loops and finally forkp, it just needs the currenterror.
Other Important Points
-
Calibrating the sensor—You might need to add offset values depending upon the type and quality of sensor used in the project.
-
Skip the processing in deadzone—As we know the total PID value is the direct output to the actuator, suppose, for a balancing robot in a single axis, if the sensor output value in degree is \(0^\circ\) when perfectly vertical, then possible deadzone range (depending upon the type of sensors, motors etc.) can be \(\pm2^\circ\), where processing the PID value can be completely skipped and simply set to 0.
-
Also Keep in account for the actuator deadzone—If you are using an arduino to drive a motor with a motor driver, you have to give out PWM value from any analog pin to the driver, which generally in code value from 0 to 255 and you might have noticed the motor does not immediately start moving in initial values maybe even for upto 10 or 20, this maximum value until when the motor does not start moving is the actuator deadzone. And to fix this problem simply add this value 20 to PID output or you can also map your PID value to range 20 to 255 of motor driver output.
-
Clamp the PID value to your actuator range—Suppose if your total PID value came out to be 257 to give it to your motor driver which should be only between range 0 to 255, the value might overflow as 2. So better clamp the value \(\gt\) 255 to 255 and \(\lt\) 0 to 0.
Combining Everything
A typical setup looks like this:
#include <Arduino.h>
/* Required variables */
float sensor_value, output, set_point, error, prev_error, p, i, d;
unsigned long loop_timer;
/* PID parameters */
float Kp = 2.0, Ki = 5.0, Kd = 1.0;
/* Utility functions */
float read_sensor() {}
void drive_motor(float value) {}
void setup() {
/* Required setup code ... */
loop_timer = micros();
}
void loop() {
/* Read sensor value */
sensor_value = read_sensor();
/* PID code */
error = set_point - sensor_value;
p = kp * error;
d = kd * (prev_error - error);
prev_error = error;
i = i + ki * error;
output = p + i - d;
/* Driving the actuator */
drive_motor(output);
/* Constant 250Hz main loop */
while (micros() - loop_timer < 4000);
loop_timer = micros();
}