Exercise 12 (11.12.2024)
Solution to Problem 1
Christian Enz
Swiss Federal Institute of Technology (EPFL), Lausanne, Switzerland
Initialization¶
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¶
LP filter specifications.
The filter mask is shown in the above figure and the corresponding specifications are given below
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'))
where $f_{ck}$ is the sampling (or clock) frequency.
The fillter mask is shown below.
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()
We now will find the minimum filter order $N$ for matching the above mask with a Chebyshev approximation.
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}'))
The corresponding transfer function magnitude is shown below.
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()
The corresponding normalized transfer function is shown below.
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()
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*}
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}'))
We can check that the magnitude of the factorized transfer function actually corresponds to that of the Chebyshev approximation.
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()
The 1st-order section can be implemented with the SC schematic shown below
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.
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}$.
T=1/fck
alpha12=wp1*T
alpha13=alpha12
display(Latex(f'$\\alpha_2 =$ ' + f'{alpha12:.6f}'))
display(Latex(f'$\\alpha_3 =$ ' + f'{alpha12:.6f}'))
The transfer function of the 1st-order section is plotted below.
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()
The 2nd-order section can be implemented by the circuit shown below
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
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.
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}'))
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()
The schematic of the complete filter is shown below.
Schematic of the complete filter.
The transfer function of the complete filter is plotted below.
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()
We can compare it to the theoretical transfer function.
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()
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
Cmin=100e-15
display(Latex(f'$C_{{min}} =$ ' + f'{Cmin/1e-15:.0f}' + ' 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$
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'))
We finally choose
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'))
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.
Schematic used for the LTSpice simulation.
with the capacitance values and capacitance ratios given below
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}'))
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...
The simulation matches the theoretical transfer function.
We can see the impact of the OPAMP finite gain.
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}'))
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...
We can observe some losses in the passband.