**[»] Electronics**.

Filters belong to an important category of analog circuits that allows selecting given frequency ranges in a signal while cancelling others. There are multiple kinds of filters including, but not limited to, low-pass filters (filters that will remove all frequencies above a given threshold), high-pass filters (filters that will remove all frequencies below a given threshold), band-pass filters (filters that will select frequencies between a low and high threshold), band-reject filters (filters that will cancel out frequencies between a low and high threshold), notch filters (sharper version of the band-reject filters typically used to remove the 50/60 Hz components of power plugs), …

In this post, I will focus on low-pass and high-pass filters. Although there exist variations of the circuit presented here for band-pass filters, it is also possible to build them by cascading a low-pass and a high-pass filter. Band-rejects filters can be built by subtracting the response of a band-pass filter to the original signal. On the other side, notch filters usually have very different circuit topology and will not be covered here.

The following sections will detail a popular circuit topology for filtering and I will then present a method to pick-up the components required to build such filters. I will then present a generic 8^{th} order filter circuit compatible with 50Ω impedance devices. The PCB files of Figure 1 are offered for free to download so that you can reproduce the circuit yourself for non commercial purposes. It is relatively inexpensive to build (about 50€) and is based on 1206 SMT components. In the final section, I will present some experimental results obtained with a 1 kHz band-pass filter.

In this post I will discuss the Sallen-Key topology whose circuit is shown in Figure 2. There exist variants of this circuit but this is the most common one. It consists of an operational amplifier with two gain resistors R_{1} and R_{2} as well as a set of four impedances Z_{1} to Z_{4}. Depending on the nature of the impedances (resistances or capacitances) the circuit will behave differently (low pass, high pass, …) but I will come back to this later.

We know that the output of the circuit will be (in the Laplace domain - you can substitute s by jω to compute the gain and phase response)

Where V_{a}(s) and V_{b}(s) are the non-inverting and inverting inputs of the operational amplifier respectively and AO(s) its frequency response.

All we need to do to compute the transfer function of the whole Sallen-Key block is to relate V_{a} and V_{b} to the input and output voltages V_{in}(s) and V_{out}(s). For clarity of the computations, I will omit the ‘s’ terms but remember that they are silently there.

V_{b} is easy to compute because R_{1} and R_{2} form a voltage divider of the output V_{out}:

Concerning V_{a}, it is a little bit trickier and we will have to use some maths.

We know that the current exiting V_{a} and the current entering V_{a} must cancel each other. Neglecting the current leakage in the operational amplifier we can then write:

Leading to

which calls in for V_{c} where we may apply the same current law

After some rearrangements and injecting the previous equation for V_{c} we find

or, for the sake of clarity,

with

Now that we have V_{a} and V_{b} we can compute the gain function through the operational amplifier:

With

which gives

This latter function allows us to compute the exact transfer function of the Sallen-Key circuit of Figure 2 provided we have a function to describe the behaviour of the operational amplifier. Most of the time, a first order low-pass filter will be enough to model the function AO:

Where K is the operational amplifier gain at DC (typically several tens of thousands) and f_{0} is the operational amplifier gain-bandwidth product (typically on the order of a few MHz). These values can be found in the datasheet of the operational amplifier that you are using.

While this model is great for simulation purposes, it does not give us a gut feeling on how the filter will behave. To gain some practical knowledge of the Sallen-Key filter, we will assume that we are working at relatively low frequencies such that the operational amplifier function can be assumed to be a very large gain, leading to 1/AO≈0.

We therefore get

We will consider two specific case of this function (from many others). The first one will be Z_{1}=Z_{2}=R and Z_{3}=Z_{4}=1/sC. This corresponds to two resistances of value R and two capacitors of value C analysed in the Laplace domain.

This equation is nothing but a low-pass filter of gain K=1/q, cut-off frequency f_{c}=1/2πRC (in Hz) and damping Q=1/(3-K):

