Exercise 12 (11.12.2024)

Solution to Problem 1

Christian Enz

Swiss Federal Institute of Technology (EPFL), Lausanne, Switzerland

Initialization¶

In [2]:
from functions import *

import numpy as np
from numpy import pi as pi
from numpy import log as ln
from numpy import log10 as log
from numpy import sqrt as sqrt
from numpy import exp as exp
from numpy import arctan as atan
import pandas as pd
import matplotlib.pyplot as plt
import subprocess
import os
import os.path as op
import sys
import sys
import re

from matplotlib.ticker import EngFormatter
import matplotlib.patches as patches
from IPython.display import display, Latex

plt.style.use('plt_style.mplstyle')
sys.path.append(".")
inkscapePath = r"C:\Program Files\Inkscape\bin\inkscape.exe"
savePath = "./Plots/"

#plt.rcParams['text.usetex'] = True
plt.rcParams['svg.fonttype'] = 'none'
plt.rcParams['pdf.fonttype'] = 42
#plt.rcParams['ps.fonttype'] = 42
plt.rcParams['font.family'] = 'Arial'
#plt.rcParams['mathtext.fontset'] = 'cm'

plt.rcParams['mathtext.fontset'] = 'custom'
plt.rcParams['mathtext.rm'] = 'Arial'
plt.rcParams['mathtext.it'] = 'Arial:italic'
plt.rcParams['mathtext.bf'] = 'Arial:bold'

def saveFigures(savePath, plotName):
    print("\nPatience, saving plot!")
    figFolder = savePath + r"\{}.{}"
    pngFile = figFolder.format(plotName,"png")
    pdfFile = figFolder.format(plotName,"pdf")
    svgFile = figFolder.format(plotName,"svg")
    plt.savefig(pngFile, format="png", dpi=1200, bbox_inches="tight", transparent=True)
    plt.savefig(pdfFile, format="pdf", dpi=1200, bbox_inches="tight", transparent=True)
    plt.savefig(svgFile, format="svg", dpi=1200, bbox_inches="tight", transparent=True)
    cmd_emf = '"{}" "{}\\{}.svg" --export-filename="{}\\{}.emf" --export-overwrite'.format(inkscapePath, savePath, plotName, savePath, plotName)
#    cmd_emf = '"{}" "{}\{}.pdf" --export-filename="{}\{}.emf" --export-overwrite'.format(inkscapePath, savePath, plotName, savePath, plotName)
#    cmd_pdf = '"{}" "{}\{}.svg" --export-filename="{}\{}.pdf" --export-overwrite'.format(inkscapePath, savePath, plotName, savePath, plotName)
    subprocess.call(cmd_emf, shell=True)
#    subprocess.call(cmd_pdf, shell=True)

kB=1.38E-23
q=1.6E-19
T=300
kT=kB*T
UT=kT/q

Introduction¶

In this problem, we want to design a low-pass (LP) switched-capacitor (SC) filter using the Chebyshev approximation and the cascade approach.

Specifications¶

No description has been provided for this image

LP filter specifications.

The filter mask is shown in the above figure and the corresponding specifications are given below

In [3]:
fp=20e3
wp=2*pi*fp
fs=120e3
ws=2*pi*fs
fck=2e6
Gp=-1
Gs=-40
display(Latex(f'$f_p =$ ' + f'{fp/1e3:.0f}' + ' kHz'))
display(Latex(f'$f_s =$ ' + f'{fs/1e3:.0f}' + ' kHz'))
display(Latex(f'$f_{{ck}} =$ ' + f'{fck/1e6:.0f}' + ' MHz'))
display(Latex(f'$G_p =$ ' + f'{Gp:.0f}' + ' dB'))
display(Latex(f'$G_s =$ ' + f'{Gs:.0f}' + ' dB'))
$f_p =$ 20 kHz
$f_s =$ 120 kHz
$f_{ck} =$ 2 MHz
$G_p =$ -1 dB
$G_s =$ -40 dB

where $f_{ck}$ is the sampling (or clock) frequency.

The fillter mask is shown below.

In [4]:
fmin=1e3
fmax=1e6

params = {'figure.figsize': (7,8),
          'axes.labelsize': 18}
plt.rcParams.update(params)

fig, axs = plt.subplots(2, sharex=True)

