Filters, PID and Signal Code

Moving average and exponential filters, discrete PID with anti-windup, sampling, the Fourier transform and tuning by simulation.

Signals, samples and sample rate

Real signals are noisy and arrive as samples at fixed times. Every filter and controller assumes equal time steps Δt, so drive the code from a timer, not from a loop that runs “as fast as it can” (see Embedded Systems: Timers). The sample rate must be more than twice the highest frequency in the signal (the Nyquist rate), or higher frequencies fold back as false lower ones (aliasing).

Worked example: aliasing

A 130 Hz vibration sampled at 200 Hz (Nyquist limit 100 Hz) appears at |130 − 200| = 70 Hz: a slow oscillation that is not there. Filter the signal with an analog low-pass before sampling, or sample faster; no code can remove an alias afterwards. In practice sample at 5 to 10 times the highest frequency of interest (see Embedded Systems: Analog Input).

The exponential filter

The simplest useful filter is the exponential moving average (a first-order low-pass), which needs only one stored value:

ynew = y + α × (x − y)    α = Δt ÷ (τ + Δt)

x is the new reading, y the filtered value, Δt the time between samples and τ the filter time constant.

Filtering a noisy ADC signal02040608010002505007501000sample numberADC countsraw samples8-sample moving averageexponential filter, α = 0.15The spike at sample 83 is spread into the averages; a median filter would remove it completely.
A noisy step filtered by an 8-sample moving average and an exponential filter (α = 0.15).

Worked example: choosing α

  • Sampling every 0.10 s with τ = 1.0 s gives α = 0.10 ÷ 1.1 = 0.0909: each new reading moves the output about 9 % of the way to it. After a step in the input the output reaches 63 % in about τ, and 95 % after 32 samples (3.2 s, about 3 τ).
  • The cut-off frequency of the filter is fc = −ln(1 − α) ÷ (2π Δt). At 1 kHz (Δt = 0.001 s) with α = 0.1: fc = 0.105 ÷ (2π × 0.001) = 16.8 Hz, a time constant of 9.5 ms, and random noise reduced to √(α ÷ (2 − α)) = 0.229 of its size.
  • The trade-off is unavoidable: more smoothing means more delay. If the signal must not lag (a control loop), filter as little as you can; if it is only displayed or logged, filter as much as you like.
  • Initialize the filter with the first reading (y = x), not zero, or the output climbs from zero.
class Ema:
    def __init__(self, dt, tau):
        self.alpha = dt / (tau + dt)
        self.y = None
    def update(self, x):
        self.y = x if self.y is None else self.y + self.alpha * (x - self.y)
        return self.y

/* C, integers: alpha = 1/8, y kept with 4 extra fractional bits */
y16 += ((int32_t)x * 16 - y16) >> 3;

Moving average and median filters

FilterWhat it doesCostUse
Moving average of NMean of the last N samples; noise falls as 1 ÷ √N; delay of (N − 1) ÷ 2 samplesN values of memorySteady signals with random noise; averaging over whole mains cycles removes hum
ExponentialThe filter aboveOne variableThe default for sensors and displays
Median of 3 or 5The middle value of the last readingsA small sortRemoves single-sample spikes without blurring edges
Median, then averageReject outliers, then smoothBothNoisy industrial signals
Rate limiterLimits how fast the output may changeOne variableSoft starts; ignoring impossible jumps
Butterworth / FIR (SciPy)Designed responses with a defined cut-off and roll-offCoefficients, stateOffline or demanding signal work; scipy.signal.butter, filtfilt

Worked example: a spike

Readings 20, 21, 20, 95, 21, 20, 22 (a spike of 95 in a steady 20 to 22). A 5-sample average centered on the spike is 35.4: the spike drags the average up by 14.9 and does so for five readings. The 5-sample median of the same window is 21: the spike is ignored. A median removes glitches; an average removes noise; use the one that matches the problem.

A discrete PID controller

A PID controller computes an output u from the error e = setpoint − measurement: a term proportional to the error (P), one to its accumulated sum (I, which removes steady-state error), and one to its rate of change (D, which damps the response). Called at a fixed interval Δt:

u = Kp e + Ki Σ e Δt + Kd (e − eprev) ÷ Δt

class Pid:
    def __init__(self, kp, ki, kd, dt, out_min=-10.0, out_max=10.0):
        self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
        self.out_min, self.out_max = out_min, out_max
        self.integral = 0.0
        self.prev_meas = None

    def update(self, setpoint, measurement):
        error = setpoint - measurement
        # derivative on the measurement, not the error: no kick when the setpoint changes
        d = 0.0 if self.prev_meas is None else -(measurement - self.prev_meas) / self.dt
        self.prev_meas = measurement
        self.integral += error * self.dt
        u = self.kp * error + self.ki * self.integral + self.kd * d
        u_sat = min(max(u, self.out_min), self.out_max)
        if u != u_sat and self.ki and error * (u - u_sat) > 0:     # anti-windup: stop integrating while saturated
            self.integral -= error * self.dt
        return u_sat
