PID Tuning & Digital Implementation

Making PID work on real plants. Learn PID forms (ideal, parallel, series), tuning methods (Ziegler–Nichols open and closed loop, Cohen–Coon, IMC/lambda, SIMC), derivative filtering and derivative-on-measurement, setpoint weighting, integral windup and anti-windup, bumpless transfer, discretisation and sample time — with a full discrete PID simulator in Python comparing Z-N and SIMC tuning.

Over 90% of industrial control loops are PID. Yet surveys regularly find that many loops in real plants run in manual or are badly tuned, oscillating and wasting energy. Good tuning and a robust implementation are practical, valuable skills.

A textbook PID equation is not enough: real controllers need derivative filtering, anti-windup, bumpless manual/auto transfer, output limits and a sensible sample time. This lesson builds all of these.

An analogy

Tuning a PID is like adjusting a shower: P is how far you turn the knob for a given temperature error, I keeps nudging until it's exactly right, D reacts to how fast the temperature is changing — and the dead time is the delay before the water reaches you, which is what makes people over-correct.

What P, I and D do

P reacts to the present error (faster but leaves offset on its own). I accumulates past error and removes offset (but adds phase lag and can cause oscillation). D reacts to the rate of change, anticipating the future (adds damping, but amplifies noise). Most process loops use PI; D helps slow, lag-dominant processes like temperature.

Tuning methods

Choose by what you know about the process:

Implementation details

Derivative-on-measurement avoids 'derivative kick' on setpoint steps; setpoint weighting (b, c factors) softens proportional kick. A first-order filter on D (N = 8–20) limits noise amplification. Anti-windup (conditional integration or back-calculation) stops the integral growing while the valve is saturated. Bumpless transfer initialises the integral so switching manual → auto doesn't jump the output. Controller action (direct/reverse) must match the valve and process gain signs.

Digital control considerations

Controllers run in discrete time. Choose sample time T_s ≈ τ/10 or less (and ≤ θ/4); too slow adds effective delay T_s/2 and aliasing, too fast amplifies noise in D and wastes CPU. Discretisation via backward Euler or Tustin; the velocity (incremental) form naturally handles saturation and bumpless transfer. Use anti-alias filters on PVs, and watch for quantisation of small integral increments in fixed-point PLCs.

Formulas

Worked example: Z-N ultimate cycle

Given: Loop oscillates steadily at K_c = 4 with period 12 s

Find: PID settings.

Answer: K_c = 2.4, T_i = 6 s, T_d = 1.5 s

Worked example: Lambda tuning

Given: K = 2, τ = 10 s, θ = 2 s, λ = 10 s

Find: PI settings.

Answer: K_c = 0.42, T_i = 10 s (slow, very robust)

Worked example: Sample time

Given: Flow loop τ = 2 s, θ = 0.5 s

Find: Maximum sample time.

Answer: ≈ 0.1 s

Discrete PID with anti-windup — Ziegler–Nichols vs SIMC

# Discrete PID with derivative-on-measurement, filtered D and anti-windup,
# controlling a first-order-plus-dead-time process  G = K e^(-Ls)/(tau s + 1)
K, tau, L, dt = 2.0, 10.0, 2.0, 0.1
delay = [0.0] * int(L / dt)

def simulate(Kp, Ti, Td, steps=600, umax=100.0):
    y = 0.0; integ = 0.0; d_f = 0.0; y_prev = 0.0
    sp = 1.0; peak = 0.0; t_settle = None; hist = []
    for n in range(steps):
        e = sp - y
        d_raw = -(y - y_prev) / dt                 # derivative on measurement
        d_f += (dt / (Td / 10 + dt)) * (d_raw - d_f) if Td > 0 else 0.0
        u = Kp * (e + integ / Ti + Td * d_f)
        u_sat = max(-umax, min(umax, u))
        if u == u_sat:                             # conditional integration (anti-windup)
            integ += e * dt
        y_prev = y
        delay.append(u_sat); u_d = delay.pop(0)
        y += dt * (K * u_d - y) / tau             # plant
        peak = max(peak, y); hist.append(y)
    for n in range(len(hist) - 1, -1, -1):
        if abs(hist[n] - sp) > 0.02:
            t_settle = (n + 1) * dt; break
    return peak, t_settle, hist[-1]

# Ziegler-Nichols open-loop (reaction curve) and SIMC tuning
zn = (1.2 * tau / (K * L), 2 * L, 0.5 * L)
tc = L                                            # SIMC closed-loop time constant
simc = (tau / (K * (tc + L)), min(tau, 4 * (tc + L)), 0.0)
for name, (Kp, Ti, Td) in (("Z-N PID", zn), ("SIMC PI", simc)):
    delay[:] = [0.0] * int(L / dt)
    peak, ts, yend = simulate(Kp, Ti, Td)
    print(f"{name}: Kp={Kp:.2f} Ti={Ti:.1f} Td={Td:.1f} -> overshoot {max(0,(peak-1))*100:.0f}%, settle(2%) {ts:.1f} s, y(60 s)={yend:.3f}")
# Z-N PID: Kp=3.00 Ti=4.0 Td=1.0 -> overshoot 70%, settle(2%) 22.7 s, y(60 s)=1.000
# SIMC PI: Kp=1.25 Ti=10.0 Td=0.0 -> overshoot 5%, settle(2%) 12.3 s, y(60 s)=1.000

Ziegler–Nichols (1942) targets quarter-amplitude decay — aggressive, 70% overshoot here. Modern model-based rules (SIMC, lambda/IMC) give smooth, robust responses and settle faster. The implementation details (derivative on PV, filtered D with N = 10, conditional-integration anti-windup) are what industrial controllers do.

PID function block in Structured Text (illustrative)

FUNCTION_BLOCK FB_PID
VAR_INPUT
    sp, pv        : REAL;
    kc, ti, td    : REAL;          (* ti, td in seconds *)
    ts            : REAL := 0.1;   (* sample time, s *)
    out_min       : REAL := 0.0;
    out_max       : REAL := 100.0;
    manual        : BOOL;
    man_out       : REAL;
END_VAR
VAR_OUTPUT
    out           : REAL;
END_VAR
VAR
    integ, pv_prev, d_filt : REAL;
END_VAR
    IF manual THEN
        out := man_out;
        integ := out / kc - (sp - pv);          (* bumpless transfer back to auto *)
    ELSE
        d_filt := d_filt + ts / (td / 10.0 + ts) * (-(pv - pv_prev) / ts - d_filt);
        out := kc * ((sp - pv) + integ + td * d_filt);
        IF out > out_max THEN out := out_max;
        ELSIF out < out_min THEN out := out_min;
        ELSE integ := integ + (sp - pv) * ts / ti;   (* integrate only when not saturated *)
        END_IF;
    END_IF;
    pv_prev := pv;
END_FUNCTION_BLOCK

Illustrative IEC 61131-3 code (not compiled here) showing the essentials. In practice use the vendor's tested PID block (PID_Compact, PIDE, PID in Codesys) and set its options rather than writing your own — but knowing what's inside helps you configure and troubleshoot it.

Common mistakes

Applications

Key takeaways

Going further