PID Derivative Filter: Kill the Derivative Kick Before It Kills Your Loop
Every PID loop has a dirty secret: the derivative term amplifies noise. A step change or a noisy measurement causes the D-term to spike — saturating the actuator, stressing the plant, and sometimes destabilizing the entire loop. The fix is a 1st-order IIR lowpass applied directly to the D-term. This post shows the exact digital coefficients, C implementation, and why you should almost never run a PID without it.
Transfer Function — H(z)
The PID derivative filter is a 1st-order IIR lowpass. Starting from the analog prototype:
$$H(s) = \frac{\omega_c}{s + \omega_c}$$
Applying the bilinear transform \(s \rightarrow \frac{2}{T}\cdot\frac{1-z^{-1}}{1+z^{-1}}\) with \(k = \omega_c T/2 = \pi f_c / f_s\):
Standard biquad form (b2 = a2 = 0 since 1st order):
$$H(z) = \frac{b_0 + b_1 z^{-1}}{1 + a_1 z^{-1}}$$
Coefficient derivation (fc = 1000 Hz, fs = 44100 Hz):
| Step | Expression | Value |
|---|---|---|
| \(k = \pi f_c / f_s\) | pre-warp factor | 0.071330 |
| \(b_{0,u}\) (unnorm.) | $k$ | 0.071330 |
| \(b_{1,u}\) (unnorm.) | $k$ | 0.071330 |
| \(a_{0,u}\) (unnorm.) | \(1 + k\) | 1.071330 |
| \(a_{1,u}\) (unnorm.) | \(k - 1\) | −0.928670 |
| b0 (normalized) | \(k/(1+k)\) | 0.066592 |
| b1 (normalized) | \(k/(1+k)\) | 0.066592 |
| a1 (normalized) | \((k-1)/(1+k)\) | −0.866816 |
Difference equation:
$$y[n] = b_0 \cdot x[n] + b_1 \cdot x[n-1] - a_1 \cdot y[n-1]$$
Frequency Response
Bode plot — magnitude (top) and phase (bottom) for fc = 1000 Hz, fs = 44100 Hz
Key observations from the Bode plot:
−3 dB at exactly 1000 Hz — bilinear transform places the -3 dB point precisely at fc
−20 dB/decade rolloff — first-order slope, sufficient to suppress switching-frequency noise (typically 10–100× fc)
−45° phase shift at fc — adds lag to the D-term; keep fc at least 5–10× the control bandwidth to minimize phase margin loss
−60 dB at 10× fc — noise at 10 kHz is attenuated 1000× in amplitude
Python Implementation
import numpy as np
from scipy.signal import lfilter
def pid_derivative_filter(fc: float, fs: float):
k = np.pi * fc / fs # bilinear pre-warp
a0 = 1.0 + k
b = [k / a0, k / a0] # b0, b1
a = [1.0, (k - 1.0) / a0] # a0=1, a1
return b, a
# Usage in PID loop:
fs = 10000.0 # 10 kHz control loop
fc = 500.0 # filter cutoff (≈ 10× bandwidth)
b, a = pid_derivative_filter(fc, fs)
error_history = [0.0] # ring buffer for filter state
def compute_d_term(error, dt, Kd):
derivative = (error - error_history[-1]) / dt
filtered_d = lfilter(b, a, [derivative])[0]
error_history.append(error)
return Kd * filtered_d
C Implementation — Direct Form II Transposed
typedef struct {
double b0, b1; /* numerator */
double a1; /* denominator (normalized, a0=1) */
double w1; /* delay state */
} DerivFilter;
void deriv_filter_init(DerivFilter *f, double fc, double fs) {
double k = M_PI * fc / fs;
f->b0 = k / (1.0 + k);
f->b1 = k / (1.0 + k);
f->a1 = (k - 1.0) / (1.0 + k);
f->w1 = 0.0;
}
/* Call once per control tick — Direct Form II Transposed */
double deriv_filter_tick(DerivFilter *f, double x) {
double y = f->b0 * x + f->w1;
f->w1 = f->b1 * x - f->a1 * y;
return y;
}
Fixed-point (Q15, 16-bit MCU): Scale coefficients by 2¹⁵:
b0_q15 = b1_q15 = 2181
a1_q15 = −28406
Use 32-bit accumulator; shift right 15 bits after each multiply-accumulate.
MATLAB Implementation
fc = 1000; fs = 44100;
k = pi * fc / fs;
b = [k, k] / (1 + k); % [b0, b1]
a = [1, (k-1)/(1+k)]; % [1, a1]
freqz(b, a, 8192, fs); % Bode plot
zplane(b, a); % single real pole at z = a1
The single pole sits at z = 0.8668 — well inside the unit circle for any fc < fs/2.
Design Trade-offs
| Parameter | Effect |
|---|---|
| fc too low | Excessive D-term lag → phase margin loss → possible instability |
| fc too high | Noise passes through → actuator chatter, thermal stress |
| Rule of thumb | Set fc = 5–10× control bandwidth |
| fs dependency | Lower sample rates push the pole closer to z=1; recalculate k each time |
Coefficient sensitivity: The bilinear transform gives excellent numerical properties for a 1st-order section. No second-order sensitivity issues; safe down to 16-bit fixed-point without scaling adjustments.
Key Takeaways
The PID derivative filter is a 1st-order IIR lowpass applied to the D-term; same topology as a digital RC filter
Bilinear transform with \(k = \pi f_c / f_s\) gives exact -3 dB placement at fc without frequency pre-warping tables
Direct Form II Transposed requires only one delay register — minimal memory, cache-friendly for microcontrollers
Keep fc ≥ 5× control bandwidth to avoid eating into your phase margin
Q15 fixed-point is viable: b0 = b1 = 2181/32768, a1 = −28406/32768
Related Posts
First-Order IIR Lowpass: The Universal Pre-Filter Every Control Loop Needs — same filter topology; covers EMA/RC equivalence and PID pre-filtering
Notch Filter 2nd Order: Surgically Remove a Single Frequency Without Touching the Rest — pairs with derivative filter to kill mechanical resonances in the D-path
Engineering question: In your control loops, do you set the derivative filter cutoff by rule-of-thumb (e.g., 10× bandwidth) or do you tune it empirically against the actual noise floor? What's your go-to method for balancing D-term effectiveness against noise attenuation?
