PID tuning cheat sheet
A PID tuning recipe for robot joints, the Ziegler-Nichols rules and their limits, the discrete PID update with derivative filtering, anti-windup and setpoint weighting, and a simulation that compares them.
This sheet is for anyone bringing up a joint, a wheel or a gimbal loop who wants a tuning that is good enough to build on. It gives the controller form we recommend for code, a step-by-step recipe, the Ziegler-Nichols starting points with their limits, and one simulation that shows what each fix buys. For the concepts, start with the PID control lesson and the glossary entries PID controller and step response.
The controller you should write
| Form | Expression | Notes |
|---|---|---|
| Parallel (gains) | u = Kp e + Ki integral(e) + Kd de/dt | What most code and drives expose |
| Standard (times) | u = Kp (e + integral(e) / Ti + Td de/dt) | Ki = Kp / Ti, Kd = Kp Td. Tuning rules are written in this form |
| With setpoint weight | u = Kp (beta r - y) + Ki integral(r - y) - Kd dy/dt | beta between 0 and 1 shapes the reference response without changing load rejection. The integral must act on the full error |
Discrete version at sample time Ts, the one in the code below:
e_k = r_k - y_k
P_k = Kp (beta r_k - y_k)
D_k = a D_(k-1) - (1 - a) Kd (y_k - y_(k-1)) / Ts a = Tf / (Tf + Ts), Tf = Td / N
u_raw = P_k + I_k + D_k
u_k = clamp(u_raw, -u_max, u_max)
I_(k+1) = I_k + Ki Ts e_k unless u_k is saturated and e_k pushes further into saturation
Three details matter more than the gains.
- Derivative on the measurement, not the error. A step in r then gives no derivative kick.
- A low-pass filter on the derivative. Åström and Murray give Tf = Td / N with N between 5 and 20. The code uses N = 10.
- Anti-windup. Clamping (conditional integration) is shown here. Back-calculation feeds the saturation error back to the integrator through a gain, which Åström and Murray suggest choosing as a multiple of Ki.
A tuning recipe for a robot joint
- Check the sign. Command a small positive output and confirm the joint moves in the positive direction of its encoder. A wrong sign looks like instability at any gain.
- Set limits first: output saturation, a position window, a current limit in the drive. Tune with the real limits in place, because saturation changes the response.
- Start with P only. Raise Kp until the response is fast but has visibly too much overshoot, then halve it. If you need a number to start from, use the ultimate-gain test below.
- Add D to damp the overshoot. Raise Td until the overshoot drops or the motor gets noisy. Noise means N is too large or the encoder is too coarse for this much D.
- Add I last, for load rejection. Start with a long Ti (a slow integral) and shorten it until a load step recovers fast enough. Keep anti-windup on from the first test.
- If the reference response now overshoots but load recovery is right, lower beta rather than the gains. Robots usually follow a smooth trajectory, so a ramped reference does the same job.
- Retune at the extremes: full payload, arm extended, cold and hot. Gains tuned on a light, folded arm are optimistic.
Ziegler-Nichols
The two classic rules from 1942, as tabulated by Åström and Murray (Table 11.1).
| Controller | Step response method: Kp, Ti, Td | Ultimate gain method: Kp, Ti, Td |
|---|---|---|
| P | 1/a | 0.5 Ku |
| PI | 0.9/a, tau/0.3 | 0.45 Ku, Tu/1.2 |
| PID | 1.2/a, tau/0.5, 0.5 tau | 0.6 Ku, Tu/2, Tu/8 |
In the step response method, a and tau come from the steepest tangent of the open-loop step response: tau is the apparent delay and a/tau the steepest slope. In the ultimate gain method, you raise a P-only gain until the loop oscillates steadily. Ku is that gain and Tu the period of the oscillation.
Caveats before you use them on a robot:
- Åström and Murray name two drawbacks: the rules use too little process information, and the resulting loops lack robustness. Expect overshoot of tens of per cent, as in the simulation below.
- Driving a real arm to sustained oscillation can damage gears and hit end stops. Do it in simulation, on a blocked joint with tiny amplitude, or with a relay test that limits the amplitude.
- A position loop around a motor already contains an integrator. Integral action then adds overshoot to every reference step, so expect to detune I or use setpoint weighting.
- Sampling and filtering add delay, so the measured Ku on a digital loop is lower than the continuous-time value.
Simulation
A joint angle driven through a motor lag (0.1 s) and an amplifier lag (0.02 s), so G(s) = 1 / (s (0.1 s + 1)(0.02 s + 1)). The loop runs at 1 kHz with an output limit of 20. A constant load acts from t = 1.5 s. The block finds Ku and Tu by bisection on a P-only loop, applies Ziegler-Nichols, then compares anti-windup, setpoint weighting and a hand-detuned set. Overshoot and settling (2 % band) are measured on the reference step before the load arrives.
import numpy as np
DT = 0.001 # controller period (s), 1 kHz
T_M, T_A, K = 0.10, 0.02, 1.0 # motor and amplifier time constants (s), plant gain
U_MAX = 20.0 # actuator limit
LOAD = 2.0 # constant load disturbance applied from t = 1.5 s
def plant_step(x: np.ndarray, u: float, load: float, sub: int = 10) -> np.ndarray:
"""Joint angle: K / (s (T_M s + 1)(T_A s + 1)), load enters at the motor."""
pos, vel, drive = x
h = DT / sub
for _ in range(sub):
drive += h * (K * u - drive) / T_A
vel += h * (drive - load - vel) / T_M
pos += h * vel
return np.array([pos, vel, drive])
def make_pid(kp: float, ki: float, kd: float, tf: float, u_max: float, anti_windup: bool = True, beta: float = 1.0):
s = {"i": 0.0, "d": 0.0, "y_prev": None}
alpha = tf / (tf + DT) # first-order low-pass on the derivative
def step(r: float, y: float) -> float:
e = r - y
y_prev = y if s["y_prev"] is None else s["y_prev"]
s["d"] = alpha * s["d"] - (1 - alpha) * kd * (y - y_prev) / DT # derivative on measurement
s["y_prev"] = y
u_unsat = kp * (beta * r - y) + s["i"] + s["d"] # setpoint weight beta on P
u = float(np.clip(u_unsat, -u_max, u_max))
if not anti_windup or u == u_unsat or np.sign(e) != np.sign(u_unsat):
s["i"] += ki * e * DT # clamping: freeze I while saturated and pushing further
return u
return step
def simulate(pid, t_end: float = 3.0, r: float = 1.0, load_on: bool = True) -> np.ndarray:
x, ys = np.zeros(3), []
for k in range(int(t_end / DT)):
x = plant_step(x, pid(r, x[0]), LOAD if (load_on and k * DT >= 1.5) else 0.0)
ys.append(x[0])
return np.array(ys)
def growing(kp: float) -> bool:
y = simulate(make_pid(kp, 0, 0, 1e-3, 1e9), t_end=4.0, r=0.01, load_on=False) - 0.01
return np.abs(y[2000:]).max() >= np.abs(y[1000:2000]).max()
lo, hi = 1.0, 200.0
for _ in range(30): # bisection on P-only gain
mid = 0.5 * (lo + hi)
lo, hi = (lo, mid) if growing(mid) else (mid, hi)
ku = hi
y = simulate(make_pid(ku, 0, 0, 1e-3, 1e9), t_end=4.0, r=0.01, load_on=False) - 0.01
tu = np.diff(np.where(np.diff(np.sign(y[1000:])) > 0)[0]).mean() * DT
print(f"Ku = {ku:.1f} (continuous-time analytic {(T_M + T_A) / (T_M * T_A * K):.1f}), Tu = {tu:.3f} s")
def report(name: str, y: np.ndarray) -> None:
step = y[:1500]
outside = np.where(np.abs(step - 1.0) > 0.02)[0]
settle = (outside[-1] + 1) * DT if len(outside) else 0.0
print(f"{name:30s} overshoot {max(0.0, 100 * (step.max() - 1)):5.1f} % settling {settle:.2f} s "
f"error at 3 s {1 - y[-1]:+.4f} rad")
kp, ti, td = 0.6 * ku, 0.5 * tu, 0.125 * tu # classic Ziegler-Nichols PID
report("Z-N, no anti-windup", simulate(make_pid(kp, kp / ti, kp * td, td / 10, U_MAX, False)))
y_zn = simulate(make_pid(kp, kp / ti, kp * td, td / 10, U_MAX))
report("Z-N, clamping", y_zn)
report("Z-N, clamping, beta = 0", simulate(make_pid(kp, kp / ti, kp * td, td / 10, U_MAX, beta=0.0)))
kp2, ti2, td2 = 0.5 * ku, 1.0 * tu, 0.2 * tu # detuned by hand from the Z-N start
y_tuned = simulate(make_pid(kp2, kp2 / ti2, kp2 * td2, td2 / 10, U_MAX, beta=0.3))
report("tuned, clamping, beta = 0.3", y_tuned)
for name, y in (("Z-N", y_zn), ("tuned", y_tuned)):
print(name, [[round(k * DT, 2), round(float(y[k - 1]), 3)] for k in range(60, 3001, 60)])
Output, first lines:
Ku = 58.7 (continuous-time analytic 60.0), Tu = 0.284 s
Z-N, no anti-windup overshoot 83.8 % settling 1.07 s error at 3 s -0.0002 rad
Z-N, clamping overshoot 41.0 % settling 0.87 s error at 3 s -0.0002 rad
Z-N, clamping, beta = 0 overshoot 16.0 % settling 0.86 s error at 3 s -0.0002 rad
tuned, clamping, beta = 0.3 overshoot 0.0 % settling 0.64 s error at 3 s +0.0000 rad
The measured Ku is 58.7 against 60.0 for the continuous plant, because of the 1 ms sample delay. The output limit of 20 is below the initial proportional action (0.6 Ku times 1 rad is about 35), so the actuator saturates at the start of every step. Without anti-windup the integral keeps growing during saturation and overshoot doubles. Clamping fixes most of that. Setpoint weighting removes more without changing how the loop handles the load. The detuned set (0.5 Ku, Ti = Tu, Td = 0.2 Tu, beta = 0.3) reaches the 2 % band in 0.64 s with no overshoot and still removes the load error.
Symptoms and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Slow oscillation that grows after a large move | Integral windup | Anti-windup, or limit the integral state |
| Fast buzzing, hot motor, audible noise | Too much D, or N too large, or encoder quantisation | Lower Td or N, filter the measurement, raise the encoder resolution |
| Steady error that never goes away | No I, or I frozen by a clamp, or static friction | Add or check I, add friction feedforward |
| Small oscillation around the target | Static friction with integral action (stick-slip), or backlash | Deadband on I, friction compensation, reduce Ki |
| Overshoot only on reference steps, fine on load steps | Integral acting on a step reference | Setpoint weighting, or ramp the reference |
| Gains that worked last week now oscillate | Different payload, temperature, firmware loop rate | Retest at the extremes, gain schedule if needed |
Further reading
- K. J. Åström and R. M. Murray, Feedback Systems: An Introduction for Scientists and Engineers, second edition, Princeton University Press, free online edition. Chapter 11 covers PID tuning, windup and implementation.
- J. G. Ziegler and N. B. Nichols, “Optimum settings for automatic controllers”, 1942, doi:10.1115/1.4019264.
- Control hub and actuators and drives hub.
- Kalman filter cheat sheet for cleaning the measurement before it reaches D.