In the Sallen-Key topology of Figure 2 we therefore control both the gain and the damping using R_{1} and R_{2} and we control the cut-off frequency using resistor R in place of Z_{1} and Z_{2} and capacitor C in place of Z_{3} and Z_{4}.

At some occasions (including on this website) you will see the circuit of Figure 2 with unity gain such as represented in Figure 3.

The second circuit that I would like to present here before moving to the next section is when Z_{1}=Z_{2}=1/sC and Z_{3}=Z_{4}=R. This is the exact opposite of the low-pass filter. We then get

which is this time a high-pass filter of gain K=1/q, cut-off frequency f_{c}=1/2πRC (in Hz) and damping Q=1/(3-K):

From these two filters (low-pass and high-pass) we can also build band-pass and band-reject filters by cascading the outputs.

Designing a filter to meet a specific frequency response is a tricky process because it involves a lot of parameters. In our single Sallen-Key filter of Figure 2 we have 6 parameters to set (Z_{1}, Z_{2}, Z_{3}, Z_{4}, R_{1} and R_{2}) which are eventually simplified to 4 parameters if we use Z_{1}=Z_{2} and Z_{3}=Z_{4} like in the previous equations. Having set a cut-off frequency and a damping parameter, we should then solve the system to find the 4 unknowns of the filter.

By the way, working with or without damping can make a big difference in your filter response. This is especially obvious when looking at the time response of a step pulse such as represented on Figure 4 for various damping parameters. So if you are tempted to use the simplified circuit of Figure 3, you should first question if this is okay for your application to work with Q=0.5.

More or less damping can then have a large impact on the filter performance. With little damping (high Q), the signal rise up quickly but also produce strong overshoot. Bigger damping (low Q) takes more time to reach the step value but does not produce overshoot.

And this is only the starting point because more complex filters can be made by cascading several Sallen-Key stages. In the circuit presented in this post, 4 stages are cascaded, producing 16 components to select. This makes the math almost impractical to apply directly to the transfer function.

In practice, each stage “i” is solved independently for a frequency f_{i} and a gain K_{i}. Tables are then available in the literature that will give you how to relate the f_{i} and K_{i} to the cut-off frequency of interest f_{c}. There exist different tables depending on the kind of behaviour that you would like your filter to have. These values are built from polynomials that share a repetitive structure. For instance, in Figure 4, Q=0.577 corresponds to a Bessel filter of 2^{nd} order, Q=0.707 corresponds to a Butterworth filter of 2^{nd} order and Q=1.129 corresponds to a Chebyshev filter of 2^{nd} order with 2 dB ripple. Each filter has its own unique characteristics. Butterworth is the best known among electronicians and is usually a good compromise in most cases while the Chebyshev filter has a steeper frequency cut-off but at the cost of ripple in the pass band region. Chebyshev filter also produce stronger overshoot than Butterworth filters. There are many different filters type and it is not possible to all list them here.

In this post I decided to focus on Bessel filters because they have the interesting property that, in low pass mode, they have little phase shift in the pass band region while the other filters tends to present alteration of the phase of the signal. This is a very nice property in signal analysis because low pass Bessel filters will conserve the shape of the signal in the time domain while the other filters will tend to distort the signal to some extends. The price to pay for that behaviour is a very slow rise in the signal such as you may have noticed on Figure 4 (Q=0.577). Note that Bessel filter have extremely little overshoot when compared to other filters.

When designing a 4 stages Bessel filter, you may find the following table in the literature for low pass filters with cut-off frequency f_{c}:

When designing high-pass filters, the multiplications should be replaced by a division. Also, note that this table is valid only for 8^{th} order (4 Sallen-Key stages) Bessel filters. If you need to design a 2^{nd} order filter (single stage), you cannot just pick the first row of the table. The tables are different for each type of filters and each total number of stages.

