Learning Goals 5 min
Bang-bang control overshoots and oscillates. PID — Proportional + Integral + Derivative — is the smoother controller used in cruise control, drones, 3D printer heaters, balancing robots, industrial process control. The same three-term formula has been industry standard for over a century. By the end of this lesson you will:
- Explain what each of the three terms (P, I, D) contributes intuitively.
- Implement a basic PID controller in Arduino C, with non-blocking sample timing.
- Tune the three gains (Kp, Ki, Kd) using the "manual" method — and know when to reach for the PID_v1 / QuickPID libraries.
Warm-Up 10 min
No hardware. We'll think through the algorithm conceptually, then code it.
The intuition
PID looks at three things:
- P (proportional): how big is the error now? Bigger error → bigger reaction.
- I (integral): how much error has accumulated over time? Persistent small offsets eventually trigger a response.
- D (derivative): how fast is the error changing? Damps overshoot — if you're approaching the target fast, ease off.
Output = Kp·P + Ki·I + Kd·D, where each K is a tunable gain.
New Concept · PID in code 25 min
The minimal PID class
class PID {
public:
PID(float kp, float ki, float kd)
: kp_(kp), ki_(ki), kd_(kd),
integral_(0), lastError_(0), lastTime_(0) {}
float update(float setpoint, float measurement) {
unsigned long now = millis();
if (lastTime_ == 0) { lastTime_ = now; return 0; }
float dt = (now - lastTime_) / 1000.0;
if (dt <= 0) return 0;
lastTime_ = now;
float error = setpoint - measurement;
integral_ += error * dt;
float derivative = (error - lastError_) / dt;
lastError_ = error;
return kp_ * error + ki_ * integral_ + kd_ * derivative;
}
void setGains(float kp, float ki, float kd) { kp_ = kp; ki_ = ki; kd_ = kd; }
void reset() { integral_ = 0; lastError_ = 0; lastTime_ = 0; }
private:
float kp_, ki_, kd_;
float integral_, lastError_;
unsigned long lastTime_;
};One update(setpoint, measurement) call per loop iteration. Returns a control output number.
Using it for a temperature controller
PID tempPID(2.0, 0.5, 1.0); // initial gains
void loop() {
float t = readTemp();
float u = tempPID.update(80.0, t); // setpoint 80 °C
int duty = constrain((int)u, 0, 255);
analogWrite(HEATER_PIN, duty);
delay(100);
}The output u can be any number — clamp to your actuator's range. For a heater (PWM 0..255), use constrain. For a bidirectional motor, the sign of u = direction.
Tuning method 1 — manual
Standard practice:
- Start with Kp small, Ki = 0, Kd = 0.
- Increase Kp until the system oscillates. Halve.
- Add a little Kd to dampen overshoot.
- Add Ki if there's persistent steady-state error.
Tuning method 2 — Ziegler-Nichols
Engineering classic. Set Ki = Kd = 0. Increase Kp until you get sustained oscillation. Note the gain (Ku) and period (Tu). Then:
- P only: Kp = 0.5 Ku.
- PI: Kp = 0.45 Ku, Ki = 1.2 Kp / Tu.
- PID: Kp = 0.6 Ku, Ki = 2 Kp / Tu, Kd = Kp Tu / 8.
Heuristic, not optimal, but a fast first cut. Tune up from there.
Anti-windup
If the actuator is saturated (e.g. motor at 100% duty but still not reaching setpoint), the integral keeps growing indefinitely. When the error finally drops, the accumulated integral causes a long overshoot. The fix is "anti-windup": stop accumulating integral while the actuator is saturated.
if (output > MAX_OUT) { output = MAX_OUT; /* don't grow integral */ }
else if (output < MIN_OUT) { output = MIN_OUT; }
else { integral_ += error * dt; }Real libraries (PID_v1, QuickPID) handle anti-windup, output limits, and derivative-on-measurement (a refinement that ignores setpoint changes in the D term) for you.
Worked Example · PID-controlled fan speed 25 min
You want to hold a fan at a target speed (in RPM) regardless of how dusty it is. Open-loop: analogWrite a duty cycle. Closed-loop: read a tachometer pulse from the fan, measure RPM, PID controls duty.
Pseudo-build
- Fan with 4-wire connector (PWM + tach input).
- PWM out from Arduino → fan's PWM pin.
- Tach pulse counted via interrupt on D2.
- PID at 10 Hz.
Sketch outline
#include "PID.h"
const int PWM_PIN = 9;
const int TACH_PIN = 2;
volatile unsigned int pulses = 0;
PID pid(0.5, 0.2, 0.05);
void onPulse() { pulses++; }
void setup() {
Serial.begin(115200);
pinMode(PWM_PIN, OUTPUT);
pinMode(TACH_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(TACH_PIN), onPulse, RISING);
}
void loop() {
static unsigned long lastSample = 0;
unsigned long now = millis();
if (now - lastSample < 100) return;
float dt = (now - lastSample) / 1000.0;
lastSample = now;
noInterrupts();
unsigned int p = pulses;
pulses = 0;
interrupts();
// 4-wire PC fans give 2 pulses per revolution
float rpm = (p / dt) * 60.0 / 2.0;
float u = pid.update(2000.0, rpm);
int duty = constrain((int)u, 0, 255);
analogWrite(PWM_PIN, duty);
Serial.print("rpm="); Serial.print(rpm);
Serial.print(" duty="); Serial.println(duty);
}Tuning the fan
- Run with Ki = Kd = 0. Increase Kp: 0.2 → 0.5 → 1.0. Watch for the moment it oscillates around 2000 RPM.
- Halve Kp. Now smooth but probably 50 RPM off setpoint.
- Add Ki = 0.1. Steady-state error should slowly fade.
- Add Kd = 0.05. Tames overshoot when setpoint changes quickly.
Final tuning depends on the fan + sensor + sample rate. Plot the RPM vs setpoint over a few minutes to see the response.
Try It Yourself 15 min
Goal: Hand-trace the PID output for a heater starting at 22 °C with setpoint 50 °C and Kp = 5, Ki = 0, Kd = 0. Compute the first 3 outputs over dt = 1 s each (assume temperature rises 1 °C per output of 50 in 1 s — a hypothetical model).
Reveal
t=0: temp 22. Error 28. Output = 5×28 = 140. Heater applies 140. Temp rises by 140/50 = 2.8 °C → 24.8.
t=1: temp 24.8. Error 25.2. Output 126. Temp rises 2.52 °C → 27.32.
t=2: temp 27.32. Error 22.68. Output 113.4. Temp rises 2.27 → 29.59.
Approaches setpoint asymptotically but never quite reaches (proportional only has steady-state offset).
Goal: Install the PID_v1 library (Brett Beauregard's industry-standard one). Replace your hand-rolled PID class. Note the API differences.
Goal: Add a Serial-tunable PID: type kp 2.0 / ki 0.5 / kd 0.1 in the monitor to adjust gains live. Plot the response with the Arduino Serial Plotter while you tune. The classic engineer's workflow.
Mini-Challenge · Identify P / I / D contributions 10 min
For each scenario, name which term is most responsible:
- System overshoots target by 20 % then settles.
- System never quite reaches target — sits at 95 % of setpoint forever.
- System oscillates rapidly around target.
- System reacts sluggishly to a sudden setpoint change.
Reveal
- Kp too high OR Kd too low. Add D to dampen.
- Pure P controller — no I term to chase out the steady-state error. Add Ki.
- Kp way too high. Halve it.
- Kp too low. Increase. Also check loop period — too-slow sample causes sluggishness.
Recap 5 min
PID = three terms acting on the error and its derivatives. Tune in this order: Kp, Kd, Ki. Anti-windup the integral. Smooth noisy measurements before computing D. For production, use PID_v1 / QuickPID library. PID lives in cruise control, drones, 3D printer heaters, factory floor regulators. Tomorrow we apply PID to a real moving target — line-following.
- PID
- Proportional + Integral + Derivative controller. The most widely-used closed-loop control algorithm.
- Proportional (P)
- Output proportional to current error. Gain Kp. Bigger gain = faster but more overshoot.
- Integral (I)
- Accumulates error over time. Eliminates steady-state offset. Gain Ki.
- Derivative (D)
- Reacts to how fast error is changing. Damps overshoot. Gain Kd. Amplifies sensor noise.
- Setpoint, error, output
- Setpoint = target; error = setpoint − measurement; output = controller's action signal.
- Anti-windup
- Preventing the integral term from growing while the actuator is saturated. Avoids long overshoots after disturbances.
- Ziegler-Nichols tuning
- Classic heuristic for setting Kp/Ki/Kd from an oscillation test.
- Sample period
- How often the controller runs. Should be much faster than the system's natural time constant.
- PID_v1 library
- Brett Beauregard's industry-standard Arduino PID library. Handles dt, anti-windup, modes.
Homework 5 min
- Code the §3 PID class. Save in your reusable library folder.
- If you have a fan with tachometer, build the §4 controller. Otherwise simulate with two pots: one as "measurement", one as "setpoint", output to an LED's brightness. Tune the gains.
- Read ahead to ARD-L04-25 (Line-Following Sensors). Bring IR reflectance sensors (TCRT5000 × 3 or a QTR sensor array).