Exercise 12 (11.12.2024)

Solution to Problem 2

Christian Enz

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

Initialization¶

In [20]:
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 [21]:
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) freuqency.

The fillter mask is shown below.

In [22]:
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 [23]:
Ap=-Gp
As=-Gs
Omegas=fs/fp
epsilon=sqrt(pow(10,Ap/10)-1)
Nest=Nestimate(Omegas,Ap,As)
N=int(np.ceil(Nest))
h=pow(1/epsilon+sqrt(1+(1/epsilon)**2),1/N)
xi=h-1/h
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}'))
display(Latex(f'$h =$ ' + f'{h:.9f}'))
display(Latex(f'$\\xi =$ ' + f'{xi:.9f}'))
$\Omega_s =$ 6
$A_p =$ 1 dB
$A_s =$ 40 dB
$\epsilon =$ 0.508847140
$N =$ 2.411571246 (estimation)
$N =$ 3
$h =$ 1.609609795
$\xi =$ 0.988341210

The corresponding transfer function magnitude is shown below.

In [24]:
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 [25]:
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

The low-pass prototype filter is shown below.

No description has been provided for this image

Low-pass prototype filter (LPPF).

The component values of the LPPF are given below.

In [26]:
Rterm='RS'
# Rterm='RL
#Rterm='test'
X=np.zeros(N)
Y=np.zeros(N)
CL=np.zeros(N)

for n in range(1,N+1):
    CL[n-1]=(16*sin((2*n-1)/(2*N)*pi)*sin((2*n+1)/(2*N)*pi))/(xi**2+(2*sin(n/N*pi))**2)

if Rterm=='RS':
# Starting from RS
    RS=1
    X[0]=4*sin(pi/(2*N))/(xi*RS)
    Y[N-1]=X[0]
    for n in range(1,N):
        X[n]=CL[n-1]/X[n-1]
        Y[N-n-1]=X[n]
    if np.mod(N,2)==0:
        RL=xi*X[N-1]/(4*sin(pi/(2*N)))
    else:
        RL=4*sin(pi/(2*N))/(xi*X[N-1])
# Starting from RL
elif Rterm=='RL':
    print('Starting from RL')
    if np.mod(N,2)==0:
        X[0]=4*RL*sin(pi/(2*N))/xi
    else:
        X[0]=4*sin(pi/(2*N))/(RL*xi)
    Y[N-1]=X[0]
    for n in range(1,N):
        X[n]=CL[n-1]/X[n-1]
        Y[N-n-1]=X[n]
else:
    print('Rterm should be set to RS if RS is known (starting from the source) or to RL if RL is known (starting from the load)')

Cnorm=np.zeros(N)
Lnorm=np.zeros(N)
for n in range(0,N,2):
    Cnorm[n]=X[n]

for n in range(1,N,2):
    Lnorm[n]=X[n]

print(f'N = {N:.0f}')
print(f'RS = {RS:.3f}')
print(f'RL = {RL:.3f}')
Cn_Ln_df=pd.DataFrame({'Cn':Cnorm, 'Ln':Lnorm})
pd.set_option('display.float_format', '{:.9f}'.format)
Cn_Ln_df
N = 3
RS = 1.000
RL = 1.000
Out[26]:
Cn Ln
0 2.023592642 0.000000000
1 0.000000000 0.994102444
2 2.023592642 0.000000000
In [27]:
display(Latex(f'$R_S =$ ' + f'{RS:.0f}' + ' $\\Omega$'))
display(Latex(f'$R_L =$ ' + f'{RL:.0f}' + ' $\\Omega$'))
display(Latex(f'$C_{{1,norm}} =$ ' + f'{Cnorm[0]:.6f}' + ' F'))
display(Latex(f'$L_{{2,norm}} =$ ' + f'{Lnorm[1]:.6f}' + ' H'))
display(Latex(f'$C_{{3,norm}} =$ ' + f'{Cnorm[2]:.6f}' + ' F'))
$R_S =$ 1 $\Omega$
$R_L =$ 1 $\Omega$
$C_{1,norm} =$ 2.023593 F
$L_{2,norm} =$ 0.994102 H
$C_{3,norm} =$ 2.023593 F
No description has been provided for this image

Third-order LC ladder filter.

In the indirect simulation or operational simulation approach we start writing the state equations of the LC ladder filter as shown above as \begin{align*} I_S &= \frac{V_{in}-V_1}{R_S},\\ V_1 &= \frac{I_S-I_2}{s\,C_1},\\ I_2 &= \frac{V_1-V_3}{s\,L_2},\\ V_{out} &= \frac{I_2-I_L}{s\,C_3},\\ I_L &= \frac{V_{out}}{R_L}. \end{align*} We then multiply all the currents by a normalization impedance $Z_0$ to only have voltages \begin{align*} V_S &\triangleq Z_0 \cdot I_S = \frac{Z_0}{R_S} \cdot (V_{in}-V_1),\\ V_1 &= \frac{I_S-I_2}{s\,Z_0\,C_1},\\ V_2 &\triangleq Z_0 \cdot I_2 = \frac{Z_0}{s\,L_2} \cdot (V_1-V_3),\\ V_{out} &= \frac{I_2-I_L}{s\,Z_0\,C_3},\\ V_L &\triangleq Z_0 \cdot I_L = \frac{Z_0}{R_L} \cdot V_{out}. \end{align*} In the above equations we can identify 3 integration operators. It is then easy to draw the signal flow graph corresponding to the above equations, which is shown below