As an example, let us say that we want to build a 8^{th} order, 1 kHz cut-off frequency, Bessel low pass filter. We will then design the first stage to have 1/2πRC = 1781 Hz, (R1+R2)/R2 = 1.024, the second stage to have 1/2πRC = 1835 Hz, (R1+R2)/R2 = 1.213 and so on.

This is easy in theory but in practice we will quickly run into some issues because components are available only in discrete values. Depending on the component series you use (E6, E12, E24, E96…) you may have a hard time matching your resistor and capacitors to yield the good frequency and good gain values for each stages. A solution is to set two or more resistors in series to increase the discrete values available but it tends to make the circuit much bigger due to the added components. The other solution is to accept some error and to select the best compromise.

For the occasion, I created a small Matlab program that help you build your Bessel filters given an even order between 2 and 8 (although you can extend it if you find the proper tables), a filter type (low pass or high pass) and the resistors/capacitors series to look up for components values:

```
% create Bessel Filter
```**function** filter = createBesselFilter(target_frequency, num_orders, type, resistor_series, capacitor_series)
filter = [];
% coefficient **for** Bessel filters **for** different orders
**switch** num_orders
**case** 1
FrequencyCoeffsList = [1.272];
GainCoeffsList = [1.268];
**case** 2
FrequencyCoeffsList = [1.432; 1.606];
GainCoeffsList = [1.084; 1.759];
**case** 3
FrequencyCoeffsList = [1.607; 1.692; 1.908];
GainCoeffsList = [1.040; 1.364; 2.023];
**case** 4
FrequencyCoeffsList = [1.781; 1.835; 1.956; 2.192];
GainCoeffsList = [1.024; 1.213; 1.593; 2.184];
otherwise
error('Too Many order **for** **this** **function**!');
end
% scale gain **for** high pass filters **if** required
**switch**(type)
**case** 'lowpass'
**case** 'highpass'
FrequencyCoeffsList = 1 ./ FrequencyCoeffsList;
otherwise
error('Unknown filter type')
end
**for** i=1:length(FrequencyCoeffsList)
% R1 = R2 = R, C1 = C2 = C
[R, C, RC_err] = pickupRC(target_frequency * FrequencyCoeffsList(i), resistor_series, capacitor_series);
s.R1 = R;
s.R2 = R;
s.C1 = C;
s.C2 = C;
s.RC_err = RC_err;
% gain
**if** GainCoeffsList(i) ~= 1
[R1, R2, G_err] = pickupGain(GainCoeffsList(i) - 1, resistor_series);
s.R3 = R2;
s.R4 = R1;
s.G_err = G_err;
**else**
s.R3 = 0;
s.R4 = 0;
end
filter = [filter; s];
end
end
% pick up RC values
**function** [R, C, err] = pickupRC(target_frequency, resistor_series, capacitor_series)
resistors_list = getComponentSerie(resistor_series);
capacitors_list = getComponentSerie(capacitor_series);
r_value = getMinResistor();
c_value = getMinCapacitor();
% best value to **return**
R = 0;
C = 0;
best_freq = inf;
**while** c_value < getMaxCapacitor()
% **if** we can increase resistor, pickup next resistor in list
**if** r_value < getMaxResistor()
r_value = pickupNextComponent(r_value, resistors_list);
% **if** not, reset resistor and increase capacitor **if** possible
**else**
r_value = getMinResistor();
c_value = pickupNextComponent(c_value, capacitors_list);
% remove me
**break**;
end
% remove me
c_value = 1.5e-9;
freq = 1 / (2 * pi * r_value * c_value);
% update best components
**if** abs(freq - target_frequency) < abs(best_freq - target_frequency)
best_freq = freq;
R = r_value;
C = c_value;
err = abs(freq - target_frequency) / target_frequency;
end
end
end
% pickup gain R values
**function** [R1, R2, err] = pickupGain(target_gain, resistor_series)
resistors_list = getComponentSerie(resistor_series);
r1**_value** = getMinResistor();
r2**_value** = getMinResistor();
% best value to **return**
R1 = 0;
R2 = 0;
best_gain = inf;
**while** r2**_value** < getMaxResistor()
% **if** we can increase resistor, pickup next resistor in list
**if** r1**_value** < getMaxResistor()
r1**_value** = pickupNextComponent(r1**_value**, resistors_list);
% **if** not, reset resistor and increase capacitor **if** possible
**else**
r1**_value** = getMinResistor();
r2**_value** = pickupNextComponent(r2**_value**, resistors_list);
end
gain = r1**_value** / r2**_value**;
% update best components
**if** abs(gain - target_gain) < abs(best_gain - target_gain)
best_gain = gain;
R1 = r1**_value**;
R2 = r2**_value**;
err = abs(gain - target_gain) / target_gain;
end
end
end
% minimum resistor value to use
**function** Rmin = getMinResistor()
Rmin = 1e3;
end
% maximum resistor value to use
**function** Rmax = getMaxResistor()
Rmax = 100e3;
end
% minimum capacitor value to use
**function** Cmin = getMinCapacitor()
Cmin = 1e-9;
end
% maximum capactiro value to use
**function** Cmax = getMaxCapacitor()
Cmax = 100e-9;
end
% **return** list of **component** **for** a given series
**function** list = getComponentSerie(series)
list = [];
**switch** series
**case** 'Kit'
list = [100; 470];
**case** 'E6'
list = [100; 150; 220; 330; 470; 680];
**case** 'E12'
list = [100; 120; 150; 180; 220; 270; 330; 390; 470; 560; 680; 820];
**case** 'E24'
list = [100; 110; 120; 130; 150; 160; 180; 200; 220; 240; 270; 300; 330; 360; 390; 430; 470; 510; 560; 620; 680; 750; 820; 910];
**case** 'E48'
list = [100; 105; 110; 115; 121; 127; 133; 140; 147; 154; 162; 169; 178; 187; 196; 205; 215; 226; 237; 249; 261; 274; 287; 301; 316; 332; 348; 365; 383; 402; 422; 442; 464; 487; 511; 536; 562; 590; 619; 649; 681; 715; 750; 787; 825; 866; 909; 953];
**case** 'E96'
list = [100; 102; 105; 107; 110; 113; 115; 118; 121; 124; 127; 130; 133; 137; 140; 143; 147; 150; 154; 158; 162; 165; 169; 174; 178; 182; 187; 191; 196; 200; 205; 210; 215; 221; 226; 232; 237; 243; 249; 255; 261; 267; 274; 280; 287; 294; 301; 309; 316; 324; 332; 340; 348; 357; 365; 374; 383; 392; 402; 412; 422; 432; 442; 453; 464; 475; 487; 499; 511; 523; 536; 549; 562; 576; 590; 604; 619; 634; 649; 665; 681; 698; 715; 732; 750; 768; 787; 806; 825; 845; 866; 887; 909; 931; 953; 976];
**case** 'E192'
list = [100; 101; 102; 104; 105; 106; 107; 109; 110; 111; 113; 114; 115; 117; 118; 120; 121; 123; 124; 126; 127; 129; 130; 132; 133; 135; 137; 138; 140; 142; 143; 145; 147; 149; 150; 152; 154; 156; 158; 160; 162; 164; 165; 167; 169; 172; 174; 176; 178; 180; 182; 184; 187; 189; 191; 193; 196; 198; 200; 203; 205; 208; 210; 213; 215; 218; 221; 223; 226; 229; 232; 234; 237; 240; 243; 246; 249; 252; 255; 258; 261; 264; 267; 271; 274; 277; 280; 284; 287; 291; 294; 298; 301; 305; 309; 312; 316; 320; 324; 328; 332; 336; 340; 344; 348; 352; 357; 361; 365; 370; 374; 379; 383; 388; 392; 397; 402; 407; 412; 417; 422; 427; 432; 437; 442; 448; 453; 459; 464; 470; 475; 481; 487; 493; 499; 505; 511; 517; 523; 530; 536; 542; 549; 556; 562; 569; 576; 583; 590; 597; 604; 612; 619; 626; 634; 642; 649; 657; 665; 673; 681; 690; 698; 706; 715; 723; 732; 741; 750; 759; 768; 777; 787; 796; 806; 816; 825; 835; 845; 856; 866; 876; 887; 898; 909; 920; 931; 942; 953; 965; 976; 988];
otherwise
error('Unknown Serie');
end
end
% **return** next **component** in serie
**function** next_val = pickupNextComponent(prev_val, list)
% scale list to fit previous entry
factor = power(10, floor(log10(prev_val / list(1))));
list = list * factor;
% browse list **for** next item
**for** i=1:length(list)
**if** list(i) > prev_val
next_val = list(i);
**return**
end
end
% **if** not found, then it is in the next decade
next_val = 10 * list(1);
end

