PID Controller
The PID controller computes a control signal from three terms based on the error (setpoint minus measurement): Proportional (react to present), Integral (correct past), Derivative (anticipate future). It’s the most widely used controller in industry — from thermostats to quadcopters.
Why It Matters
PID handles ~95% of industrial control loops. You can tune it without a mathematical model of the plant. Understanding what each term does, how to tune them, and common implementation pitfalls (integral windup, derivative kick) is essential for any control application.
The PID Equation
Continuous form:
u(t) = Kp·e(t) + Ki·∫e(t)dt + Kd·de(t)/dt
| Term | What It Does | Too Much | Too Little |
|---|---|---|---|
| P (Proportional) | Output proportional to current error | Oscillation | Slow response |
| I (Integral) | Accumulates past error, eliminates steady-state offset | Overshoot, windup | Persistent offset |
| D (Derivative) | Reacts to rate of change, damps oscillation | Amplifies noise | More overshoot |
How Each Term Affects Response
┌── P alone: fast but steady-state error
Output ┌┤── P+I: eliminates offset but overshoots
↑ / └── P+I+D: fast, no offset, less overshoot
│ ╱─────────── setpoint
│ ╱╲ ╱──
│ ╱ ╲╱
│ ╱
│╱
└���─────────────→ time
Python Simulation
import numpy as np
import matplotlib.pyplot as plt
def pid_simulate(setpoint, Kp, Ki, Kd, dt=0.01, steps=1000):
y, integral, prev_error = 0.0, 0.0, 0.0
history = []
for _ in range(steps):
error = setpoint - y
integral += error * dt
derivative = (error - prev_error) / dt
u = Kp * error + Ki * integral + Kd * derivative
y += (u - y) * dt # first-order plant: tau * dy/dt = u - y
prev_error = error
history.append(y)
return history
t = np.arange(1000) * 0.01
plt.plot(t, pid_simulate(1.0, Kp=2.0, Ki=0, Kd=0), label='P only')
plt.plot(t, pid_simulate(1.0, Kp=2.0, Ki=1.0, Kd=0), label='PI')
plt.plot(t, pid_simulate(1.0, Kp=2.0, Ki=1.0, Kd=0.5), label='PID')
plt.axhline(y=1.0, color='r', linestyle='--', label='Setpoint')
plt.xlabel('Time (s)'); plt.ylabel('Output'); plt.legend(); plt.grid(); plt.show()Tuning Methods
Ziegler-Nichols (Ultimate Gain Method)
- Set Ki=0, Kd=0
- Increase Kp until output oscillates with constant amplitude → this is Ku (ultimate gain)
- Measure the oscillation period → Tu
- Apply:
| Controller | Kp | Ki | Kd |
|---|---|---|---|
| P only | 0.5·Ku | — | — |
| PI | 0.45·Ku | 1.2·Kp/Tu | — |
| PID | 0.6·Ku | 2·Kp/Tu | Kp·Tu/8 |
This gives aggressive tuning — expect ~25% overshoot. Reduce gains for smoother response.
Manual Tuning (Practical Heuristic)
- Start with Kp small, Ki=0, Kd=0
- Increase Kp until response is fast but oscillates slightly
- Add Ki to eliminate steady-state error (start small, increase until offset disappears)
- Add Kd to reduce overshoot (small amounts — too much amplifies noise)
Common Implementation Problems
Integral Windup
When the actuator saturates (e.g., motor at 100%), the error keeps accumulating in the integral term. When the error reverses, the bloated integral causes massive overshoot.
Fix — anti-windup clamping:
integral += error * dt
output = Kp * error + Ki * integral + Kd * derivative
if output > out_max:
output = out_max
integral -= error * dt # undo the integration that caused saturation
elif output < out_min:
output = out_min
integral -= error * dtDerivative Kick
When the setpoint changes suddenly (step input), the error derivative spikes to infinity, causing a violent actuator kick.
Fix — differentiate the measurement, not the error:
# Instead of: derivative = (error - prev_error) / dt
derivative = -(measurement - prev_measurement) / dt # only reacts to actual changeDerivative Noise Amplification
Derivative amplifies high-frequency noise. Fix: low-pass filter the derivative term:
derivative_raw = (error - prev_error) / dt
derivative = alpha * derivative_raw + (1 - alpha) * prev_derivative # EMA filterCascaded PID
For systems with nested dynamics (e.g., drone attitude → angular rate), use two PID loops:
Setpoint → [Outer PID] → rate_setpoint → [Inner PID] → motor_command
↑ ↑
angle sensor gyroscope
The inner loop runs faster (1kHz) and handles fast dynamics. The outer loop (250Hz) handles slower position/angle tracking.
Related
- Open Loop vs Closed Loop — why feedback control works
- Digital Control — discrete PID implementation on a microcontroller
- Transfer Functions — PID in the Laplace domain:
Kp + Ki/s + Kd·s - Sensor Fusion — improving measurement quality fed to PID
- Drone Flight Dynamics — PID for attitude control