PID Control, Tuning & Anti-Windup
The three-term control law and everything the textbook equation leaves out — derivative on the measurement instead of the error, a filtered derivative, integral windup and the anti-windup schemes that fix it, Ziegler-Nichols with its real coefficients and its real weaknesses, and how to pick a sample rate.
PID is the default answer to feedback control and has been for eighty years: Åström puts it at more than 95% of industrial control loops, most of them actually PI. The law itself is three lines of arithmetic. Everything that makes a PID controller work — where the derivative is taken, how it is filtered, what the integrator does when the actuator is pinned at its limit, how fast you sample — is not in that equation, and a faithful implementation of the textbook formula is a bad controller. This entry is mostly about the parts that are not in the formula.
The law, and what each term buys #
The non-interacting ("standard", "ISA") form:
with the setpoint, the measurement, the proportional gain, the integral time and the derivative time. The equivalent parallel form, which is what you actually implement because the parameters enter linearly and each term can be switched off with a finite value:
| Term | What it fixes | What it costs |
|---|---|---|
| P | Reduces error proportionally; raising shrinks it | Never removes steady-state error; raising increases the tendency to oscillate |
| I | Kills steady-state error exactly, whatever the load | Adds phase lag; oscillation tendency grows as shrinks; introduces windup |
| D | Adds damping and phase lead (up to ~50°) | Amplifies measurement noise without bound unless filtered; useless on delay-dominated processes |
A third parameterization, the interacting (series) form , is common in commercial controllers. It is not the same controller: an interacting equivalent of a non-interacting one exists only when . If you copy tuning constants between two vendors' boxes without checking which form they implement, you have changed the controller.
Converting between the forms
and, in the other direction (only when ):
Derivative on the measurement, and always filtered #
Two independent problems, both fatal, both fixed at the same place in the code.
Derivative kick. A step in the setpoint produces a step in , hence an impulse in , hence a spike in that is limited only by the actuator. Nobody wants that. The fix is setpoint weighting: let the P and D terms act on weighted fractions of the reference,
The integral term must stay on the true error, or steady-state accuracy is gone. In practice always — that is what "derivative on measurement" means, and it removes the kick entirely. is the useful knob: it is a second degree of freedom that changes setpoint response without touching the load-disturbance and noise response. Overshoot to a setpoint step is smallest at (the I-PD controller) and grows with . Tune , , for disturbances first; then set for the reference response.
Noise amplification. Differentiating turns a signal-to-noise ratio of into — unbounded as grows. So the ideal derivative is never implemented. Replace it with
an ideal derivative cascaded with a first-order filter of time constant . High-frequency gain of the D-term is now capped at instead of infinite. Typical is 5–20 (Åström & Murray say 5–20, Åström's Ch. 6 says 8–20); is the usual default.
Discretizing it: positional and incremental #
Two forms, both used in production, with genuinely different operational properties.
Positional (direct) form — the controller outputs the absolute command. Integral by forward rectangular rule, derivative by backward difference:
The backward difference is not an aesthetic choice: it makes the pole coefficient land in for every , so the difference equation is unconditionally stable. A forward difference there can be unstable under discretization for large .
Incremental (velocity) form — the controller outputs and the actuator (or a trailing integrator) accumulates it: , with
| Positional | Incremental / velocity | |
|---|---|---|
| Controller state | explicit integrator I | the output u itself |
| Anti-windup | needs an explicit scheme | clamping u to the limits is anti-windup |
| Bumpless transfer & parameter changes | must detect the change and patch I | falls out for free — only increments are emitted |
| P-only / PD controllers | fine | impossible — no integral means no stationary value |
| Arithmetic precision | full precision needed throughout | increments are small; short word length is enough |
The incremental form's inability to express a P or PD controller is the real constraint. A velocity-form proportional controller reaches stationarity at any error, because a differentiator's output is zero for any constant input. If your loop needs no integral action, use the positional form with an explicit bias term (the control signal corresponding to zero error) — otherwise the steady-state error is zero only at .
Integral windup #
When the actuator saturates, the feedback loop is open: the plant input no longer responds to the controller output. The error stays non-zero, and the integrator keeps integrating it. The integral state grows without bound — it "winds up" — and the command stays past the saturation limit long after the error has changed sign. Recovery requires an equally long period of opposite-sign error just to unwind the state. The visible symptom is a large overshoot followed by a slow, bang-bang-looking oscillation as the command flips between the limits.
Any controller with integral action can wind up. Setpoint changes are the classic trigger, but so are load disturbances, equipment faults, feedforward signals added downstream of the controller, and a controller left running while a selector gave the actuator to somebody else.
flowchart LR R["Setpoint r"] --> ES((sum)) Y["Measurement y"] --> ES ES --> I["Integrator ki/s"] R --> P["P term, weight b"] Y --> P Y --> D["Filtered derivative on y"] P --> S((sum)) I --> S D --> S S --> V["v: nominal command"] V --> SAT["Actuator limits"] SAT --> U["u: applied command"] U --> AW["es = u - v, gain 1/Tt"] V --> AW AW --> I
Back-calculation (tracking) #
Feed the saturation error (applied minus nominal command) back into the integrator through a gain . When nothing saturates, and the extra path does nothing at all. When the actuator saturates, the integrator input becomes
so the nominal command settles just outside the limit rather than far past it, and responds the instant the error changes sign. is the tracking time constant: it sets how fast the integrator is reset.
The other two schemes #
- Clamping (control-signal clamping). Saturate the output and store the saturated value as the state. In the incremental form this is the whole implementation — it is what you get for free. It is also exactly back-calculation with .
- Integrator clamping / conditional integration. Freeze the integrator () whenever the output is saturated. The refined version is better and worth the few extra lines: (1) integrate to saturation — if only the increment pushes the output over the limit, reduce the increment so it lands exactly on the limit rather than dropping it; (2) allow integration in the right direction — while saturated high, block but permit . Applying this clamp before back-calculation makes the saturation error decay exactly at instead of at a rate shifted by the increment.
A correct discrete PID #
import math
class PID:
"""Positional PID: setpoint weighting, filtered derivative on the
measurement, back-calculation anti-windup. Fixed sample period h."""
def __init__(self, kp, ki, kd, h, u_min, u_max, N=10.0, b=1.0):
if ki <= 0.0:
raise ValueError("no integral action; use a P/PD form with a bias term")
if not u_min < u_max:
raise ValueError("actuator limits must bracket a non-empty range")
Ti = kp / ki
Td = kd / kp
Tf = Td / N
Tt = math.sqrt(Ti * Td) if Td > 0.0 else Ti
if Td > 0.0 and Tf < 2.0 * h:
raise ValueError(f"sample period {h} too coarse for filter Tf={Tf}")
self.kp, self.kd, self.b = kp, kd, b
self.u_min, self.u_max = u_min, u_max
self.bi = ki * h # integral gain per step
self.ad = Tf / (Tf + h) # backward difference: 0 <= ad < 1 for all h
self.bd = kd / (Tf + h)
self.br = h / Tt # anti-windup reset gain
self.integral = 0.0
self.deriv = 0.0
self.y_prev = None
def step(self, r, y):
if self.y_prev is None:
self.y_prev = y
p = self.kp * (self.b * r - y)
self.deriv = self.ad * self.deriv - self.bd * (y - self.y_prev)
v = p + self.integral + self.deriv # nominal command
u = min(max(v, self.u_min), self.u_max) # actuator model
self.integral += self.bi * (r - y) + self.br * (u - v)
self.y_prev = y
return uNote the ordering: the output is computed and applied before the states are updated, which is the sequence that minimizes latency (read input → compute → write output → update state). Both the derivative and the anti-windup path read and from this cycle only. The Tf < 2h check is the one non-obvious guard — see below.
Choosing the sample rate #
Sampling is pure loss: it buys nothing and adds dead time. With the ideal loop ordering above, the average measurement delay is and the computation is usually negligible, so the loop inherits roughly of extra delay. Most controllers and instrument systems do not order their calculations that way and end up with several sample periods of delay.
A delay of costs phase margin radians at crossover. Working that arithmetic against the range of commonly quoted in the sampled-data literature:
The range itself is convention rather than a result — treat the phase-margin cost as the real constraint and the range as a starting point. Expressed against the ultimate period (with , and crossover typically near ), that is roughly 13 to 42 samples per ultimate period — a useful number because is exactly what the tuning experiment below measures. Faster is not free either: the derivative's noise amplification is , inversely proportional to , so shrinking makes the measurement filter more necessary, not less.
Three further constraints:
- Anti-aliasing is mandatory, in analog, before the ADC. Aliasing does not merely lose high-frequency content; it folds it down into the control bandwidth where the loop will faithfully act on it. A second-order Butterworth is the common choice. Åström's sizing example: to attenuate by 16 at with a second-order filter, solve , giving . The prefilter's own dynamics are significant and belong in the plant model for design purposes.
- The derivative filter must be resolvable at . In , if then and the recursion degenerates into a raw first difference with gain — the filter you specified does not exist. Keep at least a small multiple of ; that is what the guard in the code above enforces.
- Jitter. Fixed is the normal assumption and small jitter is normally ignored. Where jitter is large or deliberate (event-based control), compute as the actual elapsed time — and remember that a changed also rescales any rate limits expressed per sample.
Tuning #
Ziegler-Nichols, with the real numbers #
Two experiments, published in 1942, and still the reference point everyone argues against.
Frequency-response (ultimate-gain) method. Set , , raise until the loop oscillates with constant amplitude. That gain is the ultimate gain and the oscillation period is the ultimate period ; by Nyquist, the experiment has located the point where the process has exactly 180° of phase lag, at .
| Controller | |||
|---|---|---|---|
| P | — | — | |
| PI | — | ||
| PID |
Step-response (bump test) method. Drive the process open-loop with a step, draw the tangent at the point of steepest slope, and read off its intercepts: (apparent dead time, the time-axis intercept) and (the value-axis intercept for a unit step).
| Controller | |||
|---|---|---|---|
| P | — | — | |
| PI | — | ||
| PID |
Softer variants of the frequency-response rules circulate widely (a "no overshoot" variant at , , ; Pessen's integral rule at , , ). They are the same experiment with different constants and the same fundamental limitation: too little information about the process.
Relay auto-tuning #
Running a proportional loop up to sustained oscillation is unpleasant on real equipment — you are deliberately driving the plant to its stability boundary and hoping the amplitude stays bounded. Åström and Hägglund's 1984 alternative replaces the proportional controller with a relay of amplitude . The relay-plus-plant loop settles into a limit cycle automatically, at the frequency where the plant has 180° of lag. Describing-function analysis of the ideal relay (keeping only the fundamental of the square wave) gives
where is the measured amplitude of the output oscillation. The experiment is short, self-limiting (amplitude is controlled by adjusting ), and automatable — push a button, the controller runs the relay test, computes and , and installs a PID. This is what "autotune" means on most commercial controllers.
The catch is the describing-function approximation: it discards all harmonics above the fundamental, and the resulting error in is typically 5–20% for common process transfer functions. The frequency estimate is considerably better than the amplitude estimate. A relay with hysteresis has describing function , which lets a sweep of identify points at known phase shifts rather than only the 180° point.
Manual loop shaping #
The order that actually works, and matches how the terms interact:
- P alone. , . Raise until the response is as fast as you can accept, then back off. Accept the steady-state offset for now.
- Add I. Reduce from a large value until steady-state error is removed at an acceptable settling time. Oscillation tendency rises as falls; when it starts oscillating you have gone too far. Most loops stop here — most industrial loops are PI.
- Add D only if it helps. Derivative action is worth adding for lag-dominated processes (normalized delay near 0) and is close to worthless for delay-dominated ones ( near 1). Increase to damp the oscillation; damping improves, then gets worse again once exceeds roughly .
- Then set . Only after the disturbance response is right, reduce from 1 toward 0 to trim setpoint overshoot. It cannot destabilize the loop — it is outside the feedback path.
PID in software systems #
Feedback controllers show up in infrastructure code far more often than they are labelled as such, and the ones that are labelled are worth reading.
PIE (RFC 8033) is an active queue management scheme that is explicitly a PI controller on queueing latency. Every T_UPDATE it computes an increment to the packet drop probability:
p = alpha * (current_qdelay - QDELAY_REF) +
beta * (current_qdelay - PIE->qdelay_old_);
PIE->drop_prob_ += p;with QDELAY_REF = 15 ms (the setpoint), T_UPDATE = 15 ms (the sample period), alpha = 1/8 Hz and beta = 1 + 1/4 Hz. This is exactly the incremental form from above: the accumulated state is the output (drop_prob_) rather than a separate integrator, the alpha term is the integral action on the error, and the beta term — the trend, — is the proportional action. The RFC's own scaling rule confirms the reading: "if we cut T_UPDATE in half, we should also cut alpha by half", which is what does. The bound is clamping anti-windup, and because clamping alone leaves residual state after a congestion episode ends, PIE adds an explicit exponential decay (×0.98 per update, a time constant of about T_UPDATE) to unwind it. RFC 8033 also documents the deliberate detuning: alpha and beta are divided by up to 2048 at low drop probabilities, a gain schedule on the operating point.
Kubernetes HPA is the instructive counter-example — a loop that deliberately refuses integral action:
That is proportional control with a ratio instead of a difference, and nothing else. There is no integrator because the plant — pods starting, warming up, joining a load balancer — has long, variable, poorly-known dead time, and integral action against unmodelled dead time is how you get sustained oscillation. Instead the loop is stabilized the way delay-dominated loops usually are: a tolerance deadband (default 0.1 — no action while the ratio is within 10% of 1.0) and a stabilization window on scale-down (default 300 s, taking the maximum recommendation over the window, a rolling max that refuses to act on transients). Deadband plus rate limiting instead of derivative damping, and no integrator at all.
Sources
- bookÅström — Control System Design, Ch. 6: PID Control (Caltech CDS 101 notes)
- bookÅström & Murray — Feedback Systems, 2nd ed., Ch. 11: PID Control
- paperSundström, Bauer, Guzmán, Hägglund & Soltesz — A Practical Guide to PID Controller Implementation (arXiv 2604.15918)
- docsRFC 8033 — Proportional Integral Controller Enhanced (PIE)
- docsKubernetes — Horizontal Pod Autoscaling
- articleHornsey — A Review of Relay Auto-tuning Methods for the Tuning of PID-type Controllers
- articleZiegler–Nichols method (tuning-rule variants table)