PID (Proportional, Integral, Derivative) is a powerful feedback algorithm used in everything from cruise control to robotics, allowing systems to respond smoothly and accurately to changing conditions. In this project, we’ll explore how PID works by applying it to a solar tracker built with an Arduino and a servo motor.
Rather than reacting abruptly to light level changes, our tracker continuously adjusts its position to align with the brightest light source using real-time feedback. This is a great, hands-on way to understand how PID combines current error, accumulated error, and predicted trends to create smart, stable movement. Let’s dive in!
PID stands for:
- P = Proportional
- I = Integral
- D = Derivative
It’s a control algorithm that continuously calculates an error value (the difference between a target and actual value) and tries to correct it by adjusting the system output.
Example: Imagine a thermostat where the current temperature is 90°F and the target is 75°F. We could set the cooler to 100% output until we reach 75°F and then shut off but this takes a lot of energy and reduces the hardware's lifetime. Instead, we can use a PID controller which gradually reduces the temperature over time based on the tuning parameters (Kp, Ki, and Kd).
Output = Kp * error + Ki * integral + Kd * derivative
Where:
- Kp controls how strongly you react to current error.
- Ki corrects for accumulated past errors (integral).
- Kd predicts future errors by analyzing the rate of change (derivative).
Include Libraries and Declare Variables
#include <Servo.h>
Servo rotator;
- We use a servo (rotator) to rotate the panel
float target = 0;
float current = 0;
float error = 0;
float prevError = 0;
float integral = 0;
float derivative = 0;
- These are variables used in the PID control loop.
Set PID Gains (Tuning Parameters)
float Kp = 0.01;
float Ki = 0.002;
float Kd = 0.015
- These values determine how aggressively the tracker reacts.
- You can tune them for better accuracy.
Servo Initialization and Setup
float currentServoAngle = 90;
unsigned long lastTime = 0;
void setup() {
Serial.begin(9600);
rotator.attach(9);
rotator.write(90);
lastTime = millis();
delay(2000);
}
- Servo is attached to pin 9 and set to the middle position (90°).
lastTime
tracks how much time passed between PID updates.- Serial is for debugging.
The Main Loop (Where the Magic Happens)
1.Calculate Time Delta
unsigned long now = millis();
float deltaTime = (now - lastTime) / 1000.0;
lastTime = now;
- We calculate the time difference (
deltaTime
) in seconds for accurate PID math.
Note: deltaTime
helps the derivative understand how to respond to fast changes (for example if the servo arm moves too fast, too quickly, reduce its power).
2. Read and Smooth LDR Inputs
int left = smoothen(20, A1);
int right = smoothen(20, A0);
current = left - right;
- LDRs read light from left and right.
current
is the difference in brightness.- Positive means more light on the right, so we need to rotate left and vice versa.
int smoothen(int arraySize, int inputPin) {
long sum = 0;
for (int i = 0; i < arraySize; i++) {
sum += map(analogRead(inputPin), 0, 675, 0, 1000);
}
return sum / arraySize;
}
- Reads and averages
arraySize
samples for a stable signal. - Uses
map()
to scale analog values.
3. PID Error Calculation
error = target - current;
- The tracker wants the light to be balanced (i.e., left = right, so
current = 0
). - So,
target = 0
, anderror = -current
.
if (abs(error) < 5) integral = 0;
- If the error is very small, we reset the integral term to prevent "integral wind-up".
Note: The integral term helps movement by making small adjustments based on previous error. However, if the previous error is adding up for a long time, the integral term can become very large. To prevent this, we set it back to 0 if the error is low i.e. we have reached the target.
4.PID Components
integral += error * deltaTime;
integral = constrain(integral, -100, 100);
derivative = (error - prevError) / deltaTime;
integral
: total accumulated error over time.derivative
: how fast the error is changing.constrain()
limits the integral to prevent runaway behavior.
5. Calculate Output and Move Servo
float output = Kp * error + Ki * integral + Kd * derivative;
currentServoAngle += output;
currentServoAngle = constrain(currentServoAngle, 0, 180);
rotator.write((int) currentServoAngle);
- Calculate the final output using the PID formula.
- Add output to current servo angle.
- Clamp the angle between 0–180°.
- Write new angle to the servo motor.
6.Debugging and Update State
prevError = error;
Serial.print("Error: ");
Serial.print(error);
Serial.print("\tOutput: ");
Serial.println(output);
delay(100);
- Save
error
for the next loop’s derivative calculation. - Print values to Serial Monitor to observe behavior.
- Small delay helps stabilize response.
- Read light values from LDRs.
- Calculate error from the difference.
- Update PID variables (integral, derivative).
- Compute output.
- Adjust servo angle accordingly.
- Wait a moment and repeat.
- Adjust Kp for responsiveness. Too high = overshoot.
- Adjust Ki if your tracker drifts over time.
- Kd helps smooth sudden changes and prevents overshoot.
Note: You can tune them manually or use the Ziegler-Nichols method (tutorial coming soon) or Tyreus–Luyben method (tutorial coming soon).
Comments