Skip to main content

Command Palette

Search for a command to run...

PID Derivative Filter: Kill the Derivative Kick Before It Kills Your Loop

Updated
5 min read

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.

https://youtu.be/30VEgpe-fpQ


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

PID Derivative Filter Bode Plot

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



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?

More from this blog

S

SW related to Power electronics

18 posts

SW related to Power electronics