No description has been provided for this image

Signal flow-graph corresponding to the state equations.

We can simplify the above SFG by choosing $Z_0 = R_5$. The last box can be replaced by a wire. We can also get rid of one summation operator by adding the weighted input voltage to the second summation operator. This results in the SFG shown below.

No description has been provided for this image

Simplified SFG replacing normalization resistance.

The above SFG can also be redrawn as shown below.

No description has been provided for this image

Simplified SFG reaplcing time constants.

The above SFG can be implemented with 3 integrators having the following integration time constants \begin{align*} \tau_1 &= R_L \cdot C_1,\\ \tau_2 &= \frac{L_2}{R_L},\\ \tau_3 &= R_L \cdot C_3. \end{align*}

These time constants can be obtained by denormalizing the time constants of the LPPF according to \begin{align*} \tau_1 &= \frac{C_{1,norm}}{\omega_p},\\ \tau_2 &= \frac{L_{2,norm}}{\omega_p},\\ \tau_3 &= \frac{C_{3,norm}}{\omega_p}. \end{align*}

In [28]:
Rnorm=1
tau1=Rnorm*Cnorm[0]/wp
tau2=Lnorm[1]/(Rnorm*wp)
tau3=Rnorm*Cnorm[2]/wp
display(Latex(f'$\\tau_1 =$ ' + f'{tau1/1e-6:.9f}' + ' $\\mu s$'))
display(Latex(f'$\\tau_2 =$ ' + f'{tau2/1e-6:.9f}' + ' $\\mu s$'))
display(Latex(f'$\\tau_3 =$ ' + f'{tau3/1e-6:.9f}' + ' $\\mu s$'))
$\tau_1 =$ 16.103238588 $\mu s$
$\tau_2 =$ 7.910815898 $\mu s$
$\tau_3 =$ 16.103238588 $\mu s$

In the particular case of Chebyshev odd order approximation, the load resistance is equal to the source resistance $R_L = R_S$ and the SFG can be further simplified as shown below.

No description has been provided for this image

Simplified SFG accouting for RL=RS.

The complete SC filter is shown below.

No description has been provided for this image

Complete 3rd-order SC LP filter including the -6 dB DC gain correction.

Notice that the $-6\,dB$ loss is corrected with adding the two SC $\alpha_{11} C_1$ and $\alpha_{12} C_1$ in the first integrator with $\alpha_{11}=\alpha_{12}$. The capacitance ratios are given by

In [29]:
T=1/fck
alpha11=1/(tau1*fck)
alpha12=alpha11
alpha2=1/(tau2*fck)
alpha3=1/(tau1*fck)
display(Latex(f'$\\alpha_{{11}} =$ ' + f'{alpha11:.6f}'))
display(Latex(f'$\\alpha_{{12}} =$ ' + f'{alpha12:.6f}'))
display(Latex(f'$\\alpha_2 =$ ' + f'{alpha2:.6f}'))
display(Latex(f'$\\alpha_3 =$ ' + f'{alpha3:.6f}'))
$\alpha_{11} =$ 0.031050
$\alpha_{12} =$ 0.031050
$\alpha_2 =$ 0.063205
$\alpha_3 =$ 0.031050
In [30]:
def Hsc(z,alpha11,alpha12,alpha2,alpha3):
    b0=(alpha11+alpha12)*alpha2*alpha3
    a0=-1
    a1=3+alpha12+alpha3-alpha11*alpha2-alpha2*alpha3
    a2=-3-2*alpha12-2*alpha3-alpha12*alpha3+alpha11*alpha2+alpha2*alpha3+alpha12*alpha2*alpha3+alpha11*alpha2*alpha3
    a3=(1+alpha12)*(1+alpha3)
    num=b0
    den=a3*z**3+a2*z**2+a1*z+a0
    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)

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,alpha11,alpha12,alpha2,alpha3)
    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(-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

We can compare it to the theoretical transfer function.

In [31]:
def Hsc(z,alpha11,alpha12,alpha2,alpha3):
    b0=(alpha11+alpha12)*alpha2*alpha3
    a0=-1
    a1=3+alpha12+alpha3-alpha11*alpha2-alpha2*alpha3
    a2=-3-2*alpha12-2*alpha3-alpha12*alpha3+alpha11*alpha2+alpha2*alpha3+alpha12*alpha2*alpha3+alpha11*alpha2*alpha3
    a3=(1+alpha12)*(1+alpha3)
    num=b0
    den=a3*z**3+a2*z**2+a1*z+a0
    return num/den

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,alpha11,alpha12,alpha2,alpha3)
    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 for the approximation to be valid), the switched-capacitor 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 [32]:
Cmin=100e-15
display(Latex(f'$C_{{min}} =$ ' + f'{Cmin/1e-12:.3f}' + ' pF'))
$C_{min} =$ 0.100 pF