The program simply applies the stage selection algorithm which has a “pickupRC” and pickupGain” function that will iterate through the component series to find the best match in terms of frequency or gain. To prevent selecting stupid components (*e.g.* a 1Ω resistor with an operational amplifier that has only 5 mA output current capacity for a ±5 Volts swing), the minimum and maximum component values can also be set in the hardcoded functions at the end of the file.

Once we have designed our filter we can simulate its frequency response using the generic formula of the previous section:

**function** [FreqHz, Gain, Phase] = computeBodePlot(filter, type, FreqMinHz, FreqMaxHz, FreqNumSteps)
% create frequency vec
FreqHz = power(10, linspace(log10(FreqMinHz), log10(FreqMaxHz), FreqNumSteps));
% compute transmitance
**switch** type
**case** 'lowpass'
T = SallenKey_LowPassFilter(filter, 2 * pi * FreqHz);
**case** 'highpass'
T = SallenKey_HighPassFilter(filter, 2 * pi * FreqHz);
otherwise
error('Unknown filter type');
end
% extract gain & phase
Gain = abs(T);
Phase = (180 / pi) * unwrap(angle(T));
end
**function** filter = appendOpAmp(filter, OpAmpGain0, OpAmpGainBandwidth)
% brose filter and insert op amp settings
**for** i=1:length(filter)
filter(i).OpAmpGain0 = OpAmpGain0;
filter(i).OpAmpGainBandwidth = OpAmpGainBandwidth;
end
end
**function** G = SallenKey_LowPassFilter(ComponentsList, FreqW)
G = ones(1, length(FreqW));
n = length(ComponentsList);
**for** i=1:n
Z1 = ComponentsList(i).R1;
Z2 = ComponentsList(i).R2;
**if** exist('ComponentsList(i).C2ESR', 'var')
Z3 = complex(ComponentsList(i).C2ESR, -1 ./ (FreqW * ComponentsList(i).C2));
**else**
Z3 = complex(0, -1 ./ (FreqW * ComponentsList(i).C2));
end
**if** exist('ComponentsList(i).C1ESR', 'var')
Z4 = complex(ComponentsList(i).C1ESR, -1 ./ (FreqW * ComponentsList(i).C1));
**else**
Z4 = complex(0, -1 ./ (FreqW * ComponentsList(i).C1));
end
R1 = ComponentsList(i).R3;
R2 = ComponentsList(i).R4;
OpAmpGain = 1 ./ ((1 / ComponentsList(i).OpAmpGain0) + complex(0, FreqW / (2 * pi * ComponentsList(i).OpAmpGainBandwidth)));
G = G .* SallenKey(Z1, Z2, Z3, Z4, R1, R2, OpAmpGain);
end
end
**function** G = SallenKey_HighPassFilter(ComponentsList, FreqW)
G = ones(1, length(FreqW));
n = length(ComponentsList);
**for** i=1:n
**if** exist('ComponentsList(i).C1ESR', 'var')
Z1 = complex(ComponentsList(i).C1ESR, -1 ./ (FreqW * ComponentsList(i).C1));
**else**
Z1 = complex(0, -1 ./ (FreqW * ComponentsList(i).C1));
end
**if** exist('ComponentsList(i).C2ESR', 'var')
Z2 = complex(ComponentsList(i).C2ESR, -1 ./ (FreqW * ComponentsList(i).C2));
**else**
Z2 = complex(0, -1 ./ (FreqW * ComponentsList(i).C2));
end
Z3 = ComponentsList(i).R2;
Z4 = ComponentsList(i).R1;
R1 = ComponentsList(i).R3;
R2 = ComponentsList(i).R4;
OpAmpGain = 1 ./ ((1 / ComponentsList(i).OpAmpGain0) + complex(0, FreqW / (2 * pi * ComponentsList(i).OpAmpGainBandwidth)));
G = G .* SallenKey(Z1, Z2, Z3, Z4, R1, R2, OpAmpGain);
end
end
**function** G = SallenKey(Z1, Z2, Z3, Z4, R1, R2, OpAmpGain)
tmp = (Z2 .* Z3 .* Z4) + (Z1 .* Z2 .* Z4) + (Z1 .* Z2 .* Z3) + (Z2 .* Z2 .* Z4) + (Z2 .* Z2 .* Z1);
**if** R2 == 0
b = 1;
**else**
b = R1 ./ (R1 + R2);
end
c = (Z2 .* Z3 .* Z4) ./ tmp;
d = (Z1 .* Z2 .* Z3) ./ tmp;
G = (c ./ b) ./ (1 + 1 ./ (OpAmpGain .* b) - (d ./ b));
end