axs[0].set(ylabel='$|T(\\Omega)|$ [dB]',ylim=(-50,10))
axs[0].add_patch(patches.Rectangle((fs, -40), 980000, 50, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[0].add_patch(patches.Rectangle((fmin, 0), 999000, 10, fill=True, color='0.9'))
axs[0].add_patch(patches.Rectangle((fmin, -50), 19000, 49, fill=True, facecolor='0.9', edgecolor='0.7'))

axs[1].set(xscale='log', xlabel='Frequency [Hz] (log)', xlim=(fmin,fmax))
axs[1].set(ylabel='$|T(f)|$ [dB]', ylim=(-4,1))
axs[1].add_patch(patches.Rectangle((fs, -4), 980000, 5, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].add_patch(patches.Rectangle((fmin, 0), 999000, 1, fill=True, color='0.9')) 
axs[1].add_patch(patches.Rectangle((fmin, -4), 19000, 3, fill=True, facecolor='0.9', edgecolor='0.7')) 

plt.subplots_adjust(hspace=0.1)
#saveFigures(savePath, 'Input_referred_noise')
plt.show()
No description has been provided for this image

We now will find the minimum filter order $N$ for matching the above mask with a Chebyshev approximation.

In [5]:
Ap=-Gp
As=-Gs
Omegas=fs/fp
epsilon=sqrt(pow(10,Ap/10)-1)
Nest=Nestimate(Omegas,Ap,As)
N=np.ceil(Nest)
Omega0=pow(1/epsilon,1/N)
display(Latex(f'$\\Omega_s =$ ' + f'{Omegas:.0f}'))
display(Latex(f'$A_p =$ ' + f'{Ap:.0f}' + ' dB'))
display(Latex(f'$A_s =$ ' + f'{As:.0f}' + ' dB'))
display(Latex(f'$\\epsilon =$ ' + f'{epsilon:.9f}'))
display(Latex(f'$N =$ ' + f'{Nest:.9f} (estimation)'))
display(Latex(f'$N =$ ' + f'{N:.0f}'))
$\Omega_s =$ 6
$A_p =$ 1 dB
$A_s =$ 40 dB
$\epsilon =$ 0.508847140
$N =$ 2.411571246 (estimation)
$N =$ 3

The corresponding transfer function magnitude is shown below.

In [6]:
Npts=1001
logfmin=3
logfmax=6
fmin=pow(10,logfmin)
fmax=pow(10,logfmax)
fthe=np.logspace(logfmin,logfmax,Npts,endpoint=True,base=10.0)

Omega=np.zeros(Npts)
Tmagthe=np.zeros(Npts)
TmagdBthe=np.zeros(Npts)

for k in range(0,Npts):
    Omega[k]=fthe[k]/fp
    Tmagthe[k]=Tche(Omega[k],epsilon,N)
    TmagdBthe[k]=20*log(Tmagthe[k])

params = {'figure.figsize': (7,8),
          'axes.labelsize': 18}
plt.rcParams.update(params)
    
fig, axs = plt.subplots(2, sharex=True)

axs[0].semilogx(fthe,TmagdBthe,'r-')
axs[0].set(ylabel='$|T(f)|$ [dB]',ylim=(-50,10))
axs[0].add_patch(patches.Rectangle((fs, -40), 980000, 50, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[0].add_patch(patches.Rectangle((fmin, 0), 999000, 10, fill=True, color='0.9'))
axs[0].add_patch(patches.Rectangle((fmin, -50), 19000, 49, fill=True, facecolor='0.9', edgecolor='0.7'))

axs[1].semilogx(fthe,TmagdBthe,'r-')
axs[1].set(xlabel='Frequency [Hz] (log)',xlim=(fmin,fmax))
axs[1].set(ylabel='$|T(f))|$ [dB]',ylim=(-4,1))
axs[1].add_patch(patches.Rectangle((fs, -4), 980000, 5, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].add_patch(patches.Rectangle((fmin, 0), 999000, 1, fill=True, color='0.9')) 
axs[1].add_patch(patches.Rectangle((fmin, -4), 19000, 3, fill=True, facecolor='0.9', edgecolor='0.7')) 

plt.subplots_adjust(hspace=0.1)
#saveFigures(savePath, 'Chebyshev_TF')
plt.show()
No description has been provided for this image

The corresponding normalized transfer function is shown below.

In [7]:
Npts=1001
logOmegamin=-1
logOmegamax=1
Omegamin=pow(10,logOmegamin)
Omegamax=pow(10,logOmegamax)
Omega=np.logspace(logOmegamin,logOmegamax,Npts,endpoint=True,base=10.0)

Tmagnorm=np.zeros(Npts)
TmagdBnorm=np.zeros(Npts)

for k in range(0,Npts):
    Tmagnorm[k]=Tche(Omega[k],epsilon,N)
    TmagdBnorm[k]=20*log(Tmagnorm[k])

params = {'figure.figsize': (7,8),
          'axes.labelsize': 18}
plt.rcParams.update(params)
    
fig, axs = plt.subplots(2, sharex=True)

axs[0].semilogx(Omega,TmagdBnorm,'r-')
axs[0].set(ylabel='$|T(\\Omega)|$',ylim=(-50,10))
axs[0].add_patch(patches.Rectangle((Omegas, -40), 4, 50, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[0].add_patch(patches.Rectangle((Omegamin, 0), 9.9, 10, fill=True, color='0.9'))
axs[0].add_patch(patches.Rectangle((Omegamin, -50), 0.9, 49, fill=True, facecolor='0.9', edgecolor='0.7'))

axs[1].semilogx(Omega,TmagdBnorm,'r-')
axs[1].set(xlabel='$\\Omega = \\omega / \\omega_p$',xlim=(Omegamin,Omegamax))
axs[1].set(ylabel='$|T(\\Omega)|$',ylim=(-4,1))
axs[1].add_patch(patches.Rectangle((Omegas, -4), 4, 5, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].add_patch(patches.Rectangle((Omegamin, 0), 9.9, 1, fill=True, color='0.9')) 
axs[1].add_patch(patches.Rectangle((Omegamin, -4), 0.9, 3, fill=True, facecolor='0.9', edgecolor='0.7')) 

plt.subplots_adjust(hspace=0.1)
#saveFigures(savePath, 'Input_referred_noise')
plt.show()
No description has been provided for this image

It can be shown that the factorized transfer function is then given by \begin{equation*} T(s) = \frac{\omega_{p1}}{s+\omega_{p1}} \cdot \frac{\omega_0^2}{s^2 + \tfrac{\omega_0}{Q}\,s + \omega_0^2}, \end{equation*} where \begin{align*} \omega_{p1} &= 2\pi\;9.883412099\,krad/s,\\ \omega_0 &= 2\pi\;19.941961657\,krad/s,\\ Q &= 2.017720344. \end{align*}

In [8]:
fp1=9.883412099e3
wp1=2*pi*fp1
f0=19.941961657e3
w0=2*pi*f0
Q=2.017720344
display(Latex(f'$f_{{p1}} =$ ' + f'{fp1/1e3:.6f}' + ' kHz'))
display(Latex(f'$\\omega_{{p1}} =$ ' + f'{wp1/1e3:.6f}' + ' krad/s'))
display(Latex(f'$f_0 =$ ' + f'{f0/1e3:.6f}' + ' kHz'))
display(Latex(f'$\\omega_0 =$ ' + f'{w0/1e3:.6f}' + ' krad/s'))
display(Latex(f'$Q =$ ' + f'{Q:.6f}'))
$f_{p1} =$ 9.883412 kHz
$\omega_{p1} =$ 62.099310 krad/s
$f_0 =$ 19.941962 kHz
$\omega_0 =$ 125.299040 krad/s
$Q =$ 2.017720

We can check that the magnitude of the factorized transfer function actually corresponds to that of the Chebyshev approximation.

In [9]:
def Tfact(s,wp1,w0,Q):
    T1=wp1/(s+wp1)
    T2=w0**2/(s**2+w0/Q*s+w0**2)
    return T1*T2

Npts=1001
logfmin=3
logfmax=6
fmin=pow(10,logfmin)
fmax=pow(10,logfmax)
ffact=np.logspace(logfmin,logfmax,Npts,endpoint=True,base=10.0)

TmagdBfact=np.zeros(Npts)
for k in range(0,Npts):
    jw=1j*2*pi*ffact[k]
    TmagdBfact[k]=20*log(abs(Tfact(jw,wp1,w0,Q)))

params = {'figure.figsize': (7,8),
          'axes.labelsize': 18}
plt.rcParams.update(params)
    
fig, axs = plt.subplots(2, sharex=True)

axs[0].semilogx(ffact,TmagdBfact,'ro', markersize=4, markevery=15)
axs[0].semilogx(fthe,TmagdBthe,'b-')
axs[0].set(ylabel='$|T(f)|$ [dB]',ylim=(-50,10))
axs[0].add_patch(patches.Rectangle((fs, -40), 980000, 50, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[0].add_patch(patches.Rectangle((fmin, 0), 999000, 10, fill=True, color='0.9'))
axs[0].add_patch(patches.Rectangle((fmin, -50), 19000, 49, fill=True, facecolor='0.9', edgecolor='0.7'))

axs[1].semilogx(ffact,TmagdBfact,'ro', markersize=4, markevery=10)
axs[1].semilogx(fthe,TmagdBthe,'b-')
axs[1].set(xlabel='Frequency [Hz] (log)',xlim=(fmin,fmax))
axs[1].set(ylabel='$|T(f))|$ [dB]',ylim=(-4,1))
axs[1].add_patch(patches.Rectangle((fs, -4), 980000, 5, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].add_patch(patches.Rectangle((fmin, 0), 999000, 1, fill=True, color='0.9')) 
axs[1].add_patch(patches.Rectangle((fmin, -4), 19000, 3, fill=True, facecolor='0.9', edgecolor='0.7')) 

plt.subplots_adjust(hspace=0.1)
#saveFigures(savePath, 'Input_referred_noise')
plt.show()
No description has been provided for this image

The 1st-order section can be implemented with the SC schematic shown below

No description has been provided for this image

General 1st-order section.

which has a transfer function given by \begin{equation*} H(z) = -\frac{\alpha_1 \cdot z - (\alpha_1+\alpha_2)}{(1+\alpha_3) \cdot z - 1} \end{equation*}

In the case of the LP filter we can remove capacitor $\alpha_1 C$ leading to the schematic shown below.

No description has been provided for this image

1st-order LP section.

The transfer function then simplifies to \begin{equation*} H(z) = \frac{\alpha_2}{(1+\alpha_3) \cdot z - 1} \end{equation*}

The design equations assuming that $\omega_z\,T \ll 1$ and $\omega_{p1}\,T \ll 1$ are simply \begin{equation*} \alpha_2 = \alpha_3 = \omega_{p1} \cdot T. \end{equation*} where $T=1/f_{ck}$.

In [10]:
T=1/fck
alpha12=wp1*T
alpha13=alpha12
display(Latex(f'$\\alpha_2 =$ ' + f'{alpha12:.6f}'))
display(Latex(f'$\\alpha_3 =$ ' + f'{alpha12:.6f}'))
$\alpha_2 =$ 0.031050
$\alpha_3 =$ 0.031050

The transfer function of the 1st-order section is plotted below.

In [11]:
def H1(z,alpha2,alpha3):
    num=alpha2
    den=(1+alpha3)*z-1
    return num/den

Npts=101
logfmin=3
logfmax=6
fmin=pow(10,logfmin)
fmax=pow(10,logfmax)
fthe=np.logspace(logfmin,logfmax,Npts,endpoint=True,base=10.0)

Hsc1the=np.zeros(Npts,dtype=complex)
Hsc1dBthe=np.zeros(Npts)

for k in range(0,Npts):
    jwT=1j*2*pi*fthe[k]*T
    z=exp(jwT)
    Hsc1the[k]=H1(z,alpha12,alpha13)
    Hsc1dBthe[k]=20*log(abs(Hsc1the[k]))

plt.style.use('plt_style.mplstyle')
plt.semilogx(fthe,Hsc1dBthe,'r-')
plt.xlim(fmin,fmax)
#plt.xticks([1e-1,1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8])
plt.xlabel('Frequency [Hz]')
#plt.ylim(-150,-80)
plt.ylabel('Frequency [Hz] (log)')
#plt.legend(loc='upper right', fontsize=12)
#textstr = '\n'.join((
#    r'$\\alpha_2 =$'+f'{alpha2:.3f}',
#    r'$\\alpha_3 =$'+f'{alpha3:.3f}'))
#plt.text(0.55, 0.7, textstr, ha='left', va='top', size=14, transform=plt.gca().transAxes,
#         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
# saveFigures(savePath, 'Input_referred_noise')
plt.show()
No description has been provided for this image

The 2nd-order section can be implemented by the circuit shown below

No description has been provided for this image

General 2nd-order section.

which has the following $z$ transfer function \begin{equation*} H(z) = -\frac{\beta_1 z^2 -(2\,\beta_1-\alpha_1 \cdot \beta_2) \cdot z + \beta_1}{(1+\beta_3) \cdot z^2 -(2-\alpha_2 \cdot \beta_2 + \beta_3) \cdot z + 1} \end{equation*}

For the implementation of LP filters we don't need capacitance $\beta_1\,C_2$. The circuit simplifies to the one shown below

No description has been provided for this image

LP 2nd-order section.

The transfer function then simplifies to \begin{equation*} H(z) = -\frac{\alpha_1 \cdot \beta_2 \cdot z}{(1+\beta_3) \cdot z^2 - (2-\alpha_2 \cdot \beta_2 + \beta_3) \cdot z + 1} \end{equation*} with \begin{align*} \alpha_1 &= \alpha_2 = \beta_2 =\omega_0\,T,\\ \beta_3 &= \frac{\omega_0\,T}{Q}. \end{align*}

The transfer function of the 2nd-order section is shown below.

In [12]:
alpha21=w0*T
alpha22=w0*T
beta22=w0*T
beta23=w0*T/Q
display(Latex(f'$\\alpha_1 =$ ' + f'{alpha21:.6f}'))
display(Latex(f'$\\alpha_2 =$ ' + f'{alpha22:.6f}'))
display(Latex(f'$\\beta_2 =$ ' + f'{beta22:.6f}'))
display(Latex(f'$\\beta_3 =$ ' + f'{beta23:.6f}'))
$\alpha_1 =$ 0.062650
$\alpha_2 =$ 0.062650
$\beta_2 =$ 0.062650
$\beta_3 =$ 0.031050
In [13]:
def H2(z,alpha1,alpha2,beta2,beta3):
    num=alpha1*beta2*z
    den=(1+beta3)*z**2-(2-alpha2*beta2+beta3)*z+1
    return -num/den

Npts=101
logfmin=3
logfmax=6
fmin=pow(10,logfmin)
fmax=pow(10,logfmax)
fthe=np.logspace(logfmin,logfmax,Npts,endpoint=True,base=10.0)

Hsc2the=np.zeros(Npts,dtype=complex)
Hsc2dBthe=np.zeros(Npts)

for k in range(0,Npts):
    jwT=1j*2*pi*fthe[k]*T
    z=exp(jwT)
    Hsc2the[k]=H2(z,alpha21,alpha22,beta22,beta23)
    Hsc2dBthe[k]=20*log(abs(Hsc2the[k]))

plt.style.use('plt_style.mplstyle')
plt.semilogx(fthe,Hsc2dBthe,'r-')
plt.xlim(fmin,fmax)
#plt.xticks([1e-1,1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8])
plt.xlabel('Frequency [Hz]')
#plt.ylim(-150,-80)
plt.ylabel('Frequency [Hz] (log)')
#plt.legend(loc='upper right', fontsize=12)
#textstr = '\n'.join((
#    r'$\\alpha_2 =$'+f'{alpha2:.3f}',
#    r'$\\alpha_3 =$'+f'{alpha3:.3f}'))
#plt.text(0.55, 0.7, textstr, ha='left', va='top', size=14, transform=plt.gca().transAxes,
#         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
# saveFigures(savePath, 'Input_referred_noise')
plt.show()
No description has been provided for this image

The schematic of the complete filter is shown below.

No description has been provided for this image

Schematic of the complete filter.

The transfer function of the complete filter is plotted below.

In [14]:
def Hsc(z,alpha12,alpha13,alpha21,alpha22,beta22,beta23):
    return H1(z,alpha12,alpha13)*H2(z,alpha21,alpha22,beta22,beta23)

Npts=101
logfmin=3
logfmax=6
fmin=pow(10,logfmin)
fmax=pow(10,logfmax)
fthe=np.logspace(logfmin,logfmax,Npts,endpoint=True,base=10.0)

Hscthe=np.zeros(Npts,dtype=complex)
HscdBthe=np.zeros(Npts)

for k in range(0,Npts):
    jwT=1j*2*pi*fthe[k]*T
    z=exp(jwT)
    Hscthe[k]=Hsc(z,alpha12,alpha13,alpha21,alpha22,beta22,beta23)
    HscdBthe[k]=20*log(abs(Hscthe[k]))

plt.style.use('plt_style.mplstyle')
plt.semilogx(fthe,HscdBthe,'r-')
plt.xlim(fmin,fmax)
#plt.xticks([1e-1,1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8])
plt.xlabel('Frequency [Hz]')
plt.ylim(-100,5)
plt.ylabel('Frequency [Hz] (log)')
#plt.legend(loc='upper right', fontsize=12)
#textstr = '\n'.join((
#    r'$\\alpha_2 =$'+f'{alpha2:.3f}',
#    r'$\\alpha_3 =$'+f'{alpha3:.3f}'))
#plt.text(0.55, 0.7, textstr, ha='left', va='top', size=14, transform=plt.gca().transAxes,
#         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
# saveFigures(savePath, 'Input_referred_noise')
plt.show()
No description has been provided for this image

We can compare it to the theoretical transfer function.

In [15]:
def Hsc(z,alpha12,alpha13,alpha21,alpha22,beta22,beta23):
    return H1(z,alpha12,alpha13)*H2(z,alpha21,alpha22,beta22,beta23)

Npts=1001
logfmin=3
logfmax=6
fmin=pow(10,logfmin)
fmax=pow(10,logfmax)
fthe=np.logspace(logfmin,logfmax,Npts,endpoint=True,base=10.0)

Hscthe=np.zeros(Npts,dtype=complex)
HscdBthe=np.zeros(Npts)
Tmagthe=np.zeros(Npts)
TmagdBthe=np.zeros(Npts)

for k in range(0,Npts):
    jwT=1j*2*pi*fthe[k]*T
    z=exp(jwT)
    Hscthe[k]=Hsc(z,alpha12,alpha13,alpha21,alpha22,beta22,beta23)
    HscdBthe[k]=20*log(abs(Hscthe[k]))
    Omega=fthe[k]/fp
    Tmagthe[k]=Tche(Omega,epsilon,N)
    TmagdBthe[k]=20*log(Tmagthe[k])

params = {'figure.figsize': (7,8),
          'axes.labelsize': 18}
plt.rcParams.update(params)
    
fig, axs = plt.subplots(2, sharex=True)

axs[0].semilogx(fthe,HscdBthe,'ro', markersize=4, markevery=15)
axs[0].semilogx(fthe,TmagdBthe,'b-')
axs[0].set(ylabel='$|T(f)|$ [dB]',ylim=(-50,10))
axs[0].add_patch(patches.Rectangle((fs, -40), 980000, 50, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[0].add_patch(patches.Rectangle((fmin, 0), 999000, 10, fill=True, color='0.9'))
axs[0].add_patch(patches.Rectangle((fmin, -50), 19000, 49, fill=True, facecolor='0.9', edgecolor='0.7'))

axs[1].semilogx(fthe,HscdBthe,'ro', markersize=4, markevery=8)
axs[1].semilogx(fthe,TmagdBthe,'b-')
axs[1].set(xlabel='Frequency [Hz] (log)',xlim=(fmin,fmax))
axs[1].set(ylabel='$|T(f))|$ [dB]',ylim=(-4,1))
axs[1].add_patch(patches.Rectangle((fs, -4), 980000, 5, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].add_patch(patches.Rectangle((fmin, 0), 999000, 1, fill=True, color='0.9')) 
axs[1].add_patch(patches.Rectangle((fmin, -4), 19000, 3, fill=True, facecolor='0.9', edgecolor='0.7')) 

plt.subplots_adjust(hspace=0.1)
#saveFigures(savePath, 'Chebyshev_TF')
plt.show()
No description has been provided for this image

We still need to choose the impedance level. Since we have very small capacitance ratios (because the sampling frequency was set much above the cut-off frequency), the switched-capacitors will be very small. However they need to be large enough in order to ensure a good matching and therefore a good control on the capacitance ratios. We can choose

In [16]:
Cmin=100e-15
display(Latex(f'$C_{{min}} =$ ' + f'{Cmin/1e-15:.0f}' + ' fF'))
$C_{min} =$ 100 fF

This capacitance corresponds to capacitors $\alpha_{12} C_1$ and $\alpha_{13} C_1$ of the 1st-order section and $\beta_{23} C_3$ of the 2nd-order section. From there we get the values of the integrating capacitances $C_1$ and $C_3$

In [17]:
Cmin=100e-15
C1=Cmin/alpha12
C3=Cmin/beta23
display(Latex(f'$C_1 =$ ' + f'{C1/1e-12:.6f}' + ' pF'))
display(Latex(f'$C_3 =$ ' + f'{C3/1e-12:.6f}' + ' pF'))
$C_1 =$ 3.220648 pF
$C_3 =$ 3.220648 pF

We finally choose

In [18]:
C1=3.2e-12
C2=3.2e-12
C3=3.2e-12
display(Latex(f'$C_1 =$ ' + f'{C1/1e-12:.1f}' + ' pF'))
display(Latex(f'$C_2 =$ ' + f'{C2/1e-12:.1f}' + ' pF'))
display(Latex(f'$C_3 =$ ' + f'{C3/1e-12:.1f}' + ' pF'))
$C_1 =$ 3.2 pF
$C_2 =$ 3.2 pF
$C_3 =$ 3.2 pF

Verification¶

The above design can unfortunately not be verified by a simple SPICE simulator. However, we have designed a special library that allows for the the simulation of two non-overlapping phases switched-capacitor circuits like the one we have designed. Without going into the details of the implementation of this library, what it does is to simulate the circuits in phase $\Phi_1$ and $\Phi_2$ concurently and then insure the charge conservation at the virtual gound nodes betwen the two phases. The library was implemented for LTSpice taking advantage of the Laplace instruction. Unfortunately the latter is not available in ngspice, reason for which we have done the simulation in LTSpice.

The SC components are part of my analog.lib library. To install the library follow the instructions given in the moodle site. To be able to run the LTSpice simulation from the Jupyter Notebook, you need to install the PyLTSpice package. Refer to the instructions given in PyLTSpice site

The simulations were done with the following schematic.

No description has been provided for this image

Schematic used for the LTSpice simulation.

with the capacitance values and capacitance ratios given below

In [19]:
A=1e5
AdB=20*log(A)
display(Latex(f'$A =$ ' + f'{AdB:.0f}' + ' dB'))
display(Latex(f'$f_{{ck}} =$ ' + f'{fck/1e6:.0f}' + ' MHz'))
display(Latex(f'$C_1 =$ ' + f'{C1/1e-12:.1f}' + ' pF'))
display(Latex(f'$C_2 =$ ' + f'{C2/1e-12:.1f}' + ' pF'))
display(Latex(f'$C_3 =$ ' + f'{C3/1e-12:.1f}' + ' pF'))
display(Latex(f'$\\alpha_{{12}} =$ ' + f'{alpha12:.9f}'))
display(Latex(f'$\\alpha_{{13}} =$ ' + f'{alpha13:.9f}'))
display(Latex(f'$\\alpha_{{21}} =$ ' + f'{alpha21:.9f}'))
display(Latex(f'$\\alpha_{{22}} =$ ' + f'{alpha22:.9f}'))
display(Latex(f'$\\beta_{{22}} =$ ' + f'{beta22:.9f}'))
display(Latex(f'$\\beta_{{23}} =$ ' + f'{beta23:.9f}'))
$A =$ 100 dB
$f_{ck} =$ 2 MHz
$C_1 =$ 3.2 pF
$C_2 =$ 3.2 pF
$C_3 =$ 3.2 pF
$\alpha_{12} =$ 0.031049655
$\alpha_{13} =$ 0.031049655
$\alpha_{21} =$ 0.062649520
$\alpha_{22} =$ 0.062649520
$\beta_{22} =$ 0.062649520
$\beta_{23} =$ 0.031049655
In [20]:
from PyLTSpice import SimRunner, SpiceEditor, RawRead

# select spice model
LTC = SimRunner(output_folder='./Simulations/LTSpice/')
LTC.create_netlist('./Simulations/LTSpice/SC_3rd_order_LPF_cascade.asc')
netlist = SpiceEditor('./Simulations/LTSpice/SC_3rd_order_LPF_cascade.net')
# set default arguments
print("Creating the netlist...")
netlist.set_parameters(A=A, fck=fck, C1=C1, C2=C2, C3=C3, a12=alpha12, a13=alpha13, a21=alpha21, a22=alpha22, b22=beta22, b23=beta23)
#netlist.set_component_value('R2', '2k')  # Modifying the value of a resistor
#netlist.set_component_value('R1', '4k')
#netlist.set_element_model('V3', "SINE(0 1 3k 0 0 0)")  # Modifying the
#netlist.set_component_value('XU1:C2', 20e-12)  # modifying a define simulation
#netlist.add_instructions(
#    "; Simulation settings",
#    ";.param run = 0"
#)

print("Simulating the circuit...")
LTC.run(netlist)
LTC.wait_completion()

print('Successful/Total Simulations: ' + str(LTC.okSim) + '/' + str(LTC.runno))

print("Reading data...")
raw = RawRead('./Simulations/LTSpice/SC_3rd_order_LPF_cascade_1.raw')   # Read the RAW file contents from disk

#print(raw.get_trace_names())            # Get and print a list of all the traces
#print(raw.get_raw_property())           # Print all the properties found in the Header section

vout1 = raw.get_trace('V(out1)')            # Get the trace data
vout2 = raw.get_trace('V(out2)')          # Get the second trace

xdata = np.real(raw.get_axis())                  # Get the X-axis data (time)
ydata = 20*log(abs(vout1.get_wave()))                  # Get all the values for the 'vin' trace
#ydata = vout2.get_wave()                 # Get all the values for the 'vout' trace

params = {'figure.figsize': (7,8),
          'axes.labelsize': 18}
plt.rcParams.update(params)
    
fig, axs = plt.subplots(2, sharex=True)

axs[0].semilogx(xdata,ydata,'ro', label='Simulation', markersize=4, markevery=15)
axs[0].semilogx(fthe,HscdBthe,'b-', label='Theory')
axs[0].set(ylabel='$|T(f)|$ [dB]',ylim=(-50,10))
axs[0].add_patch(patches.Rectangle((fs, -40), 980000, 50, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[0].add_patch(patches.Rectangle((fmin, 0), 999000, 10, fill=True, color='0.9'))
axs[0].add_patch(patches.Rectangle((fmin, -50), 19000, 49, fill=True, facecolor='0.9', edgecolor='0.7'))
textstr1 = '\n'.join((
    r'$A =$'+f'{AdB:.0f} dB',
    r'$f_{ck} =$'+f'{fck/1e6:.0f} MHz',
    r'$C_1 =$'+f'{C1/1e-12:.1f} pF',
    r'$C_2 =$'+f'{C2/1e-12:.1f} pF',
    r'$C_3 =$'+f'{C3/1e-12:.1f} pF'))
axs[0].text(0.76, 0.93, textstr1, ha='left', va='top', transform=axs[0].transAxes, size=12,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
axs[0].legend(loc='lower left')

axs[1].semilogx(xdata,ydata,'ro', label='Simulation', markersize=4, markevery=15)
axs[1].semilogx(fthe,HscdBthe,'b-', label='Theory')
axs[1].set(xlabel='Frequency [Hz] (log)',xlim=(fmin,fmax))
axs[1].set(ylabel='$|T(f))|$ [dB]',ylim=(-4,1))
axs[1].add_patch(patches.Rectangle((fs, -4), 980000, 5, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].add_patch(patches.Rectangle((fmin, 0), 999000, 1, fill=True, color='0.9')) 
axs[1].add_patch(patches.Rectangle((fmin, -4), 19000, 3, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
    r'$\alpha_{12} =$'+f'{alpha12:.6f}',
    r'$\alpha_{13} =$'+f'{alpha13:.6f}',
    r'$\alpha_{21} =$'+f'{alpha21:.6f}',
    r'$\alpha_{22} =$'+f'{alpha22:.6f}',
    r'$\beta_{22} =$'+f'{beta22:.6f}',
    r'$\beta_{23} =$'+f'{beta23:.6f}'))
axs[1].text(0.75, 0.93, textstr2, ha='left', va='top', transform=axs[1].transAxes, size=12,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})

plt.subplots_adjust(hspace=0.1)
#saveFigures(savePath, 'Chebyshev_TF')
plt.show()
Creating the netlist...
Simulating the circuit...
Successful/Total Simulations: 1/1
Reading data...
No description has been provided for this image

The simulation matches the theoretical transfer function.

We can see the impact of the OPAMP finite gain.

In [21]:
A=1e2
AdB=20*log(A)
display(Latex(f'$A =$ ' + f'{AdB:.0f}' + ' dB'))
display(Latex(f'$f_{{ck}} =$ ' + f'{fck/1e6:.0f}' + ' MHz'))
display(Latex(f'$C_1 =$ ' + f'{C1/1e-12:.0f}' + ' pF'))
display(Latex(f'$C_2 =$ ' + f'{C2/1e-12:.0f}' + ' pF'))
display(Latex(f'$C_3 =$ ' + f'{C3/1e-12:.0f}' + ' pF'))
display(Latex(f'$\\alpha_{{12}} =$ ' + f'{alpha12:.9f}'))
display(Latex(f'$\\alpha_{{13}} =$ ' + f'{alpha13:.9f}'))
display(Latex(f'$\\alpha_{{21}} =$ ' + f'{alpha21:.9f}'))
display(Latex(f'$\\alpha_{{22}} =$ ' + f'{alpha22:.9f}'))
display(Latex(f'$\\beta_{{22}} =$ ' + f'{beta22:.9f}'))
display(Latex(f'$\\beta_{{23}} =$ ' + f'{beta23:.9f}'))
$A =$ 40 dB
$f_{ck} =$ 2 MHz
$C_1 =$ 3 pF
$C_2 =$ 3 pF
$C_3 =$ 3 pF
$\alpha_{12} =$ 0.031049655
$\alpha_{13} =$ 0.031049655
$\alpha_{21} =$ 0.062649520
$\alpha_{22} =$ 0.062649520
$\beta_{22} =$ 0.062649520
$\beta_{23} =$ 0.031049655
In [22]:
from PyLTSpice import SimRunner, SpiceEditor, RawRead

# select spice model
LTC = SimRunner(output_folder='./Simulations/LTSpice/')
LTC.create_netlist('./Simulations/LTSpice/SC_3rd_order_LPF_cascade.asc')
netlist = SpiceEditor('./Simulations/LTSpice/SC_3rd_order_LPF_cascade.net')
# set default arguments
print("Creating the netlist...")
netlist.set_parameters(A=A, fck=fck, C1=C1, C2=C2, C3=C3, a12=alpha12, a13=alpha13, a21=alpha21, a22=alpha22, b22=beta22, b23=beta23)
#netlist.set_component_value('R2', '2k')  # Modifying the value of a resistor
#netlist.set_component_value('R1', '4k')
#netlist.set_element_model('V3', "SINE(0 1 3k 0 0 0)")  # Modifying the
#netlist.set_component_value('XU1:C2', 20e-12)  # modifying a define simulation
#netlist.add_instructions(
#    "; Simulation settings",
#    ";.param run = 0"
#)

print("Simulating the circuit...")
LTC.run(netlist)
LTC.wait_completion()

print('Successful/Total Simulations: ' + str(LTC.okSim) + '/' + str(LTC.runno))

print("Reading data...")
raw = RawRead('./Simulations/LTSpice/SC_3rd_order_LPF_cascade_1.raw')   # Read the RAW file contents from disk

#print(raw.get_trace_names())            # Get and print a list of all the traces
#print(raw.get_raw_property())           # Print all the properties found in the Header section

vout1 = raw.get_trace('V(out1)')            # Get the trace data
vout2 = raw.get_trace('V(out2)')          # Get the second trace

xdata = np.real(raw.get_axis())                  # Get the X-axis data (time)
ydata = 20*log(abs(vout1.get_wave()))                  # Get all the values for the 'vin' trace
#ydata = vout2.get_wave()                 # Get all the values for the 'vout' trace

params = {'figure.figsize': (7,8),
          'axes.labelsize': 18}
plt.rcParams.update(params)
    
fig, axs = plt.subplots(2, sharex=True)

axs[0].semilogx(xdata,ydata,'ro', label='Simulation', markersize=4, markevery=15)
axs[0].semilogx(fthe,HscdBthe,'b-', label='Theory')
axs[0].set(ylabel='$|T(f)|$ [dB]',ylim=(-50,10))
axs[0].add_patch(patches.Rectangle((fs, -40), 980000, 50, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[0].add_patch(patches.Rectangle((fmin, 0), 999000, 10, fill=True, color='0.9'))
axs[0].add_patch(patches.Rectangle((fmin, -50), 19000, 49, fill=True, facecolor='0.9', edgecolor='0.7'))
textstr1 = '\n'.join((
    r'$A =$'+f'{AdB:.0f} dB',
    r'$f_{ck} =$'+f'{fck/1e6:.0f} MHz',
    r'$C_1 =$'+f'{C1/1e-12:.1f} pF',
    r'$C_2 =$'+f'{C2/1e-12:.1f} pF',
    r'$C_3 =$'+f'{C3/1e-12:.1f} pF'))
axs[0].text(0.76, 0.93, textstr1, ha='left', va='top', transform=axs[0].transAxes, size=12,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
axs[0].legend(loc='lower left')

axs[1].semilogx(xdata,ydata,'ro-', label='Simulation', markersize=4, markevery=15)
axs[1].semilogx(fthe,HscdBthe,'b-', label='Theory')
axs[1].set(xlabel='Frequency [Hz] (log)',xlim=(fmin,fmax))
axs[1].set(ylabel='$|T(f))|$ [dB]',ylim=(-4,1))
axs[1].add_patch(patches.Rectangle((fs, -4), 980000, 5, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].add_patch(patches.Rectangle((fmin, 0), 999000, 1, fill=True, color='0.9')) 
axs[1].add_patch(patches.Rectangle((fmin, -4), 19000, 3, fill=True, facecolor='0.9', edgecolor='0.7'))
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
    r'$\alpha_{12} =$'+f'{alpha12:.6f}',
    r'$\alpha_{13} =$'+f'{alpha13:.6f}',
    r'$\alpha_{21} =$'+f'{alpha21:.6f}',
    r'$\alpha_{22} =$'+f'{alpha22:.6f}',
    r'$\beta_{22} =$'+f'{beta22:.6f}',
    r'$\beta_{23} =$'+f'{beta23:.6f}'))
axs[1].text(0.75, 0.93, textstr2, ha='left', va='top', transform=axs[1].transAxes, size=12,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})

plt.subplots_adjust(hspace=0.1)
#saveFigures(savePath, 'Chebyshev_TF')
plt.show()
Creating the netlist...
Simulating the circuit...
Successful/Total Simulations: 1/1
Reading data...
No description has been provided for this image

We can observe some losses in the passband.