note

Low-Pass Filter on a Squarewave

A square wave is rich in odd harmonics, its spectrum contains the fundamental frequency plus components at , , , with amplitudes decreasing as . Passing it through a low-pass RC filter progressively attenuates those harmonics, rounding the sharp edges and making the waveform increasingly sine-like as the cutoff frequency drops. This is a useful exercise for understanding filter behaviour, and also a practical technique: a PWM output from a microcontroller can be converted to an analogue voltage using nothing more than a resistor and a capacitor.

In this notebook, the square wave is set to . The RC filter is defined with and , giving a cutoff frequency of , about three times the signal frequency. The filter is then also tested with (, well above the signal) and (, below the signal) to illustrate the range of effects.

Transfer function of the RC low-pass filter

The output voltage is derived from the voltage divider formed by and :

import matplotlib.pyplot as plt
import numpy as np

def rc_low_pass_filter(f, R, C):
    omega = 2*np.pi*f
    return 1.0/(1j*R*omega*C+1.0)

Cutoff frequency

The cutoff frequency is defined by:

At , the filter attenuates the signal by , meaning the output amplitude drops to of the input. Above the attenuation increases at per decade (i.e. a ×10 increase in frequency halves the output amplitude again).

def rc_low_pass_cutoff(R, C):
    return 1.0/(2*np.pi*R*C)

Define the filter

R=1.0e3    # 1kOhm
C=1.0e-6   # 1µF

fc = rc_low_pass_cutoff(R, C)

print("Filter cut off is: {:7.2f} Hz".format(fc))

Filter cut off is: 159.15 Hz

Define the square wave

f = 50Hz

from scipy import signal

# Frequency of the square wave :
f = 50.0   # frequency in [Hz]
T = 1.0/f  # period of the signal in [s]

# Sample points :
samples = 2**15                     # a 2^N number of samples makes the FFT very fast
periods = 2                         # number of periods to show
delta_t = T * periods / samples     # time corresponding to one sample

t = np.linspace(0, 1/f * periods, samples, endpoint=False)

y = signal.square(2*np.pi*t*f)     # square wave, amplitude ±1 V, peak-to-peak = 2 V

plt.figure(figsize=(10,5))
plt.plot(1e3*t, y)             # Change the x-axis scale to ms by multiplying by 10^3
ax = plt.gca()
plt.grid(True)
plt.title("Square Wave")
plt.xlabel("time(ms)",position=(0.95,1))
plt.ylabel("signal(V)",position=(1,0.9))
plt.show()
50 Hz square wave, amplitude ±1 V
50 Hz square wave, amplitude ±1 V

Apply the filter to the square wave

The filter is applied in the frequency domain: the FFT of the square wave is multiplied bin-by-bin by the complex filter gain , then transformed back with an IFFT. This is mathematically equivalent to convolution in the time domain but far more efficient for long signals.

from scipy.fftpack import fft, ifft, fftfreq, fftshift

# helper function :
def apply_filter_and_plot(R, C, samples, delta_t, y):
    y_out = ifft(fft(y) * rc_low_pass_filter(fftfreq(samples, delta_t), R, C))
    plt.figure(figsize=(10,5))
    plt.plot(1e3*t,np.real(y_out))
    ax = plt.gca()
    plt.grid(True)
    plt.title("Square wave with LP filter. R={:7.2f} Ohms, C={:0.2f} uF, cutoff={:0.1f} Hz".format(R, C*1e6, rc_low_pass_cutoff(R, C)))
    plt.xlabel("time(ms)",position=(0.95,1))
    plt.ylabel("signal(V)",position=(1,0.9))
    plt.show()

, approximately the signal frequency (). The cutoff is close to the 3rd harmonic (), so the higher harmonics are significantly attenuated and the edges begin to round noticeably.

apply_filter_and_plot(R, C, samples, delta_t, y)
Square wave filtered at fc ≈ 159 Hz (C = 1µF)
Square wave filtered at fc ≈ 159 Hz (C = 1µF)

, well above the signal frequency (). The cutoff is far above the fundamental and most audible harmonics, so the filter has little effect and the square wave is largely preserved.

apply_filter_and_plot(R, C/10, samples, delta_t, y)
Square wave filtered at fc ≈ 1591 Hz (C = 0.1µF)
Square wave filtered at fc ≈ 1591 Hz (C = 0.1µF)

, below the signal frequency (). The cutoff is now below the fundamental itself, so even the first harmonic is attenuated. The output is nearly sinusoidal, only the fundamental survives with meaningful amplitude.

apply_filter_and_plot(R, C*10, samples, delta_t, y)
Square wave filtered at fc ≈ 15.9 Hz (C = 10µF)
Square wave filtered at fc ≈ 15.9 Hz (C = 10µF)

See also