The function returns the gain and phase data for the Bode plots of the function using the Sallen-Key transfer function. The function not only accepts the values of resistances and capacitances Z_{1}, Z_{2}, Z_{3}, Z_{4}, R_{1} and R_{2} but also the capacitances equivalent series resistor (ESR) and the operational amplifier frequency response modelled as a first order transfer function.

These functions should already help you simulating the response of your filter provided the components you buy are at their nominal value. In practice, this is never the case because each component values will fluctuate around its nominal value by the tolerance of the component you purchased. Tighter toleranced components exists but they are more expensive. Most of the time, we will have to work with 1% or 5% resistors and 10% or 20% capacitors.

To determine how much your filter may fluctuate from its nominal response taking into account the components tolerance, the idea is to run a Monte Carlo simulations on the filter.

In Monte Carlo simulations, all components of the nominal system are perturbed randomly in their tolerance window. Each perturbed system are them simulated giving its own frequency response curve. The process is then repeated a large number of times such that a statistical confidence interval can be built with the mean response of the system and a window where you will have a certain amount of probability “p%” such that if you build many systems, p% of these systems will have a frequency response falling in that window. Most of the time, I work with 95% confidence intervals (such that 95% of the filters that I would build would fall into the region shown).

The program itself is not too complex:

**function** [FreqHz, Gain_mean, Gain_std, Phase_mean, Phase_std] = computeBodePlotWithTolerances(filter, type, FreqMinHz, FreqMaxHz, FreqNumSteps, TolRes, TolCap, NumSimulations)
% init
Gain_mean = zeros(1, FreqNumSteps);
Gain_var = zeros(1, FreqNumSteps);
Phase_mean = zeros(1, FreqNumSteps);
Phase_var = zeros(1, FreqNumSteps);
**for** i=1:NumSimulations
% init filter with tolerance
filter_with_tol = filter;
**for** j=1:length(filter_with_tol)
filter_with_tol(j) = applyTolerances(filter_with_tol(j), TolRes, TolCap);
end
% compute transmitance
[FreqHz, Gain, Phase] = computeBodePlot(filter_with_tol, type, FreqMinHz, FreqMaxHz, FreqNumSteps);
% add to mean and variance
Gain_mean = Gain_mean + Gain;
Gain_var = Gain_var + Gain .^ 2;
Phase_mean = Phase_mean + Phase;
Phase_var = Phase_var + Phase .^ 2;
end
Gain_mean = Gain_mean / NumSimulations;
Gain_std = sqrt(Gain_var / NumSimulations - Gain_mean .^ 2);
Phase_mean = Phase_mean / NumSimulations;
Phase_std = sqrt(Phase_var / NumSimulations - Phase_mean .^ 2);
end
% randomize components **using** given tolerances
**function** filter_with_tol = applyTolerances(filter_without_tol, TolRes, TolCap)
% init
filter_with_tol = filter_without_tol;
% randomize resistors
filter_with_tol.R1 = filter_without_tol.R1 * (1 + (rand() - 1) * 2 * TolRes);
filter_with_tol.R2 = filter_without_tol.R2 * (1 + (rand() - 1) * 2 * TolRes);
filter_with_tol.R3 = filter_without_tol.R3 * (1 + (rand() - 1) * 2 * TolRes);
filter_with_tol.R4 = filter_without_tol.R4 * (1 + (rand() - 1) * 2 * TolRes);
% randomize capacitors
filter_with_tol.C1 = filter_without_tol.C1 * (1 + (rand() - 1) * 2 * TolCap);
filter_with_tol.C2 = filter_without_tol.C2 * (1 + (rand() - 1) * 2 * TolCap);
end
**function** plotBodeWithTolerances(FreqHz, Gain_mean, Gain_std, Phase_mean, Phase_std)
% Display Gain
figure(1)
subplot(2,1,1)
hold all
h = fill([FreqHz, fliplr(FreqHz)], [20 * log10(abs(Gain_mean + 1.96 * Gain_std)), fliplr(20 * log10(abs(Gain_mean - 1.96 * Gain_std)))], 'red');
set(h, 'FaceColor', [1 0.5 0]);
set(h, 'FaceAlpha', 0.25);
set(h, 'EdgeColor', 'black');
set(h, 'EdgeAlpha', 0.25);
plot(FreqHz, 20 * log10(abs(Gain_mean)), 'black', 'LineWidth', 1.5);
set(gca,'xscale','log')
title('Gain');
xlabel('Freq (Hz)');
ylabel('Gain (dB)');
% Display Phase
figure(1)
subplot(2,1,2)
hold all
h = fill([FreqHz, fliplr(FreqHz)], [Phase_mean + 1.96 * Phase_std, fliplr(Phase_mean - 1.96 * Phase_std)], 'black');
set(h, 'FaceColor', [1 0.5 0]);
set(h, 'FaceAlpha', 0.25);
set(h, 'EdgeColor', 'black');
set(h, 'EdgeAlpha', 0.25);
plot(FreqHz, Phase_mean, 'black', 'LineWidth', 1.5);
set(gca,'xscale','log')
title('Phase');
xlabel('Freq (Hz)');
ylabel('Phase (deg°)');
end

