Exercise 12 (11.12.2024)
Solution to Problem 2
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) freuqency.
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=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}'))
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()
The low-pass prototype filter is shown below.
Low-pass prototype filter (LPPF).
The component values of the LPPF are given below.
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
| Cn | Ln | |
|---|---|---|
| 0 | 2.023592642 | 0.000000000 |
| 1 | 0.000000000 | 0.994102444 |
| 2 | 2.023592642 | 0.000000000 |
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'))
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
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.
Simplified SFG replacing normalization resistance.
The above SFG can also be redrawn as shown below.
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*}
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$'))
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.
Simplified SFG accouting for RL=RS.
The complete SC filter is shown below.
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
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}'))
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()
We can compare it to the theoretical transfer function.
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()
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
Cmin=100e-15
display(Latex(f'$C_{{min}} =$ ' + f'{Cmin/1e-12:.3f}' + ' pF'))
this leads to the following integration capacitances $C_1$, $C_2$ and $C_3$ given by
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'))
We finally choose $C_1=C_2=C_3$
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'))
The sampling capacitances are the given by
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'))
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:.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}'))
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...
The simulation matches the theoretical transfer function.
We can see the impact of the OPAMP finite gain.
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}'))
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...
We can observe some losses in the passband.