Your controller is a noise amplifier. A speed loop that looks clean on paper turns encoder hash into actuator chatter, heat, and whine. The fix is a filter — but inside a feedback loop a filter is never free: every degree of lag it adds at your crossover comes straight out of your phase margin. This page builds the skill of placing that trade deliberately: the one-pole filter first, then the Butterworth/Bessel/Chebyshev families, then analog and digital realizations, ending with a seven-step recipe and a live loop you can push into oscillation.
A PI loop holds a DC motor at 10 rad/s. The speed signal comes from an optical encoder: count the clicks, difference them every millisecond. On the scope, that "speed" is a fuzzy band — the true value plus junk of about ±0.3 rad/s. Differentiated quantization (every click is a tiny step), PWM ripple coupling in at 20 kHz, ground noise. Nothing is wrong with the control law. The problem is what the controller does to that junk: it multiplies whatever sits in the error by its gain and commands the result onto the motor. The 300 Hz slice of the hash alone, times the proportional gain of 0.31 V/(rad/s), is about 0.09 V of continuous chatter on a drive that should be sitting still.
Notice what the noise is not: it is not something you know, so you cannot subtract it, and it is not small, so you cannot ignore it. But you know its time scale. The real speed changes over tens of milliseconds; the junk wiggles hundreds to thousands of times per second. Separation by time scale is separation by frequency — and that is an opening you can exploit with a linear circuit or a few lines of code.
You already think of signals as sums of sinusoids, so here is the whole idea in one sentence: a linear filter treats each sinusoid separately, multiplying it by \(|H(j\omega)|\) and shifting it by \(\angle H(j\omega)\). "Removing the noise" therefore has an exact meaning: choose \(|H| \approx 1\) where the information lives (below about 20 Hz here), \(|H| \approx 0\) where the junk lives (above 300 Hz), and transition as fast as convenient in between.
Everything on this page is about that gold curve and its hidden partner, the phase. You choose the corner frequency (where \(|H|\) starts falling), the steepness (how fast it falls), and the shape of the knee. The phase lag that comes along with them is not optional — it is the price, and §4 is about who collects it.
The simplest useful filter is one pole — the classic RC low-pass:
with the pole at \(-\omega_c\). Below the corner \(f_c = \omega_c/2\pi\), \(|H|\approx 1\). Above it, \(|H|\) falls at \(-20\) dB per decade: ten times the frequency, ten times less signal. The phase swings from 0° to \(-90°\), passing \(-45°\) exactly at the corner. One pole, three consequences, all visible in Figure 3: at 2× the corner you have already lost 7 dB of noise; at 5×, 14 dB; and the phase lag is already \(-5.7°\) at one tenth of the corner — remember that number for §4.
Drag the corner yourself and watch two things happen at the same time:
The fuzz dies and the gold curve starts trailing the truth. The lag you see in the time domain is the phase of Figure 3 — same phenomenon, other picture. At DC the delay is \(1/\omega_c\): 1.6 ms at 100 Hz, 32 ms at 5 Hz. So far this is a mere nuisance. The next section is where it gets expensive.
Up to now the filter was a signal-processing box sitting on a bench. In the loop it becomes part of the dynamics. The loop transmission — the gain-around-the-loop that decides stability — changes from \(L = CP\) to
Whatever the filter does to the signal, it now also does to the loop.
Now put the filter in. Its magnitude at crossover is \(\approx 1\) — with the corner 5× above crossover, \(|H|\) there is 0.999+, and the crossover barely moves (20.00 → 19.98 Hz). But its phase at crossover is pure loss:
Here is the whole design problem in one sentence: noise wants the corner low, stability wants the corner high, and the space between your crossover and your noise band is the room you have to work in. The rules of thumb fall straight out of the geometry: keep the corner at 5–10× the crossover, which costs a one-pole filter \(57.3/\text{ratio}\) degrees and a 2-pole Butterworth about \(81/\text{ratio}\) degrees at crossover — 16.4° for the 100 Hz design. Spend only what you have: with a 60° margin, a 10–15° spend is tolerable; 30° is asking for ringing.
Slide the ratio down to 1.5 and watch the verdict flip to oscillates: at a 1× ratio the filter alone contributes \(-90°\) at crossover — the margin is gone before the loop even starts. This is why "just filter the measurement" is the most classic way to destabilize an otherwise healthy loop.
One pole buys \(-20\) dB/decade. The 100 Hz one-pole design leaves the 300 Hz hash only 14 dB down — often not enough. Cascade \(n\) poles and the stopband steepens to \(-n\cdot 20\) dB/decade:
| |H| at \(n\,\times\) the corner | n = 1 | n = 2 | n = 3 | n = 4 |
|---|---|---|---|---|
| 2× | −7.0 dB | −12.3 dB | −18.1 dB | −24.1 dB |
| 5× | −14.1 dB | −28.0 dB | −41.9 dB | −55.9 dB |
| 10× | −20.0 dB | −40.0 dB | −60.0 dB | −80.0 dB |
The bill grows with the order (each pole adds lag near and below the corner), and one free choice remains: where exactly the \(n\) poles sit in the s-plane. The classical answers have names and personalities — same corner, same order, different placements:
For design work you need one family by heart — Butterworth. Its poles sit on a circle, \( s_k = \omega_c\, e^{\,j(2k+n-1)\pi/(2n)} \), which produces the factored forms:
| order | Butterworth \(H(s)\), corner \(\omega_c\) |
|---|---|
| 1 | \(\dfrac{\omega_c}{s+\omega_c}\) |
| 2 | \(\dfrac{\omega_c^2}{s^2+\sqrt{2}\,\omega_c s+\omega_c^2}\) |
| 3 | \(\dfrac{\omega_c^3}{(s+\omega_c)(s^2+\omega_c s+\omega_c^2)}\) |
| 4 | \(\dfrac{\omega_c^4}{(s^2+0.765\,\omega_c s+\omega_c^2)(s^2+1.848\,\omega_c s+\omega_c^2)}\) |
And to turn an attenuation spec into an order \(n\) (Butterworth): \( n \ge \log_{10}(10^{A/10}-1)\,\big/\,2\log_{10}(\omega_s/\omega_c) \). For "\(-20\) dB at 3× the corner": \(n \ge \log_{10}(99)/(2\times 0.477) = 2.1\), so \(n=3\). (Check the table: \(n=2\) would deliver only \(-19.1\) dB — almost.)
The poles are the design; the schematic or the code is just a skin over them. For one 2nd-order analog section the workhorse is the Sallen–Key filter. With equal resistors and capacitors its corner is \(\omega_c = 1/RC\), and the op-amp's gain \(K\) sets the damping: \(Q = 1/(3-K)\). Butterworth needs \(Q = 1/\sqrt{2}\), so \(K = 3-\sqrt{2} = 1.586\). With \(C = 100\) nF and \(R = 15.92\) kΩ: corner = 100.1 Hz. Higher orders cascade sections; filter tables and tools fill in the values.
A digital filter sees the world only at sample times, and every frequency above \(f_s/2\) folds back below it. A 600 Hz sinusoid sampled at 1 kHz is indistinguishable from a 400 Hz one. The nasty party trick: 20 kHz PWM ripple sampled at 1 kHz folds to exactly 0 Hz — a phantom DC "speed error" that your integrator will faithfully chase forever.
That is why real products have both an analog and a digital filter: a cheap RC before the ADC — here \(\tau = 200\,\mu s\), corner 796 Hz — takes \(-28\) dB out of the 20 kHz ripple (killing the fold-back junk) while barely touching 300 Hz (\(-0.6\) dB, leaving it for the digital stage). The analog RC is the bodyguard; the digital filter is the surgeon.
Discretize \(\tau\dot{y} = x-y\) over one sample time \(T\):
With \(\tau = 1.59\) ms (100 Hz) and \(T = 1\) ms: \(\alpha = 0.5335\). Three lines of C; the realized \(-3\) dB corner is 103.5 Hz, within 3.5% of target.
y[k] = alpha*y[k-1] + (1-alpha)*x[k]; // alpha = exp(-2*pi*f_c*T)
For higher orders, substitute the bilinear transform into your s-domain \(H(s)\) and simplify:
It maps the whole left half-plane into the unit circle, so stability survives, and every design becomes a difference equation. Its one quirk: frequencies compress, so a 100 Hz design lands at 96.9 Hz when \(f_s = 1\) kHz. The fix is to prewarp — design the s-domain corner at \((2/T)\tan(\omega T/2) = 103.43\) Hz, and the realized filter hits 100.00 Hz exactly. (Below \(f_s/10\), nobody bothers.) Doing that to the 2nd-order Butterworth section gives, verified:
y[k] = 0.067455*x[k] + 0.134911*x[k-1] + 0.067455*x[k-2]
+ 1.142981*y[k-1] - 0.412802*y[k-2];
Gold is the continuous design; blue dots are what the microcontroller actually produces. Even at 800 Hz sampling — eight samples per corner period — the dots sit on the curve. The recipe is mechanical, and it works.
Everything so far, as a recipe you can run on any noisy measurement feeding a controller:
Slide the corner down from 100 Hz and watch the loop pay: at 50 Hz the step overshoots 16%; at 30 Hz, 47%; at 25 Hz it rings on every step; below about 17 Hz the margin is gone entirely and the loop howls — with the same controller gains. The filter did it, through phase alone. Then slide back up and watch the actuator panel go quiet: that is the 86 mV → 7 mV payoff.
scipy.signal.butter + scipy.signal.bilinear documentation:
the tool you will actually type — this page is what happens inside it.The whole skill in one line: put the corner between the crossover and the noise, pay for steepness with order and for placement with margin, and verify the loop — not the plot.