The function takes the filter as input and the tolerances on the capacitors and resistors. It then generates a given number of simulations (typically 10,000) and compute the gain and phase of each of these systems. The function then returns an average gain and phase with their standard deviation so that you can build your confidence intervals.

Figure 5 shows a Bode Plot for a 1 kHz, 8^{th} order, low pass Bessel filter built from 5% E12 resistors and 20% E6 capacitors. The orange area represents the 95% confidence interval for the filter through the Monte Carlo simulations.

For this post I generated three filters to test, all based on 8^{th} order Bessel filters. There is a 1 kHz low-pass filter, a 1 kHz high-pass filter and a 1 kHz band-pass filter. Each table gives the components (Z_{1}, Z_{2}, Z_{3}, Z_{4}, R_{1} and R_{2}) to use for each Sallen-Key stages (A, B, C and D).

The circuit proposed here is given in Figure 6. It comprises an input stage with a BNC connector and a 50Ω impedance load with a TL072 (U2) as a unity follower. We then have four Sallen-Key stages labelled A,B,C and D (only stage A is shown in the figure) with a low noise generic operational amplifier TL074 for U1. We then have a current booster LT1010 (U3) connected to the output BNC connector. Because the LT1010 has a relatively large output offset voltage, it is zeroed by U2:B which has much better performances in terms of output offset voltage but which cannot drive a 50Ω load at large output voltage swings. Capacitor C1 is added to prevent oscillations at high frequencies. You can find this output buffer circuit in the LT1010 datasheet.

