Published: 2016-12-04 | Category: [»] Electricity & 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 8th 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.

Figure 1 – The final circuit presented in this post
Analysis of the Sallen-Key Topology

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 R1 and R2 as well as a set of four impedances Z1 to Z4. 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.

Figure 2 – The Sallen-Key topology

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 Va(s) and Vb(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 Va and Vb to the input and output voltages Vin(s) and Vout(s). For clarity of the computations, I will omit the ‘s’ terms but remember that they are silently there.

Vb is easy to compute because R1 and R2 form a voltage divider of the output Vout:

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

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

Leading to

which calls in for Vc where we may apply the same current law

After some rearrangements and injecting the previous equation for Vc we find

or, for the sake of clarity,


Now that we have Va and Vb we can compute the gain function through the operational amplifier:


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 f0 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 Z1=Z2=R and Z3=Z4=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 fc=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 R1 and R2 and we control the cut-off frequency using resistor R in place of Z1 and Z2 and capacitor C in place of Z3 and Z4.

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

Figure 3 – Sallen-Key topology with unity gain

The second circuit that I would like to present here before moving to the next section is when Z1=Z2=1/sC and Z3=Z4=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 fc=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

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 (Z1, Z2, Z3, Z4, R1 and R2) which are eventually simplified to 4 parameters if we use Z1=Z2 and Z3=Z4 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.

Figure 4 - Step response with various damping parameter

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 fi and a gain Ki. Tables are then available in the literature that will give you how to relate the fi and Ki to the cut-off frequency of interest fc. 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 2nd order, Q=0.707 corresponds to a Butterworth filter of 2nd order and Q=1.129 corresponds to a Chebyshev filter of 2nd 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 fc:

When designing high-pass filters, the multiplications should be replaced by a division. Also, note that this table is valid only for 8th order (4 Sallen-Key stages) Bessel filters. If you need to design a 2nd 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 8th 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 Z1, Z2, Z3, Z4, R1 and R2 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, 8th 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.

Figure 5 – Bode Plot for a 1 kHz 8th order Bessel low pass filter with its 95% confidence interval

For this post I generated three filters to test, all based on 8th 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 (Z1, Z2, Z3, Z4, R1 and R2) to use for each Sallen-Key stages (A, B, C and D).

Generic 8th Order Sallen-Key PCB

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.

Figure 6 - Circuit Diagram

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).

Figure 7 - Power Supply

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.

Figure 8 – PCB of the final circuit

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.

Figure 9 – Experimental results compared to theoretical simulations

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.

Figure 10 – Some ringing effect between 30 Hz and 250 Hz

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!

[⇈] Top of Page

You may also like:

[»] Sine Wave Oscillator with Fewer Op-Amp

[»] 1 Amp Power LED driver with Modulation Input

[»] DIY Conductometry

[»] In-line Absorption and Fluorescence Sensor

[»] Low Noise, Adjustable Gain, Photodiode Amplifier