A first-order plant with a disturbance nobody asked for
A car's longitudinal dynamics, for cruise-control purposes, reduce to one simple force balance: mass times acceleration equals engine thrust minus aerodynamic drag minus the pull of gravity on a slope.
m v̇ = F_engine(u) - c·v² - m g sin(θ_hill)
Drag makes this mildly nonlinear, but the important structural fact for a controller designer is simpler: the hill term is an unmeasured disturbance entering the same equation as the control input. A controller that doesn't explicitly account for it will, in general, settle at some speed error whenever θ_hill ≠ 0 — and how much error depends entirely on which controller you pick.
Why proportional control alone leaves you short on a hill
A pure proportional controller sets throttle from the speed error: u = Kp·(v_set - v). On flat ground this converges to some working point. Add a hill, and at steady state the car needs a nonzero throttle just to counteract gravity — but a proportional controller can only produce a nonzero output from a nonzero error. So the car settles below v_set, by exactly the error needed to generate the throttle the hill demands: a permanent offset, sometimes called droop. Raising Kp shrinks that offset but never eliminates it, and pushing Kp too high trades the offset for oscillation and actuator chatter instead.
Why adding an integral term fixes it structurally
The integral term accumulates error over time and keeps growing the throttle command for as long as any error remains: u = Kp·e + Ki·∫e dt. At true steady state the throttle must stop changing, and the only way the integral term stops changing is for e itself to reach exactly zero. That's the structural guarantee a PI controller gives you that P alone cannot: zero steady-state error to a constant disturbance, regardless of how large Kp is or how steep the hill is (short of actuator saturation) — a direct consequence of the plant having no integrator of its own, so the controller has to supply one.
u(t) = Kp·e(t) + Ki·∫₀ᵗ e(τ) dτ, e = v_set - v
P only: settles with a nonzero offset proportional to disturbance size
PI: offset is driven to exactly zero, because only e = 0 lets
the integral term stop growing
The integral term's dark side: windup
Every real throttle actuator saturates — it cannot exceed 100% open. Send the car up a hill steep enough, and the error stays stubbornly positive even at full throttle, because the engine simply cannot supply enough force yet. The integral term, blind to saturation, keeps accumulating that positive error the entire time the actuator is pinned at its limit. By the time the car finally crests the hill and gravity's disturbance vanishes, the integral term has wound up to a value far larger than it needs — and it has to unwind all the way back down before the throttle command drops to what the flat road actually requires. The visible symptom is a large, sluggish overshoot in speed right after the crest: the controller keeps commanding near-full throttle for a noticeable stretch of flat road, purely because of accumulated integral history that has nothing to do with the current error.
Anti-windup: telling the integrator the truth about the actuator
The fix is always some version of letting the integrator know when the actuator has stopped listening. Three standard techniques, in increasing order of how directly they intervene:
clamping (conditional integration)
freeze the integral term whenever the actuator is saturated AND
the error would push it further into saturation — cheapest fix,
needs only a saturation flag, no extra plant knowledge
back-calculation
feed the difference between the commanded and the actually-applied
(saturated) control signal back into the integrator with its own
gain, so the integral state is actively pulled toward whatever
value it should have had given the real, clamped output
observer-based / model-based anti-windup
reconstruct what state the plant would be in given the saturated
input, rather than the unsaturated one, and integrate against that
— most accurate, but needs a decent plant model to be worth it
Back-calculation is the most widely used compromise in production automotive and industrial control: it costs one extra gain and one extra feedback path, and it makes the integrator track reality — namely, that the plant only ever saw the clamped throttle command — instead of the fantasy command the unsaturated math wanted to send.
Where the design still needs judgment
None of this is free of trade-offs. A large Ki eliminates offset faster but windsup faster too when saturation hits, and interacts with Kp to set the closed-loop's damping — push both too high and the same integral action that erased the hill offset starts to overshoot and ring even without ever saturating. Real cruise-control tuning is done against a family of disturbances (grade changes, headwind, load), not a single step, and anti-windup gains are chosen so recovery from a saturated climb is fast without making the unsaturated, everyday response twitchy.
Frequently asked questions
Why does a P-only cruise control never quite reach the set speed on a hill?
Because a proportional controller can only output a nonzero throttle command in response to a nonzero speed error, but climbing a hill at constant speed requires a permanently nonzero throttle to counteract gravity. The controller settles at whatever speed error generates exactly that throttle — a steady offset that shrinks but never disappears as you raise the gain.
What causes integral windup?
It happens when the control error stays the same sign for a sustained period while the actuator is saturated (e.g. full throttle on a steep climb) — the integral term keeps accumulating that error even though the actuator can't act on the extra command, so it grows far beyond the value it needs once conditions return to normal, causing a slow, sluggish overshoot when it finally unwinds.
How does anti-windup fix the overshoot?
By stopping the integral term from accumulating error it can't act on. Clamping simply freezes integration during saturation; back-calculation actively feeds the gap between the commanded and the actually-applied (saturated) signal back into the integrator, pulling its internal state toward what it should be given the real, limited actuator output.
Try it live
Everything above runs in your browser — open Cruise Control PI and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open Cruise Control PI simulation