The circuit is powered by two TC962 as shown in Figure 7. A LED is added to both monitor the power status (on/off) and to have a minimal load on the system to prevent malfunctions of the power supply (12 Volts DA12-120EU-M unit).

The circuit PCB Gerber files of Figure 8 can be downloaded [∞] here for non-commercial reproduction. All the components are SMT 1206 size or SOIC/SOIC-W, only U3 is in TO220-5 format and the aluminium capacitors are in 6.60x7.90 and 4.30x6.00 formats. 100 nF decoupling capacitors C5-C10 were also added to help reducing the noise levels. It is strongly suggested to select components for the Sallen-Key stage that are very stable in temperature. This is especially true for the ceramic capacitors that can be very sensitive in their cheapest versions.

Concerning the BNC connectors J1 and J2 I used the “1-1634612-0” connectors from TE Connectivity. For the power supply connector, J3, I used the “PJ-102BH” from CUI Inc. Both the BNC and power supply connectors are available at [∞] Digi-Key.

Due to a mistake when ordering the component (I bought 0805 capacitors instead of 1206 ones…), I was not able to implement the Bessel filters I planned to. Fortunately, I had some 1.5 nF 1206 capacitors on hand and I decided to use them to build my filter (yeah, because I could not wait… you know how it is). Obviously, this is not ideal and the filter will not be as good as it could have been.