A discrete PID controller on a lagged plant01234500.511.5time (s)output (setpoint = 1)P only (Kp = 4)PI (Kp = 3, Ki = 4)PID (Kp = 3, Ki = 4, Kd = 0.6)Simulated at dt = 10 ms: output limited to ±10, integral stopped while the output is saturated (anti-windup).
Step responses of three controllers on a plant made of two first-order lags (τ = 0.5 s each).

Worked example: P, PI and PID on the same plant

A plant made of two cascaded lags (each τ = 0.5 s, gain 1) is driven by a unit step in the setpoint. The simulation (Δt = 10 ms, output limited to ±10) gives:

ControllerGainsFinal valueOvershootTime to stay within 2 %
PKp = 480.0 %0.0 %not settled
PIKp = 3, Ki = 4100.1 %23.9 %2.35 s
PIDKp = 3, Ki = 4, Kd = 0.6100.0 %15.5 %2.52 s
  • P alone stops short: with Kp = 4 the plant settles at Kp ÷ (1 + Kp) = 80 % of the setpoint, a steady-state error of 20 %. Raising Kp shrinks it but makes the loop oscillate.
  • The integral term removes the steady-state error but adds phase lag, hence the overshoot.
  • The derivative term brakes the response and reduces the overshoot, but it amplifies noise: filter it (see below).

Anti-windup, limits and derivative filtering

Real actuators saturate: a valve is fully open, a motor at maximum voltage. While the output is stuck at its limit the error stays large, and the integral keeps growing (windup); when the setpoint finally comes within reach, the huge stored integral drives the output far past the target.

Worked example: windup

The same PI controller (Kp = 3, Ki = 4) on one lag (τ = 0.5 s) with the output limited to ±1 is asked for a setpoint of 2 (which the limited output cannot reach in the steady state), then for 0.5 at t = 3 s. Without anti-windup the integral has grown large while saturated, so after the setpoint drops the output stays pinned at its limit and the response takes more than 3 s to fall to 0.55. With anti-windup (stop integrating while saturated) it takes 0.45 s. The unwound integral is paid back as a long, useless delay.

  • Clamp the integral or stop integrating while the output is saturated (as in the code above); more elaborate schemes back-calculate it.
  • Limit the output to what the actuator can do, and the rate of change if it should not slam.
  • Derivative on the measurement, not the error, so a step in the setpoint does not produce a huge spike (“derivative kick”).
  • Filter the derivative with a first-order low-pass (an exponential filter with a time constant of Kd ÷ (N × Kp), N = 5 to 20), or leave Kd at zero (a PI controller is enough for most processes).
  • Bumpless transfer: when switching between manual and automatic, set the integral so that the output starts where it was.
  • Dead-band and noise: a small dead-band around the setpoint stops an actuator chattering on noise.
  • Units and scaling: keep the gains in physical units (volts per degree) so that you can reason about them; set sample time Δt explicitly.

Tuning a loop

The classic Ziegler-Nichols closed-loop method: with only P control, raise Kp until the output oscillates steadily; call that gain Ku (the ultimate gain) and the oscillation period Tu. The rules give starting values:

TypeKpKi = Kp ÷ TiKd = Kp Td
P4.00––
PI3.603.60–
PID4.808.000.72

The table shows the values for Ku = 8 and Tu = 1.2 s (PID: Kp = 0.6 Ku, Ti = Tu ÷ 2, Td = Tu ÷ 8). These give a fast but oscillatory response (about 25 % overshoot): treat them as a starting point, then reduce Kp by a third or more for a gentler loop.

  • Tune on a simulation first when you can: the code on this page, a model of the plant and a few seconds of simulated time show the effect of each gain safely.
  • Step-response (open-loop) method: record how the process responds to a step in the output; fit a gain, a time constant and a dead time; use tuning rules such as Cohen-Coon or lambda tuning.
  • Change one gain at a time, increase Kp until the response is quick without much overshoot, add Ki to remove the offset, and add Kd only if you need more damping.
  • Autotune functions in controllers and libraries (relay feedback) automate the Ziegler-Nichols test, safely limited.
  • Limit the test: a loop at its stability limit can damage equipment; use small steps and output limits (see Motion Control and Robotics Tools).

Finding frequencies: the Fourier transform

The discrete Fourier transform (DFT) splits a sampled signal into sine waves of different frequencies and reports how big each is; the FFT is the fast algorithm that computes it in N log N steps. It finds vibration frequencies, mains hum, motor speeds and the dominant frequency of a noisy signal.

