Discretization of a Fourth-Order Butterworth Filter

Pieter P

This is an example on how to design a filter in the analog domain, and then use the bilinear transform to transform it to the digital domain, while preserving the cut-off frequency.

We'll be using formulas derived on the Bilinear Transform and Butterworth Filters pages.

Design criteria

In this example, we'll design a digital fourth order Butterworth low-pass filter, with a sample frequency of and a cut-off frequency of .

Frequency Pre-Warping

As discussed in the page on the Bilinear Transform, we have to apply pre-warping to the cut-off frequency before designing a filter. If we don't the cut-off frequency will shift to an incorrect frequency when we discretize the filter.

First, let's calculate the normalized digital frequency , using the cut-off frequency and the sample frequency : The Nyquist-Shannon sampling theorem tells us that we can never sample frequencies higher than without losing information. This also means that the cut-off frequency can never be higher than half of the sample frequency. Or in other words, all normalized frequencies will be in the interval .

Next, we'll use the pre-warping formula we derived in the page on the Bilinear Transform, in order to calculate the analog design frequency : Note that this frequency is relatively close to , but it is not the same. The higher the cut-off frequency (relative to the sample frequency), the larger the error between and .

Designing the Butterworth filter in the Analog Domain

Now that we know the pre-warped analog cut-off frequency, we can start designing the analog filter.
We'll use the formula for the Butterworth low-pass filter derived in the page on Butterworth Filters: Defining these constants will make the calculations much easier:

Discretizing the Analog Filter

We can now just apply the Bilinear Transform to the analog transfer function, by substituting . Therefore: Again, we'll introduce a constant to simplify the expression: What follows is just rearranging the expression of from Equation , using the substitution of Equation .
Finally, we end up with an expression for the transfer function, using Equation , and we can determine the coefficients using the constants defined in Equations , & .

Frequency response & Pole-Zero Map

We can check the filter's frequency response to make sure that we didn't make any mistakes. As mentioned in other pages, the frequency response of a digital system can be a obtained by evaluating the transfer function along the unit circle (). We'll plot the magnitude in decibels. We can also plot the phase angle of the response: You can see that the corner frequency lies around . We can check this mathematically:

MATLAB & GNU Octave

If you have to design many different filters, you don't want to calculate them all by hand. Luckily, MATLAB and GNU Octave come with a command to calculate the coefficients of Butterworth filters.

f_s = 360;    % Sample frequency in Hz
f_c = 45;     % Cut-off frequency in Hz
order = 4;    % Order of the butterworth filter

omega_c = 2 * pi * f_c;       % Cut-off angular frequency
omega_c_d = omega_c / f_s;    % Normalized cut-off frequency (digital)

[b, a] = butter(order, omega_c_d / pi);    % Design the Butterworth filter
disp('a = '); disp(a);                     % Print the coefficients
disp('b = '); disp(b);
H = tf(b, a, 1 / f_s);                     % Create a transfer function
bode(H);                                   % Show the Bode plot
Note that MATLAB expects the normalized frequency as a number from 0 to 1, so we have to divide by before passing it to the butter function.

Python

A similar function is available in the SciPy signal package: butter.

from scipy.signal import butter, freqz, freqs
import matplotlib.pyplot as plt
from math import pi
import numpy as np

f_s = 360    # Sample frequency in Hz
f_c = 45     # Cut-off frequency in Hz
order = 4    # Order of the butterworth filter

omega_c = 2 * pi * f_c       # Cut-off angular frequency
omega_c_d = omega_c / f_s    # Normalized cut-off frequency (digital)

# Design the digital Butterworth filter
b, a = butter(order, omega_c_d / pi)    
print('Coefficients')
print("b =", b)                           # Print the coefficients
print("a =", a)

w, H = freqz(b, a, 4096)                  # Calculate the frequency response
w *= f_s / (2 * pi)                       # Convert from rad/sample to Hz

# Plot the amplitude response
plt.subplot(2, 1, 1)            
plt.suptitle('Bode Plot')
H_dB = 20 * np.log10(abs(H))              # Convert modulus of H to dB
plt.plot(w, H_dB)
plt.ylabel('Magnitude [dB]')
plt.xlim(0, f_s / 2)
plt.ylim(-80, 6)
plt.axvline(f_c, color='red')
plt.axhline(-3, linewidth=0.8, color='black', linestyle=':')

# Plot the phase response
plt.subplot(2, 1, 2)
phi = np.angle(H)                         # Argument of H
phi = np.unwrap(phi)                      # Remove discontinuities 
phi *= 180 / pi                           # and convert to degrees
plt.plot(w, phi)
plt.xlabel('Frequency [Hz]')
plt.ylabel('Phase [°]')
plt.xlim(0, f_s / 2)
plt.ylim(-360, 0)
plt.yticks([-360, -270, -180, -90, 0])
plt.axvline(f_c, color='red')

plt.show()

Comparison Between the Analog and Digital Filter

We can easily plot the Bode plots of the two filters on top of each other, in order to compare their properties.

from scipy.signal import butter, freqz, freqs
import matplotlib.pyplot as plt
from math import pi
import numpy as np

f_s = 360    # Sample frequency in Hz
f_c = 45     # Cut-off frequency in Hz
order = 4    # Order of the butterworth filter

omega_c = 2 * pi * f_c       # Cut-off angular frequency
omega_c_d = omega_c / f_s    # Normalized cut-off frequency (digital)

# Design the digital Butterworth filter
b_d, a_d = butter(order, omega_c_d / pi)    
print('Digital Coefficients')
print("b =", b_d)                         # Print the coefficients
print("a =", a_d)

w, H_d = freqz(b_d, a_d, 4096)            # Calculate the frequency response
w *= f_s / (2 * pi)                       # Convert from rad/sample to Hz

# Design the analog Butterworth filter
b_a, a_a = butter(order, f_c, analog=True)
print('Analog Coefficients')
print("b =", b_a)                         # Print the coefficients
print("a =", a_a)

w, H_a = freqs(b_a, a_a, w)               # Calculate the frequency response

# Plot the amplitude response
plt.subplot(2, 1, 1)            
plt.suptitle('Bode Plot')
H_d_dB = 20 * np.log10(abs(H_d))          # Convert modulus of H to dB
H_a_dB = 20 * np.log10(abs(H_a))
plt.plot(w, H_d_dB, color='blue', label='Digital')
plt.plot(w, H_a_dB, color='green', label='Analog')
plt.legend()
plt.ylabel('Magnitude [dB]')
plt.xlim(0, f_s / 2)
plt.ylim(-80, 6)
plt.axvline(f_c, color='red')
plt.axhline(-3, linewidth=0.8, color='black', linestyle=':')

# Plot the phase response
plt.subplot(2, 1, 2)
phi_d = np.angle(H_d)                     # Argument of H
phi_a = np.angle(H_a)
phi_d = np.unwrap(phi_d) * 180 / pi       # Remove discontinuities
phi_a = np.unwrap(phi_a) * 180 / pi       # and convert to degrees
plt.plot(w, phi_d, color='blue')
plt.plot(w, phi_a, color='green')  
plt.xlabel('Frequency [Hz]')
plt.ylabel('Phase [°]')
plt.xlim(0, f_s / 2)
plt.ylim(-360, 0)
plt.yticks([-360, -270, -180, -90, 0])
plt.axvline(f_c, color='red')

plt.show()