The filter used for the test performed here is given in the table below:

Still, the tests were successful with a nice sharp bandpass at just 1.1 kHz. The results are reported in Figure 9 and compared to the expected performances. Concerning the output voltage swing, it is slightly below ±5V with a 50Ω load when running at 12 Volts power input.

We can see that the filter behaves almost as expected from the simulations. The actual data are however slightly shifted towards the lower frequencies tough it is still within the margin of the Monte-Carlo simulations (dotted lines). At frequencies higher than 2.5 kHz the cut-off is nice up to the MHz range (not shown) even if it stalls to about -60 dB probably due to the remaining noise in the output. However, there is a clear departure from the theoretical curve below 500 Hz. I noticed some odd ringing effect between from about 30 Hz up to 500 Hz that are illustrated in Figure 10. I have no explanation of this phenomenon yet but it clearly affects the performance of the filter.

Finally, I also checked the noise level of the circuit. With the DA12-120EU-M power supply unit the noise level reach 2.5 mVrms (100 MHz bandwidth probe) with a clear contribution of the power supply that could be seen at about 10 kHz. I therefore decided to solder a second circuit, with the same Sallen-Key stages but running from two 9 Volts batteries. The overall noise then dropped to 850 µVolts which relates well to the 18 nVolts/√Hz of the operational amplifiers used (475 µVolts expected) when considering that the oscilloscope already shows about 350 µVolts of noise when the circuit is not powered on.

If the 2.5 mVrms noise levels are too high for your application, you may consider running the circuit from batteries. And if the 850 µVolts are also too much, you may consider changing the operational amplifiers by a AD8674 for U1 and a AD8672 for U2 which should be pin-to-pin compatible in their SOIC version. With these, I would expect the noise to drop to about 200 µVolts (untested). At this point, the noise will be mostly limited by the LT1010. One possibility (untested), would be to replace the LT1010 by a BUF634 but this would require changing the PCB since the two IC are not pin-to-pin compatibles.

I am currently working on noise reduction and I will post updates as soon as I manage to get something nice.

I hope you still enjoyed the post!

**You may also like:**

[»] Sine Wave Oscillator with Fewer Op-Amp

[»] 1 Amp Power LED driver with Modulation Input