The same signal in time and in frequency (DFT)0255075100-202sample numbersignal010020030040050000.51frequency (Hz)amplitude50 Hz: 1.03120 Hz: 0.50100 samples at 1 kHz: the bins are 10 Hz apart
A noisy signal (top) and its spectrum (bottom): a 50 Hz component of amplitude 1 and a 120 Hz component of amplitude 0.5 appear as peaks.

Worked example: reading a spectrum

  • N = 100 samples at fs = 1 000 Hz cover T = N ÷ fs = 0.1 s, so the frequency bins are 1 ÷ T = fs ÷ N = 10 Hz apart, running from 0 to fs ÷ 2 = 500 Hz.
  • The signal 1·sin(2π 50 t) + 0.5·sin(2π 120 t) plus noise puts its energy in bins 5 (50 Hz) and 12 (120 Hz): the computed amplitudes are 1.03 and 0.50, close to 1 and 0.5, with the noise spread thinly over all bins.
  • Resolution is set by the record length, not by the sample rate: to separate two tones 2 Hz apart you need at least 0.5 s of data. Faster sampling extends the top frequency, not the resolution.
  • Leakage: if the record does not hold a whole number of cycles the peak smears over neighboring bins. Multiply by a window (Hann) before the transform, and use zero-padding only to interpolate the display.
import numpy as np
fs = 1000.0
x = np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*120*t) + noise      # t = np.arange(100) / fs
X = np.fft.rfft(x * np.hanning(len(x)))                            # real-input FFT with a Hann window
freq = np.fft.rfftfreq(len(x), 1 / fs)                             # 0, 10, 20 ... 500 Hz
amp = 2 * np.abs(X) / np.sum(np.hanning(len(x)))                   # scale to amplitude
peak = freq[np.argmax(amp)]                                        # the dominant frequency: 50 Hz

Timing: fixed rate and jitter

  • On a microcontroller run the loop from a timer interrupt or an RTOS task with a fixed period, so Δt is exact (see Embedded Systems: Architecture).
  • On a PC or Raspberry Pi, the operating system is not real-time: expect jitter of a millisecond or more. Schedule from a fixed start time (next += period) and sleep until then, rather than sleeping a fixed time each pass, and measure the actual Δt you got.
  • Use the measured Δt in the math when it varies (integral += error × dt_actual), and log it to see the jitter.
  • Hard real-time loops (a servo at 10 kHz) belong in a microcontroller or a real-time OS, with the PC sending setpoints only.
  • Latency counts: filter delay, communication delay and actuator lag all reduce the gain at which the loop stays stable.
import time
PERIOD = 0.01                         # 100 Hz
next_t = time.monotonic()
while running:
    now = time.monotonic()
    out = pid.update(setpoint, read_sensor())
    write_actuator(out)
    next_t += PERIOD                  # fixed schedule: no drift
    time.sleep(max(0.0, next_t - time.monotonic()))

A checklist for loop code

CheckWhy
Fixed, known sample time ΔtFilters and PID assume it
Anti-aliasing before the ADC; sample rate 5 to 10 times the signalAliases cannot be removed later
Filter initialized with the first readingNo start-up ramp
Output, rate and integral limitedActuators saturate; windup
Derivative on the measurement, filteredNo kick; less noise
Sensor plausibility checks and a fail-safe outputA dead sensor must not drive the actuator to a limit
Manual override and a hardware stopSafety does not depend on the software
Tested in simulation, then on the plant with small stepsFind mistakes safely
Logging of setpoint, measurement, output and ΔtYou cannot tune what you cannot see

Related

FAQ

Do I need the D term?

Often not. Many processes (temperature, level, flow) work well with PI. D helps with fast, lightly damped systems such as motion axes, if the measurement is clean or the derivative is filtered.

Why does my controller oscillate?

The gain is too high for the delay in the loop, the integral is too strong, or the sample time is too slow. Lower Kp, reduce Ki, and check the sample time and any filter delay.

What filter should I use for a sensor?

An exponential filter with a time constant matched to how fast the quantity really changes; add a median for spikes. Keep the delay below what the control loop can tolerate.

What is the difference between FFT and DFT?

They give the same result: the FFT is the fast way to compute the DFT, in N log N steps instead of N².

References

  • Åström, K. J. and Murray, R. M., Feedback Systems; Åström, K. J. and Hägglund, T., PID Controllers: Theory, Design, and Tuning.
  • Ziegler, J. G. and Nichols, N. B., “Optimum settings for automatic controllers”, Transactions of the ASME, 1942.
  • Oppenheim, A. V. and Schafer, R. W., Discrete-Time Signal Processing; Smith, S. W., The Scientist and Engineer’s Guide to Digital Signal Processing (free online).

Last reviewed: October 2026. Simulation results are computed in the page builder; real plants differ, so tune carefully with limits in place.