A PID controller — Proportional, Integral, Derivative — is the workhorse of industrial and hobbyist control systems. Whether you are building a self-balancing robot, a line follower, a temperature controller, or a motor speed regulator, the PID algorithm provides the closed-loop feedback needed to reach and maintain a target setpoint despite disturbances and system variations. This tutorial explains PID theory from first principles and walks through a complete, tuned Arduino implementation.
Table of Contents
- PID Theory: P, I, and D Explained
- PID Mathematics
- Arduino PID Implementation
- PID Tuning Methods: Ziegler-Nichols
- Integral Windup Prevention
- Real-World Application Examples
- Recommended Products
- Frequently Asked Questions
PID Theory: P, I, and D Explained
A PID controller continuously calculates an error value — the difference between the desired setpoint and the measured process variable — and applies a correction based on three terms:
Proportional (P): Applies a correction proportional to the current error. Large error = large correction. If the error is halved, the correction is halved. P control alone results in steady-state error (offset) — the system settles slightly away from the setpoint because the correction needed to counteract friction or gravity requires a non-zero error.
Integral (I): Accumulates the error over time and applies a correction proportional to the accumulated error. This eliminates steady-state error — even a tiny persistent error grows the integral term until the system reaches the exact setpoint. Too much I gain causes oscillation and slow response (the system overshoots and hunts around the setpoint).
Derivative (D): Applies a correction proportional to the rate of change of the error. D control acts as a damper — it counteracts rapid changes in error, reducing overshoot and improving stability. D gain must be tuned carefully: too much D causes the controller to react aggressively to sensor noise, producing unstable oscillations.
PID Mathematics
The PID control output at time t is:
// Continuous PID formula:
// u(t) = Kp*e(t) + Ki*∫e(t)dt + Kd*(de/dt)
// Discrete (digital) version using delta-time dt:
// error = setpoint - measured_value
// P_term = Kp * error
// I_term += Ki * error * dt
// D_term = Kd * (error - previous_error) / dt
// output = P_term + I_term + D_term
The three tuning parameters are:
- Kp (Proportional gain): Primary response magnitude. Units: output_units / error_units
- Ki (Integral gain): Eliminates steady-state error. Units: output_units / (error_units × time)
- Kd (Derivative gain): Damps overshoot. Units: output_units × time / error_units
Arduino PID Implementation
This implementation follows best practices: using delta-time for consistent behaviour regardless of loop speed, clamping output to valid range, and separating the PID class for reusability:
class PID {
private:
float Kp, Ki, Kd;
float integral;
float prevError;
float outMin, outMax;
unsigned long prevTime;
public:
PID(float kp, float ki, float kd, float minOut, float maxOut)
: Kp(kp), Ki(ki), Kd(kd),
outMin(minOut), outMax(maxOut),
integral(0), prevError(0), prevTime(0) {}
float compute(float setpoint, float measured) {
unsigned long now = micros();
float dt = (now - prevTime) / 1e6f; // Convert µs to seconds
if (prevTime == 0 || dt <= 0 || dt > 0.5f) {
// First call or stale timing — skip derivative
prevTime = now;
prevError = setpoint - measured;
return 0;
}
prevTime = now;
float error = setpoint - measured;
float dError = (error - prevError) / dt;
prevError = error;
float pTerm = Kp * error;
integral += Ki * error * dt;
// Clamp integral to prevent windup
integral = constrain(integral, outMin, outMax);
float dTerm = Kd * dError;
float output = constrain(pTerm + integral + dTerm, outMin, outMax);
return output;
}
void reset() {
integral = 0;
prevError = 0;
prevTime = 0;
}
void setGains(float kp, float ki, float kd) {
Kp = kp; Ki = ki; Kd = kd;
}
};
Example: Motor Speed PID Controller
// Motor speed control using PID
// Encoder on interrupt pins, motor on PWM pin
#include <Arduino.h>
#define ENCODER_A 2 // Interrupt pin
#define MOTOR_PWM 9 // PWM motor control
#define MOTOR_DIR 8 // Motor direction
volatile long encoderCount = 0;
void IRAM_ATTR encoderISR() {
encoderCount++;
}
PID speedPID(2.0f, 0.5f, 0.1f, -255.0f, 255.0f);
float targetRPM = 60.0f; // Target speed: 60 RPM
long prevCount = 0;
unsigned long prevMeasTime = 0;
float measureRPM() {
unsigned long now = millis();
long count = encoderCount;
float dt_ms = now - prevMeasTime;
if (dt_ms < 50) return -1; // Not enough time for accurate measurement
float pulses = count - prevCount;
float rpm = (pulses / 20.0f) * (60000.0f / dt_ms); // 20 pulses/rev encoder
prevCount = count;
prevMeasTime = now;
return rpm;
}
void setMotor(float output) {
if (output >= 0) {
digitalWrite(MOTOR_DIR, HIGH);
analogWrite(MOTOR_PWM, (int)output);
} else {
digitalWrite(MOTOR_DIR, LOW);
analogWrite(MOTOR_PWM, (int)(-output));
}
}
void setup() {
Serial.begin(115200);
pinMode(ENCODER_A, INPUT_PULLUP);
pinMode(MOTOR_PWM, OUTPUT);
pinMode(MOTOR_DIR, OUTPUT);
attachInterrupt(digitalPinToInterrupt(ENCODER_A), encoderISR, RISING);
}
void loop() {
float rpm = measureRPM();
if (rpm < 0) return;
float output = speedPID.compute(targetRPM, rpm);
setMotor(output);
Serial.print("Target: "); Serial.print(targetRPM);
Serial.print(" RPM | Actual: "); Serial.print(rpm);
Serial.print(" RPM | Output: "); Serial.println(output);
}
PID Tuning Methods: Ziegler-Nichols
The Ziegler-Nichols method provides a systematic starting point for PID tuning:
- Set Ki = 0 and Kd = 0 (P-only control)
- Slowly increase Kp until the system oscillates continuously at a constant amplitude. Record this value as Ku (ultimate gain) and the oscillation period as Tu (ultimate period in seconds)
- Apply the Ziegler-Nichols PID formulas:
// Ziegler-Nichols PID starting values:
float Kp = 0.6f * Ku;
float Ki = 2.0f * Kp / Tu; // = 1.2 * Ku / Tu
float Kd = Kp * Tu / 8.0f; // = 0.075 * Ku * Tu
Ziegler-Nichols gives a starting point — fine-tune from there. The method tends to produce aggressive response with some overshoot; reduce Kp by 20% and Ki by 30% from the Z-N values for a more conservative, overshoot-free response.
Integral Windup Prevention
Integral windup occurs when the output is saturated (at its maximum/minimum value) but the error persists — the integral keeps accumulating to enormous values. When the system finally reaches setpoint, the large integral causes significant overshoot before it winds down.
Prevention strategies:
- Clamping: Clamp the integral to [outMin, outMax] — shown in the class above
- Conditional integration: Only accumulate integral when output is NOT saturated
- Back-calculation: Reduce integral by the difference between saturated and actual output each time step
// Anti-windup with conditional integration:
float rawOutput = pTerm + integral + dTerm;
if (rawOutput >= outMax || rawOutput <= outMin) {
// Output is saturated — stop integrating
} else {
integral += Ki * error * dt; // Only integrate when not saturated
}
Real-World Application Examples
Self-Balancing Robot
An inverted pendulum (two-wheeled balancing robot) uses PID with the body tilt angle (from MPU6050 IMU) as the measured variable and motor PWM as the output. Typical gains: Kp=50–100, Ki=100–200, Kd=5–20. The MPU6050’s complementary filter or Kalman filter output provides the tilt angle; the PID output drives left and right motor speeds differentially.
Temperature Controller
A PID temperature controller for a reflow oven or PCB hotplate uses a thermocouple or NTC thermistor for temperature measurement and a SSR (Solid State Relay) for heater control. Since temperature changes slowly, the derivative term is often minimal. Typical gains: Kp=2–5, Ki=0.1–0.5, Kd=0.5–2. Time constant is seconds to minutes — sample and update every 500 ms.
Line-Following Robot
A PID line follower uses an array of IR sensors to measure the line position offset from centre (e.g., -100 to +100) as the error, and differential motor speed as the output. Kp=0.5–2.0, Ki=0–0.1, Kd=0.5–3.0. The D term is critical for smooth steering on curved lines.
Recommended Products
Waveshare General Driver Board for Robots (ESP32)
This ESP32-based robot driver board is purpose-built for PID-controlled motor projects. It integrates a dual H-bridge motor driver, encoder interfaces, servo headers, and the ESP32’s hardware PWM timers — all the hardware you need for closed-loop PID motor speed control without external motor driver boards.
Waveshare 20 kg.cm Bus Servo with 360° Encoder
Bus servos with integrated encoders are ideal for PID position control of robotic arm joints. The 360° magnetic encoder provides real-time position feedback that the PID controller uses to track joint angle targets with precision. The serial bus daisy-chain simplifies multi-joint PID implementation.
Waveshare DDSM115 Direct Drive Servo Hub Motor
For mobile robot PID speed control, the DDSM115 hub motor provides integrated current sensing and speed feedback — all the signals needed for closed-loop PID wheel speed regulation. The low-noise brushless design eliminates the encoder-jitter issues common with brushed DC motors and optical encoders.
Waveshare Direct Drive Servo Motor Driver Board (ESP32)
Waveshare’s ESP32-based motor driver board specifically designed for their DDSM-series hub motors. Integrates the ESP32 and motor control circuitry with ESP-NOW 2.4G WiFi support for wireless PID setpoint commands. Perfect for building a PID-controlled differential drive robot with remote speed control.
Frequently Asked Questions
Should I use the Arduino PID Library or write my own?
The Arduino PID library by Brett Beauregard is excellent and production-tested — it handles edge cases like derivative kick, output clamping, and bumpless parameter changes. Writing your own PID class (as shown in this tutorial) provides deeper understanding and customisation. For beginners, start with the library; once comfortable, implement your own for projects with specific requirements.
Why does my PID system oscillate even with small gains?
Oscillation with small gains usually indicates a measurement delay or sample rate that is too slow for your system’s dynamics. The PID controller responds based on stale measurements, causing phase lag that destabilises the loop. Increase your sample frequency, or reduce sensor averaging that introduces additional lag. Systems with significant actuator delay (e.g., heating elements) need a Smith Predictor or model-predictive control rather than a standard PID.
What is the difference between P, PI, PD, and PID controllers?
P-only: Simple, fast response, but steady-state error remains. PI: Eliminates steady-state error at the cost of some stability margin — most common for slow processes. PD: Fast response with damping, no integral so steady-state error persists — used for position control where gravity is absent. Full PID: Combines all three for the best performance in most applications.
How do I tune a PID controller for a self-balancing robot?
Start with Kp only, raising it until the robot begins to balance but oscillates slightly. Then add Kd (typically 10–20% of Kp) to damp the oscillation. Only add Ki if the robot drifts away from vertical over time; start very small (1–5% of Kp). Use Serial plotter in Arduino IDE to visualise the tilt angle and motor output during tuning.
Can PID work without derivative (PD or PI only)?
Yes — for many applications. Temperature controllers often use PI because temperature changes slowly and derivative action amplifies thermocouple noise. Motor speed controllers use PI with a velocity feed-forward term instead of derivative. Derivative is most valuable for fast mechanical systems (balancing robots, flight controllers) where overshoot must be minimised.
Add comment