this leads to the following integration capacitances $C_1$, $C_2$ and $C_3$ given by

In [33]:
C1=Cmin/alpha11
C2=Cmin/alpha2
C3=Cmin/alpha11
display(Latex(f'$C_1 =$ ' + f'{C1/1e-12:.6f}' + ' pF'))
display(Latex(f'$C_2 =$ ' + f'{C2/1e-12:.6f}' + ' pF'))
display(Latex(f'$C_3 =$ ' + f'{C3/1e-12:.6f}' + ' pF'))
$C_1 =$ 3.220648 pF
$C_2 =$ 1.582163 pF
$C_3 =$ 3.220648 pF

We finally choose $C_1=C_2=C_3$

In [34]:
C1=3.3e-12
C2=C1
C3=C1
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.3 pF
$C_2 =$ 3.3 pF
$C_3 =$ 3.3 pF

The sampling capacitances are the given by

In [35]:
display(Latex(f'$\\alpha_{{11}}\\,C_1 =$ ' + f'{alpha11*C1/1e-15:.0f}' + ' fF'))
display(Latex(f'$\\alpha_{{12}}\\,C_1 =$ ' + f'{alpha12*C1/1e-15:.0f}' + ' fF'))
display(Latex(f'$\\alpha_2\\,C_2 =$ ' + f'{alpha2*C2/1e-15:.0f}' + ' fF'))
display(Latex(f'$\\alpha_3\\,C_3 =$ ' + f'{alpha3*C3/1e-15:.0f}' + ' fF'))
$\alpha_{11}\,C_1 =$ 102 fF
$\alpha_{12}\,C_1 =$ 102 fF
$\alpha_2\,C_2 =$ 209 fF
$\alpha_3\,C_3 =$ 102 fF

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 [36]:
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:.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_{{11}} =$ ' + f'{alpha11:.9f}'))
display(Latex(f'$\\alpha_{{12}} =$ ' + f'{alpha12:.9f}'))
display(Latex(f'$\\alpha_2 =$ ' + f'{alpha2:.9f}'))
display(Latex(f'$\\alpha_3 =$ ' + f'{alpha3:.9f}'))
$A =$ 100 dB
$f_{ck} =$ 2 MHz
$C_1 =$ 3 pF
$C_2 =$ 3 pF
$C_3 =$ 3 pF
$\alpha_{11} =$ 0.031049655
$\alpha_{12} =$ 0.031049655
$\alpha_2 =$ 0.063204606
$\alpha_3 =$ 0.031049655
In [37]:
from PyLTSpice import SimRunner, SpiceEditor, RawRead

# select spice model
LTC = SimRunner(output_folder='./Simulations/LTSpice/')
LTC.create_netlist('./Simulations/LTSpice/SC_3rd_order_LPF.asc')
netlist = SpiceEditor('./Simulations/LTSpice/SC_3rd_order_LPF.net')
# set default arguments
print("Creating the netlist...")
netlist.set_parameters(A=A, fs=fck, C1=C1, C2=C2, C3=C3, a11=alpha11, a12=alpha12, a2=alpha2, a3=alpha3)
#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_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=5)
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=2)
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_{11} =$'+f'{alpha11:.6f}',
    r'$\alpha_{12} =$'+f'{alpha12:.6f}',
    r'$\alpha_2 =$'+f'{alpha2:.6f}',
    r'$\alpha_3 =$'+f'{alpha3:.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 [38]:
A=100
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_{{11}} =$ ' + f'{alpha11:.9f}'))
display(Latex(f'$\\alpha_{{12}} =$ ' + f'{alpha12:.9f}'))
display(Latex(f'$\\alpha_2 =$ ' + f'{alpha2:.9f}'))
display(Latex(f'$\\alpha_3 =$ ' + f'{alpha3:.9f}'))
$A =$ 40 dB
$f_{ck} =$ 2 MHz
$C_1 =$ 3 pF
$C_2 =$ 3 pF
$C_3 =$ 3 pF
$\alpha_{11} =$ 0.031049655
$\alpha_{12} =$ 0.031049655
$\alpha_2 =$ 0.063204606
$\alpha_3 =$ 0.031049655
In [40]:
from PyLTSpice import SimRunner, SpiceEditor, RawRead

# select spice model
LTC = SimRunner(output_folder='./Simulations/LTSpice/')
LTC.create_netlist('./Simulations/LTSpice/SC_3rd_order_LPF.asc')
netlist = SpiceEditor('./Simulations/LTSpice/SC_3rd_order_LPF.net')
# set default arguments
print("Creating the netlist...")
netlist.set_parameters(A=A, fs=fck, C1=C1, C2=C2, C3=C3, a11=alpha11, a12=alpha12, a2=alpha2, a3=alpha3)
#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_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=5)
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=2)
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_{11} =$'+f'{alpha11:.6f}',
    r'$\alpha_{12} =$'+f'{alpha12:.6f}',
    r'$\alpha_2 =$'+f'{alpha2:.6f}',
    r'$\alpha_3 =$'+f'{alpha3:.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.