---
title: |
  Fundamentals of Analog VLSI Design \
  Exercise 5 - Solution
subtitle: The Cascode Gain Stage
author: Christian Enz (christian.enz@epfl.ch)
affiliation: EPFL
date: 10-15-2025
date-format: "DD.MM.YYYY"
execute:
  echo: false
  warning: false
toc: true
number-sections: true
number-depth: 3
highlight-style: pygments
bibliography: references.bib
csl: ieee.csl
format: 
  html:
    include-in-header: mathjax.html
    code-fold: true
    html-math-method: mathjax
    css: styles.css
  pdf:
    keep-tex: true
    pdf-engine: pdflatex
    documentclass: scrreprt
    papersize: a4
    geometry:
      - top=20mm
      - bottom=20mm
      - left=20mm
      - right=20mm
    include-in-header:
      text: |
        \usepackage{scrlayer-scrpage}
        \rohead[\thepage]{\thepage}
        \cohead[Exercise 5]{Exercise 5}
        \lofoot[$\copyright$ C. Enz]{$\copyright$ C. Enz}
        \cofoot[Fundamentals of Analog VLSI Design]{Fundamentals of Analog VLSI Design}
        \rofoot[15.10.2025]{15.10.2025}
---

```{python}
# Initialization

from ekv_functions import *

import pandas as pd
import matplotlib.pyplot as plt
import subprocess
import shutil
import os
import os.path as op
import sys
sys.path.append(".")
import re

from IPython.display import Markdown
from tabulate import tabulate

plt.rcParams['svg.fonttype'] = 'none'
plt.rcParams['pdf.fonttype'] = 42
plt.rcParams['font.family'] = 'Arial'
plt.rcParams['mathtext.fontset'] = 'custom'
plt.rcParams['mathtext.rm'] = 'Arial'
plt.rcParams['mathtext.it'] = 'Arial:italic'
plt.rcParams['mathtext.bf'] = 'Arial:bold'

inkscapePath = r"C:\\Program Files\\Inkscape\bin\\inkscape.exe"
savePlotPath = "./Plots/"
#savePlots=True
savePlots=False

if savePlots:
  lw=1
  curvelw=2
  msize=6
  textFontSize=12
  BodePlotParams = {'figure.figsize': (6,6)}
  pltStyle='plt_style_medium.mplstyle'
else:
  lw=1
  curvelw=1
  msize=5
  textFontSize=9
  BodePlotParams = {'figure.figsize': (5,5)}
  pltStyle='plt_style_small.mplstyle'

plt.style.use(pltStyle)

circuitName="cascode"
newSim=True
#newSim=False
refloat=r'.*?([+-]?\d+([.]\d*)?([eE][+-]?\d+)?|[.]\d+([eE][+-]?\d+)?)'

def saveFigures(savePath, plotName):
#  print("Patience, 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)
```

# Cascode stage

![Schematic of the cascode gain stage.](Figures/cascode_schematic.png){#fig-cascode_schematic}

## Analysis
### Small-signal analysis

![Small-signal schematic of the cascode stage of @fig-cascode_schematic for the calculation of the equivalent transconductance.](Figures/cascode_small-signal_schematic_gm.png){#fig-cascode_small-signal_schematic_gm}

The small-signal schematic corresponding to @fig-cascode_schematic for the derivation of the equivalent transconductance is shown in @fig-cascode_small-signal_schematic_gm. Note that the output is short-circuited to ground for calculating the short-circuit output current and the corresponding transconductance. The equivalent transconductance is then given by
\begin{equation}
  G_{meq} \triangleq \left.\frac{\Delta I_{out}}{\Delta V_{in}}\right|_{\Delta V_{out}=0}
  = G_{m1} \cdot \frac{G_{ms2} + G_{ds2}}{G_{ms2} + G_{ds1} + G_{ds2}} \cong G_{m1},
\end{equation}
which shows that assuming $G_{ms2} \gg G_{ds1}, G_{ds2}$, the equivalent transconductance of the cascode stage is equal to the transconductance of the driver transistor M~1~. This result is expected since the cascode transistor is a common gate stage which has a unity current gain so that the current coming from the driver transistor M~1~ is directly directed to the output.

![Small-signal schematic of the cascode stage of @fig-cascode_schematic for the calculation of the output conductance.](Figures/cascode_small-signal_schematic_gout.png){#fig-cascode_small-signal_schematic_gout}

The small-signal schematic for the calculation of the output conductance is shown in @fig-cascode_small-signal_schematic_gout. The output conductance is given by
\begin{equation}
  G_{out} \triangleq \frac{\Delta I_{out}}{\Delta V_{out}}
  = \frac{G_{ds1} G_{ds2}}{G_{ms2} + G_{ds1} + G_{ds2}} \cong \frac{G_{ds1}}{G_{ms2}/G_{ds2}},
\end{equation}
which is equal to the output conductance of M~1~, $G_{ds1}$, divided by the voltage gain of the cascode $G_{ms2}/G_{ds2}$. This means that, at low-frequency, the output conductance of a single transistor can be reduced by adding a cascode stage at the cost of some voltage headroom to maintain M~2~ in saturation.

![Small-signal schematic of the cascode stage of @fig-cascode_schematic for the calculation of the output admittance including the parasitic capacitance $C_{p1}$.](Figures/cascode_small-signal_schematic_gout_cp.png){#fig-cascode_small-signal_schematic_gout_cp}

![Effect of the parasitic capacitance $C_{p1}$ on the output admittance $Y_{out}$.](Figures/cascode_yout.png){#fig-cascode_yout}

The impact of the parasitic capacitance at node 1 on the output admittance can be investigated by adding the capacitance as shown in @fig-cascode_small-signal_schematic_gout_cp. The output admittance then becomes
\begin{equation}
  Y_{out} = G_{out} \cdot \frac{1+s/\omega_z}{1+s/\omega_p},
\end{equation}
where
\begin{align}
  G_{out} &\cong \frac{G_{ds1} G_{ds2}}{G_{ms2}},\\
  \omega_z &\triangleq \frac{G_{ds1}}{C_{p1}},\\
  \omega_p &\triangleq \frac{G_{ms2}}{C_{p1}}.
\end{align}

The magnitude of $Y_{out}$ versus frequency is sketched in @fig-cascode_yout. At low-frequency, i.e. for $\omega \ll \omega_z \ll \omega_p$, the output admittance is equal to the output conductance $Y_{out} \cong G_{out}$. However for $\omega_z \ll \omega_p \ll \omega$, the output admittance increases to the output conductance of M~2~ $Y_{out} \cong G_{ds2}$. We see that the cascode effect is lost. This can easily be understood since for $\omega_p \ll \omega$, the cascode node 1 is shortened to the ac ground and the voltage controlling the source of M~2~ is zero leaving the output conductance of M~2~ only in the small-signal schematic of @fig-cascode_small-signal_schematic_gout_cp.

### Noise analysis

![Small-signal schematic of the cascode stage of @fig-cascode_schematic for the noise calculation.](Figures/cascode_small-signal_schematic_noise.png){#fig-cascode_small-signal_schematic_noise}

To calculate the output noise PSD we can use the schematic shown in @fig-cascode_small-signal_schematic_noise. The output noise current is then given by
\begin{equation}
  I_{nout} = \frac{G_{ms2} + G_{ds2}}{G_{ms2} + G_{ds1} + G_{ds2}} \cdot I_{n1} + \frac{G_{ds1}}{G_{ms2} + G_{ds1} + G_{ds2}} \cdot I_{n2}
  \cong I_{n1} + \frac{I_{n2}}{G_{ms2}/G_{ds1}},
\end{equation}
for $G_{ms2} \gg G_{ds1}, G_{ds2}$. We see that the contribution to the output noise current of the cascode stage $I_{n2}$ is actually divided by $G_{ms2}/G_{ds1}$. Provided that this gain can be made sufficiently large and that both transistor have the same noise, the noise of the cascode stage can be made negligible compared to the noise due to M~1~. Ultimately if $G_{ds1}=0$, the noise current $I_{n2}$ circulates in the cascode transistor M~2~ ($G_{ms2}$ in the small-signal schematic of @fig-cascode_small-signal_schematic_noise) and hence does not reach the output.

The output noise conductance $G_{nout}$ is then given by
\begin{equation}
  G_{nout}(f) \cong G_{n1}(f) + \left(\frac{G_{ds1}}{G_{ms2}}\right)^2 G_{n2}(f)
\end{equation}
where the noise conductances are given by
\begin{equation}\label{eqn:Gni}
  G_{ni}(f) = \gamma_{ni} \cdot G_{mi} + \frac{\rho_n}{f\,W_i\,L_i} \cdot G_{mi}^2 \quad \text{for $i=1,2$}.
\end{equation}

The input-referred noise is obtained by dividing $G_{nout}$ by $G_{meq}^2$, resulting in
\begin{equation}
  R_{nin} = \frac{G_{nout}(f)}{G_{meq}^2}
  \cong \frac{G_{nout}(f)}{G_{m1}^2}
  \cong \frac{G_{n1}(f)}{G_{m1}^2} + \left(\frac{G_{ds1}}{G_{m1}\,G_{ms2}}\right)^2\;G_{n2}(f).
\end{equation}
It can be decomposed into the thermal and flicker noise components according to
\begin{equation}
  R_{nin} = R_{nt} + R_{nf}(f)
\end{equation}
where $R_{nt}$ is the total input-referred thermal noise given by
\begin{equation}
  R_{nt} \cong \frac{\gamma_{n1}}{G_{m1}} + \left(\frac{G_{ds1}}{G_{m1}}\right)^2 \frac{\gamma_{n2}}{n_2\,G_{ms2}}
  = \frac{\gamma_{n1}}{G_{m1}} \cdot (1+\eta_{th}),
\end{equation}
where $\eta_{th}$, given by
\begin{equation}
  \eta_{th} \cong \frac{\gamma_{n2}}{n_2\,\gamma_{n1}}  \cdot  \frac{G_{ds1}^2}{G_{m1}\,G_{ms2}},
\end{equation}
represents the contribution of the cascode transistor M~2~ to the input-referred thermal noise relative to that of M~1~. Since $\gamma_{n1}$ and $\gamma_{n2}$ are of the same order of magnitude and $G_{m1},G_{ms2} \gg G_{ds1}$, this results in $\eta_{th} \ll 1$, meaning that the contribution of M~2~ is negligible compared to that of M~1~ (at low frequency).

The noise excess factor of the cascode gain stage is given by
\begin{equation}
  \gamma_{cas} \triangleq G_{meq} \cdot R_{nt} \cong G_{m1} \cdot R_{nt}
  = \gamma_{n1} \cdot (1+\eta_{th}) \cong \gamma_{n1}.
\end{equation}
The thermal noise excess factor of the cascode gain stage is about equal to the thermal noise excess factor of the driver transistor M~1~.

The input-referred flicker noise $R_{nf}(f)$ is given by
\begin{equation}\label{eqn:p1:Rnf}
  R_{nf}(f) \cong \frac{\rho_n}{f\,W_1 L_1} + \left(\frac{G_{ds1}}{G_{m1}}\right)^2 \frac{\rho_n}{n_2^2 f\,W_2 L_2}
  = \frac{\rho_n}{f\,W_1 L_1} \cdot (1+\eta_{fl}).
\end{equation}
where $\eta_{fl}$, given by
\begin{equation}
  \eta_{fl} = \left(\frac{G_{ds1}}{n_2\,G_{m1}}\right)^2 \frac{W_1 L_1}{W_2 L_2}
\end{equation}
Assuming M~1~ and M~2~ have the same area and $G_{m1}/G_{ds1} \gg 1$, the contribution of M~2~ to the input-referred flicker noise is negligible compared to that of M~1~ and \eqref{eqn:p1:Rnf} simplifies to
\begin{equation}
  R_{nf}(f) \cong  \frac{\rho_n}{f\,W_1 L_1},
\end{equation}
which corresponds to the contribution of M~1~ only.

![Small-signal schematic of the cascode stage of @fig-cascode_schematic for the calculation of the noise including the effect of the parasitic capacitance $C_{p1}$.](Figures/cascode_small-signal_schematic_noise_cp.png){#fig-cascode_small-signal_schematic_noise_cp}

Similarly, the impact of the parasitic capacitance on the noise can be calculated from @fig-cascode_small-signal_schematic_noise_cp. The output noise conductance is then given by
\begin{equation}
  G_{nout} = |H_{n1}(\omega)|^2 \cdot G_{n1} + |H_{n2}(\omega)|^2 \cdot G_{n2},
\end{equation}
where
\begin{align}
  H_{n1}(s) &= \frac{1}{1+s/\omega_p}\\
  H_{n2}(s) &= \frac{G_{ds1}}{G_{ms2}} \cdot \frac{1+s/\omega_z}{1+s/\omega_p}.
\end{align}

![Magnitude of the noise transfer functions $H_{n1}$ and $H_{n2}$ versus frequency.](Figures/cascode_hn1_hn2.png){#fig-cascode_hn1_hn2}

The magnitude of $H_{n1}$ and $H_{n2}$ versus frequency are sketched in @fig-cascode_hn1_hn2. For $\omega \ll \omega_z \ll \omega_p$, $H_{n1} \cong 1$ and $H_{n2} \cong G_{ds1}/G_{ms2}$ which is the result obtained above. However, for $\omega_z \ll \omega_p \ll \omega$, $H_{n1} \cong \omega_p/s$ and $H_{n2} \cong 1$. We see that the cascode effect is lost since the noise of M~2~ is no more divided by the cascode gain $G_{ms2}/G_{ds1}$ but is entirely transferred to the output.

As a conclusion, adding a cascode stage reduces the output conductance without penalty on the noise, but at the cost of a slight voltage overhead for maintaining M~2~ in saturation. This is only true for $\omega < \omega_z = G_{ds1}/C_{p1}$. For frequencies $\omega \gg \omega_p = G_{ms2}/C_{p1}$ the cascode effect is lost. Note that in order to maximize $G_{ms2}$ at a given current and minimize its saturation voltage, M~2~ should be biased in weak inversion.

## Design
We want to size the circuit of @fig-cascode_schematic for the specifications given in @tbl-specs. We need to find the minimum current and size the transistor to achieve these specs. We will design the amplifier for a generic 180nm bulk CMOS process. The physical parameters are given in @tbl-physics_param, the global process parameters in @tbl-process_param and finally the MOSFET parameters in @tbl-mos_param. 

```{python}
#| label: tbl-specs
#| tbl-cap: Specifications for the differential pair with resistive loads.

from specs import *

table = [["DC gain","$A_{{dc}}$",f'{AdcdBspec:.0f}',"$dB$"],
         ["DC gain","$A_{{dc}}$",f'{Adcspec:.0f}',"-"],
         ["Gain-bandwidth product","$GBW$",f'{GBWspec/1e6:.0f}',"$MHz$"],
         ["Load capacitance","$C_L$",f'{CL/1e-12:.0f}',"$pF$"]
         ]

Markdown(tabulate(
  table,
  headers=["Specification","Symbol","Value","Unit"],
  numalign="center",
  stralign="center",
  colalign=("left","center","center","center")
))
```

```{python}
#| label: tbl-physics_param
#| tbl-cap: Physical parameters

from tsmc018_sekv_ekv2p6 import *

table = [["$T$",f'{T:.0f}',"$K$"],
         ["$U_T$",f'{UT/1e-3:.3f}',"$mV$"]
         ]

Markdown(tabulate(
  table,
  headers=["Parameter","Value","Unit"],
  numalign="center",
  stralign="center",
  colalign=("center", "center", "center")
))
```

```{python}
#| label: tbl-process_param
#| tbl-cap: Global process parameters

table = [["$V_{DD}$",f'{VDD:.1f}',"$V$"],
         ["$C_{ox}$",f'{Cox*1e3:.3f}',"$\\frac{fF}{\\mu m^2}$"],
         ["$W_{min}$",f'{Wmin/1e-9:.0f}',"$nm$"],
         ["$L_{min}$",f'{Lmin/1e-9:.0f}',"$nm$"]
         ]

Markdown(tabulate(
  table,
  headers=["Parameter","Value","Unit"],
  numalign="center",
  stralign="center",
  colalign=("center", "center", "center")
))
```

```{python}
#| label: tbl-mos_param
#| tbl-cap: Transistor process parameters

table = [["sEKV parameters"],
         ["$n$",f'{n0n:.2f}',f'{n0p:.2f}',"-"],
         ["$I_{{spec\\Box}}$",f'{Ispecsqn/1e-9:.0f}',f'{Ispecsqp/1e-9:.0f}',"$nA$"],
         ["$V_{{T0}}$",f'{VT0n:.3f}',f'{VT0p:.3f}',"$V$"],
         ["$L_{{sat}}$",f'{Lsatn/1e-9:.0f}',f'{Lsatp/1e-9:.0f}',"$nm$"],
         ["$\\lambda$",f'{lambdan*1e-6:.0f}',f'{lambdap*1e-6:.0f}',"$\\frac{{V}}{{\\mu m}}$"],
         ["Overlap capacitances parameters"],
         ["$C_{{GDo}}$",f'{CGDOn*1e9:.3f}',f'{CGDOp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["$C_{{GSo}}$",f'{CGSOn*1e9:.3f}',f'{CGSOp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["$C_{{GBo}}$",f'{CGBOn*1e9:.3f}',f'{CGBOp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["Junction capacitances parameters"],
         ["$C_J$",f'{CJn*1e3:.3f}',f'{CJp*1e3:.3f}','$\\frac{{fF}}{{\\mu m^2}}$'],
         ["$C_{{JSW}}$",f'{CJSWn*1e9:.3f}',f'{CJSWp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["Flicker noise parameters"],
         ["$K_F$",f'{KFn:.1e}',f'{KFp:.1e}',"$J$"],
         ["$AF$",f'{AFn:.1f}',f'{AFp:.1f}',"-"],
         ["$\\rho$",f'{rhon:.3e}',f'{rhop:.3e}','$\\frac{{V \\cdot m^2}}{{A \\cdot s}}$'],
         ["Matching parameters"],
         ["$A_{{VT}}$",f'{AVTn*1e9:.0f}',f'{AVTp*1e9:.0f}',"$mV \\cdot \\mu m$"],
         ["$A_{{\\beta}}$",f'{Abetan*1e8:.0f}',f'{Abetap*1e8:.0f}',"$\\% \\cdot \\mu m$"],
         ["Source and drain sheet resistance parameter"],
         ["$R_{{sh}}$",f'{RSHn:.0f}',f'{RSHp:.0f}',"$\\frac{{\\Omega}}{{\\mu m}}$"],
         ["Width and length parameters"],
         ["$\\Delta W$",f'{DWn/1e-9:.0f}',f'{DWp/1e-9:.0f}',"$\\,nm$"],
         ["$\\Delta L$",f'{DLn/1e-9:.0f}',f'{DLp/1e-9:.0f}',"$\\,nm$"]
         ]

Markdown(tabulate(
  table,
  headers=["Parameter","NMOS","PMOS","Unit"],
  numalign="center",
  stralign="center",
  colalign=("center", "center", "center")
))
```

```{python}
Gm1=2*pi*CL*GBWspec
IC1=1
Ib=Gm1*n0n*UT/gmsid_ic(IC1)
Ispec1=Ib/IC1
WoverL1=Ispec1/Ispecsqn
Gout=Gm1/Adcspec
IC2=0.1
Ispec2=Ib/IC2
WoverL2=Ispec2/Ispecsqn
Gms2=Ib/UT*gmsid_ic(IC2)
Gds1=Gm1/sqrt(Adcspec)
Gds2=Gms2/sqrt(Adcspec)
L1ini=Ib/(lambdan*Gds1)
L2ini=Ib/(lambdan*Gds2)
Leff1=n0n*UT*sqrt(Adcspec)/(lambdan*gmsid_ic(IC1))
Leff2=UT*sqrt(Adcspec)/(lambdan*gmsid_ic(IC2))
L1=Leff1-DLn
L2=Leff2-DLn
Weff1=WoverL1*Leff1
Weff2=WoverL2*Leff2
W1=Weff1-DWn
W2=Weff2-DWn
VDSsat1=UT*vdssat_ic(IC1)
#VGS2=VT0n+n0n*UT*vps_ic(IC2)
VGS2=VT0n
Vb2ini=VGS2+VDSsat1
Vb2=0.65
```

The gain-bandwidth product direcly sets the transconductance of M~1~ as $G_{m1} =$ `{python} f'{Gm1/1e-6:.3f}'` $\mu A/V$. Imposing an inversion coefficient $IC_1 =$ `{python} f'{IC1:.0f}'` allows to derive the required bias current as $I_b = G_{m1}\,n\,U_T/gmsid(IC1) =$ `{python} f'{Ib/1e-6:.3f}'` $\mu A$. We then can find the specific current $I_{spec1} =$ `{python} f'{Ispec1/1e-6:.3f}'` $\mu A$ and aspect ratio $W_1/L_1 =$ `{python} f'{WoverL1:.3f}'`.

The DC gain then sets the output conductance as $G_{out} =$ `{python} f'{Gout/1e-9:.3f}'` $nA/V$. To minimize the voltage headroom needed for M~2~ and maximize its current efficiency, we will bias M~2~ in weak inversion setting its inversion coefficient to $IC_2 =$ `{python} f'{IC2:.1f}'`. Knowing $IC_2$ and $I_b$ we can deduce its source transconductance as $G_{ms2} =$ `{python} f'{Gms2/1e-6:.3f}'` $\mu A/V$. If we split the overall voltage gain equally between M~1~ and M~2~ we get $G_{m1}/G_{ds1} = G_{ms2}/G_{ds2} = \sqrt{A_{dc}} =$ `{python} f'{sqrt(Adcspec):.0f}'`. We now can deduce $G_{ds1} =$ `{python} f'{Gds1/1e-6:.3f}'` $\mu A/V$ and $G_{ds2} =$ `{python} f'{Gds2/1e-6:.3f}'` $\mu A/V$. This leads to $L_1 =$ `{python} f'{L1/1e-6:.2f}'` $\mu m$ and $L_2 =$  `{python} f'{L2/1e-6:.2f}'` $\mu m$.

It turns out that the length of M~1~ and M~2~ are actually independent of their transconductances $G_{m1}$ and $G_{ms2}$. Indeed, from the output conductance we get the transistor length as
\begin{equation}\label{eqn:l1}
  L_1 = \frac{I_b}{\lambda_n\,G_{ds1}}
\end{equation}
The output conductance $G_{ds1}$ is set by the required DC gain as
\begin{equation}\label{eqn:cascode_gds1}
  G_{ds1} = \frac{G_{m1}}{\sqrt{A_{dc}}} = \frac{2\pi\,GBW\,C_L}{\sqrt{A_{dc}}}
\end{equation}
Now the bias current is given by
\begin{equation}\label{eqn:ib}
  I_b = G_{m1}\,\frac{n_1 U_T}{gmsid(IC_1)}
\end{equation}
Replacing \eqref{eqn:ib} and \eqref{eqn:cascode_gds1} in \eqref{eqn:l1} results in
\begin{equation}
  L_1 = \frac{n_1 U_T\,\sqrt{A_{dc}}}{\lambda_n\,gmsid(IC_1)},
\end{equation}
which is independent of the transconductance $G_{m1}$.

Similarly for M~2~ we get
\begin{equation}
  L_2 = \frac{U_T\,\sqrt{A_{dc}}}{\lambda_n\,gmsid(IC_2)},
\end{equation}
which is independent of the source transconductance $G_{ms2}$ of M~2~.

We now get the transistor width as $W_1 =$ `{python} f'{W1/1e-6:.2f}'` $\mu m$ and $W_2 =$ `{python} f'{W2/1e-6:.2f}'` $\mu m$.

We still need to set the bias voltage for the cascode transistor. We want to bias M~1~ at the onset of saturation $V_{DSsat1}$. The $V_{GS2}$ voltage of M~2~ can be approximated by $V_{T0n}$. The bias voltage is then given by
\begin{equation}
  V_{b2} = V_{GS2} +V_{DSsat1}
\end{equation}
which is `{python} f'{Vb2ini:.3f}'` $V$. We will set it to $V_{b2} =$  `{python} f'{Vb2:.3f}'` $V$ so we have some margin.


```{python}
#| label: tbl-transistor_info1
#| tbl-cap: Transistor size and bias information.

transistor_info={
    "Type": ["n","n","n","n"],
    "Function": ["CS","CA","BI","BI"],
    "W": [W1,W2,W1,W2],
    "L": [L1,L2,L1,L2],
    "ID": [Ib,Ib,Ib,Ib]
}
transistor_list=["M1","M2","M3","M4"]
mos_df=pd.DataFrame(transistor_info, index=transistor_list)

mos_df["W/L"]=mos_df["W"]/mos_df["L"]
mos_df["Ispec"]=np.where(mos_df["Type"]=="n",
                                     mos_df["W/L"]*Ispecsqn,
                                     mos_df["W/L"]*Ispecsqp)
mos_df["IC"]=mos_df["ID"]/mos_df["Ispec"]
mos_df["VP-VS"]=UT*vps_ic(mos_df["IC"])
mos_df["n"]=np.where(mos_df["Type"]=="n",
                                     n0n,
                                     n0p)
mos_df["VG-VT0"]=np.where(mos_df["Type"]=="n",
                                     mos_df["VP-VS"]/n0n,
                                     mos_df["VP-VS"]/n0p)
mos_df["VDSsat"]=UT*vdssat_ic(mos_df["IC"])
mos_df["Gspec"]=mos_df["Ispec"]/UT
mos_df["Gms"]=mos_df["Gspec"]*gms_ic(mos_df["IC"])
mos_df["Gm"]=np.where(mos_df["Type"]=="n",
                                     mos_df["Gms"]/n0n,
                                     mos_df["Gms"]/n0p)
mos_df["Gds"]=np.where(mos_df["Type"]=="n",
                                     mos_df["ID"]/(lambdan*mos_df["L"]),
                                     mos_df["ID"]/(lambdap*mos_df["L"]))
mos_df["gamman"]=np.where(mos_df["Type"]=="n",
                                     gamman_ic(mos_df["IC"],n0n),
                                     gamman_ic(mos_df["IC"],n0p))
mos_df["COX"]=Cox*mos_df["W"]*mos_df["L"]
mos_df["CGSi"]=mos_df["COX"]*cgsi_ic(mos_df["IC"])
mos_df["CGDi"]=0
mos_df["CGBi"]=np.where(mos_df["Type"]=="n",
                                     mos_df["COX"]*cgbi_ic(mos_df["IC"],n0n),
                                     mos_df["COX"]*cgbi_ic(mos_df["IC"],n0p))
mos_df["CBSi"]=np.where(mos_df["Type"]=="n",
                                     mos_df["COX"]*cbsi_ic(mos_df["IC"],n0n),
                                     mos_df["COX"]*cbsi_ic(mos_df["IC"],n0p))
mos_df["CBDi"]=np.where(mos_df["Type"]=="n",
                                     mos_df["CGDi"]*(n0n-1),
                                     mos_df["CGDi"]*(n0p-1))
mos_df["CGSe"]=np.where(mos_df["Type"]=="n",
                                     mos_df["W"]*CGSOn,
                                     mos_df["W"]*CGSOp)
mos_df["CGDe"]=np.where(mos_df["Type"]=="n",
                                     mos_df["W"]*CGDOn,
                                     mos_df["W"]*CGDOp)
mos_df["CGBe"]=np.where(mos_df["Type"]=="n",
                                    mos_df["W"]*CGBOn,
                                    mos_df["W"]*CGBOp)
mos_df["CBSe"]=np.where(mos_df["Type"]=="n",
                                    mos_df["W"]*mos_df["L"]*CJn+2*(mos_df["W"]+mos_df["L"])*CJSWn,
                                    mos_df["W"]*mos_df["L"]*CJp+2*(mos_df["W"]+mos_df["L"])*CJSWp)
mos_df["CBDe"]=np.where(mos_df["Type"]=="n",
                                    mos_df["W"]*mos_df["L"]*CJn+2*(mos_df["W"]+mos_df["L"])*CJSWn,
                                    mos_df["W"]*mos_df["L"]*CJp+2*(mos_df["W"]+mos_df["L"])*CJSWp)
mos_df["CGS"]=mos_df["CGSi"]+mos_df["CGSe"]
mos_df["CGD"]=mos_df["CGDi"]+mos_df["CGDe"]
mos_df["CGB"]=mos_df["CGBi"]+mos_df["CGBe"]
mos_df["CBS"]=mos_df["CBSi"]+mos_df["CBSe"]
mos_df["CBD"]=mos_df["CBDi"]+mos_df["CBDe"]
#pd.set_option('display.float_format', '{:.2E}'.format)
#mos_df

# Generate the table from dataframe
feature_list=["W","L","ID","Ispec","IC","VG-VT0","VDSsat"]
Nmos=len(transistor_list)
Nfeature=len(feature_list)
table=[]
for mos in transistor_list:
  row=[mos]
  for feature in feature_list:
    row.append(mos_df.at[mos,feature])
  table.append(row)

# Scale data for proper units
for i in range(Nmos):
  for j in [1,2,3,4]:
    table[i][j]=table[i][j]/1e-6
  for j in [6,7]:
    table[i][j]=table[i][j]/1e-3

Markdown(tabulate(
  table,
  headers=["Transistor","$W\\;[\\mu m]$","$L\\;[\\mu m]$","$I_D\\;[\\mu A]$","$I_{{spec}}\\;[\\mu A]$","$IC$","$V_G-V_{{T0}}\\;[mV]$","$V_{{DSsat}}\\;[mV]$"],
  numalign="center",
  stralign="center",
  colalign=("center","center","center","center","center","center","center","center"),
  floatfmt=(".3f",".2f",".2f",".3f",".3f",".1f",".0f",".0f")
))
```

```{python}
#| label: tbl-transistor_info2
#| tbl-cap: Transistor small-signal and thermal noise parameters.

# Generate the table from dataframe
feature_list=["Gspec","Gms","Gm","Gds","gamman"]
Nmos=len(transistor_list)
Nfeature=len(feature_list)
table=[]
for mos in transistor_list:
  row=[mos]
  for feature in feature_list:
    row.append(mos_df.at[mos,feature])
  table.append(row)

# Scale data for proper units
for i in range(Nmos):
  for j in [1,2,3]:
    table[i][j]=table[i][j]/1e-6
  for j in [4]:
    table[i][j]=table[i][j]/1e-9

Markdown(tabulate(
  table,
  headers=["Transistor","$G_{{spec}}\\;[\\mu A/V]$","$G_{{ms}}\\;[\\mu A/V]$","$G_m\\;[\\mu A/V]$","$G_{{ds}}\\;[nA/V]$","$\\gamma_n$"],
  numalign="center",
  stralign="center",
  colalign=("center","center","center","center","center","center"),
  floatfmt=(".3f",".3f",".3f",".3f",".3f",".3f")
))
```

## Simulations

![Schematic used for the simulation of the transfer function.](Figures/cascode_av_sim.png){#fig-cascode_av_sim}

We can now can check the design by simulation using the circuit shown in @fig-cascode_av_sim. The purpose of M~3~ and M~4~ is to set the DC gate voltage of M~1~ such that its current is equal to the bias current. Of course M~3~ is identical to M~1~ and M~4~ is identical to M~2~.

We first will check the operating point. The node voltages are given in @tbl-ng_voltages. We can check that all transistors are biased in saturation. The transistor operating point information are given in @tbl-ng_op1 and @tbl-ng_op2. We see that the simulated transconductances are very close to the theoretical values given in @tbl-transistor_info2.

```{python}
#| label: tbl-ng_voltages
#| tbl-cap: OTA node voltages with the OTA in open-loop without offset correction.

circuitName="cascode"
simtype = ".op"
fileName = circuitName + simtype
simulationPath = "./Simulations/ngspice/"
qucsPath = "./Simulations/qucs-s/"
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationVoltages = simulationPath + fileName + ".ic"
simulationData = simulationPath + fileName + ".dat"

if newSim:
  paramstr = '\n'.join((
    f'.param VDD={VDD:.1f} Vb2={Vb2:.2f} Ib={Ib/1e-6:.3f}u',
    f'.param CL={CL/1e-12:.0f}p',
    f'.param W1={W1/1e-6:.2f}u L1={L1/1e-6:.2f}u',
    f'.param W2={W2/1e-6:.2f}u L2={L2/1e-6:.2f}u'
  ))
  # Write the transistors sizes and bias to the ngspice directory
  with open(simulationPath+'size_bias.par', 'w') as f:
    f.write(paramstr)
  # Write the transistors sizes and bias to the qucs-s directory
  with open(qucsPath+'size_bias.par', 'w') as f:
    f.write(paramstr)
  #print('Starting ngspice simulation...\n')
  result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)


nodeList=["vdd","in","out","vb2","1","2","3"]
v_df=pd.DataFrame(index=nodeList)
Nnodes=len(nodeList)
for k in range(0,Nnodes):
  nodeName=nodeList[k]
  regexp = re.compile(r'v\(' + nodeName + r'\)' + refloat)
  with open(simulationVoltages) as f:
    for line in f:
      match = re.search(regexp, line)
      if match:
        v_df.at[nodeName, 'Voltage']=float(match.group(1))

Voutq0=v_df.loc['out','Voltage']
#pd.set_option('display.float_format', '{:.2f}'.format)
#v_df

Markdown( 
  tabulate(v_df,
  showindex=True,
  headers=["Node","Voltage"],
  numalign="center",
  stralign="center",
  colalign=("center","center")
))
```


```{python}
#| label: tbl-ng_op1
#| tbl-cap: Large-signal operating point information extracted from ngspice .op file for each transistor.

df_mos=pd.read_table(simulationData, sep=r'\s+', skiprows=[2,4,6,8,10], dtype=np.float64, engine='python')
df_mos=df_mos.rename(columns={'@n1[id]': 'Transistor', '@N1[Id]': 'ID', '@N1[Ispec]': 'Ispec', '@N1[IC]': 'IC', '@N1[n0]': 'n', '@N1[Vdsat]': 'VDsat',
                            '@N1[gm]': 'Gm', '@N1[gms]': 'Gms', '@N1[gds]': 'Gds', '@N1[gmbs]': 'Gmb',
                            '@N1[Rn]': 'Rn', '@N1[Vnth]': 'Vnth', '@N1[gamman]': 'gamman', '@N1[Vnfl]': 'Vnfl at 1Hz'})
df_mos['Transistor'] = df_mos['Transistor'].astype(str)
df_mos.at[0, 'Transistor'] = 'M1'
df_mos.at[1, 'Transistor'] = 'M2'
df_mos.at[2, 'Transistor'] = 'M3'
df_mos.at[3, 'Transistor'] = 'M4'
df_mos.set_index('Transistor', inplace=True)
df_mos.rename_axis(index=None, inplace=True)
pd.set_option('display.float_format', '{:.2E}'.format)
#df_mos

transistor_list=["M1","M2","M3","M4"]
feature_list=["ID","Ispec","IC","n","VDsat"]
Nmos=len(transistor_list)
Nfeature=len(feature_list)
table=[]
for mos in transistor_list:
  row=[mos]
  for feature in feature_list:
    row.append(df_mos.at[mos,feature])
  table.append(row)

for i in range(Nmos):
  for j in [1,2]:
    table[i][j]=table[i][j]/1e-6
  for j in [5]:
    table[i][j]=table[i][j]/1e-3

Markdown(tabulate(
  table,
  headers=["Transistor","$I_D\\;[nA]$","$I_{spec}\\;[nA]$","$IC$ [-]","$n$ [-]","$V_{DSsat}\\;[mV]$"],
  numalign="center",
  stralign="center",
  colalign=("center","center","center","center","center","center"),
  floatfmt=(".3f",".3f",".3f",".3f",".2f",".0f")
))
```


```{python}
#| label: tbl-ng_op2
#| tbl-cap: Small-signal operating point information extracted from ngspice .op file for each transistor.

feature_list=["Gm","Gms","Gds"]

Nfeature=len(feature_list)
table=[]
for mos in transistor_list:
  row=[mos]
  for feature in feature_list:
    row.append(df_mos.at[mos,feature])
  table.append(row)

for i in range(Nmos):
  for j in [1,2]:
    table[i][j]=table[i][j]/1e-6
  for j in [3]:
    table[i][j]=table[i][j]/1e-9

Markdown(tabulate(
  table,
  headers=["Transistor","$G_m\\;[\\mu A/V]$","$G_{ms}\\;[\\mu A/V]$","$G_{ds}\\;[nA/V]$"],
  numalign="center",
  stralign="center",
  colalign=("center","center","center","center"),
  floatfmt=(".3f",".3f",".3f",".3f")
))
```

### Transfer function
We now can simulate the transfer function which is plotted in @fig-simulated_tf. We see that the simulation results are close to the theoretical vaues. The $GBW$ is slightly lower than specs despite the simulated transconductance $G_{m1}$ is larger. This is coming from the self-loading. Indeed, since M~2~ is biased in weak inversion, it is a fairly large transistor and therefore its paraisitc capacitance at the drain adds to the load capaciance. We could estimate this parasitic capacitance and add it to the load capacitance.

```{python}
#| label: fig-simulated_tf
#| fig-cap: "Simulated gain response compared to theoretical estimation."

def Av(s,Adc,wp):
  num=Adc
  den=1+s/wp
  return num/den

simtype = ".ac"
fileName = circuitName + simtype
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

if newSim:
  #print('Starting NGSpice simulation...\n')
  result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

df_sim=pd.read_table(simulationData, sep=' +', engine='python')
fsim=df_sim['frequency'].to_numpy()
Av1magdBsim=df_sim['Av1magdB'].to_numpy()
Av1phidegsim=df_sim['Av1phdeg'].to_numpy()
AvmagdBsim=df_sim['AvmagdB'].to_numpy()
Avphidegsim=df_sim['Avphdeg'].to_numpy()
Npts=len(fsim)
fmin=fsim[0]
fmax=fsim[Npts-1]

regexp = re.compile(r'adc' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      AdcdBsim=float(match.group(1))

regexp = re.compile(r'gbw' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      GBWsim=float(match.group(1))

regexp = re.compile(r'fc' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      fcsim=float(match.group(1))

regexp = re.compile(r'fz' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      fzsim=float(match.group(1))

AdcdBsim=AvmagdBsim[0]
Adcsim=pow(10,AdcdBsim/20)

fthe=fsim
AvmagdBthe=np.zeros(Npts)
Avphidegthe=np.zeros(Npts)
Adcthe=-Adcspec
AdcdBthe=20*log(abs(Adcthe))
GBWthe=GBWspec
fpthe=GBWspec/Adcspec
wpthe=2*pi*fpthe

for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe[k]=20*log(abs(Av(jw,Adcthe,wpthe)))
  Avphidegthe[k]=np.angle(Av(jw,Adcthe,wpthe),deg=True)

Avphidegthe=Avphidegthe-360
Av1phidegsim=Av1phidegsim-360
Avphidegsim=Avphidegsim-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[-40,0],'k--', linewidth=lw)
#axs[0].semilogx([fmin,fmax],[AdcinfdB,AdcinfdB],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='$|A_v(out)|$ (sim.)')
axs[0].semilogx(fthe,AvmagdBthe,'r-',label='$|A_v(out)|$ (theory)')
axs[0].semilogx(fsim,Av1magdBsim,'r--', label='$|A_v(1)|$ (sim.)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,100), ylabel='$|A_v|$ [dB]')
axs[0].set_yticks([-40,-20,0,20,40,60,80,100])
axs[0].legend(loc='upper right')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)'))
axs[0].text(1.05, 0.5, textstr1, ha='left', va='center', transform=axs[0].transAxes, size=textFontSize,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})

#axs[1].semilogx([fmin,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='$arg\\{A_v(out)\\}$ (sim.)')
axs[1].semilogx(fthe,Avphidegthe,'b-',label='$arg\\{A_v(out)\\}$ (theory)')
#axs[1].semilogx(fsim,Av1phidegsim,'b--', label='$A_v(1)$ (sim.)')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_v\\}$ [degree]')
#axs[1].set_xticks([1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$W_1 =$ {W1/1e-6:.3f} $\\mu m$',
  f'$L_1 =$ {L1/1e-6:.3f} $\\mu m$',
  f'$W_2 =$ {W2/1e-6:.3f} $\\mu m$',
  f'$L_2 =$ {L2/1e-6:.3f} $\\mu m$',
  f'$V_{{b2}} =$ {Vb2:.2f} V'))
axs[1].text(1.05, 0.5, textstr2, ha='left', va='center', transform=axs[1].transAxes, size=textFontSize,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
plt.subplots_adjust(hspace=0.1)
if savePlots: saveFigures(savePlotPath, 'CS_GBW_tf_long_sim')
plt.show()
plt.style.use(pltStyle)
```

### Input-referred noise
We can compare the theoretical input-referred noise to that obtained from simulations. The simulation results are presented in @fig-ng_noise.

```{python}
#| label: fig-ng_noise
#| fig-cap: "Simulated input-referred noise PSD compared to theoretical estimation."

simtype = ".nz"
fileName = circuitName + simtype
simulationPath = "./Simulations/ngspice/"
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"
simulationRaw = simulationPath + fileName + ".raw"

if newSim:
  #print('Starting NGSpice simulation...\n')
  result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

regexp = re.compile(r'No. Variables:' + refloat)
with open(simulationRaw) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      Nvar=int(match.group(1))

#print(Nvar)

regexp = re.compile(r'No. Points:' + refloat)
with open(simulationRaw) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      Npts=int(match.group(1))

#print(Npts)

df_noise=pd.read_table(simulationRaw, sep=r'\s+', skiprows=(23), skip_blank_lines=True, header=None, dtype=np.float64, engine='python')

fsim=np.zeros(Npts)
Vnin=np.zeros(Npts)
VnoutM1=np.zeros(Npts)
VnoutM1f=np.zeros(Npts)
VnoutM1w=np.zeros(Npts)
VnoutM2=np.zeros(Npts)
VnoutM2f=np.zeros(Npts)
VnoutM2w=np.zeros(Npts)
Vnout=np.zeros(Npts)
SnoutM1=np.zeros(Npts)
SnoutM1f=np.zeros(Npts)
SnoutM1w=np.zeros(Npts)
SnoutM2=np.zeros(Npts)
SnoutM2f=np.zeros(Npts)
SnoutM2w=np.zeros(Npts)
A=np.zeros(Npts)
SninM1=np.zeros(Npts)
SninM1f=np.zeros(Npts)
SninM1w=np.zeros(Npts)
SninM1dBv=np.zeros(Npts)
SninM1fdBv=np.zeros(Npts)
SninM1wdBv=np.zeros(Npts)
SninM2=np.zeros(Npts)
SninM2f=np.zeros(Npts)
SninM2w=np.zeros(Npts)
SninM2dBv=np.zeros(Npts)
SninM2fdBv=np.zeros(Npts)
SninM2wdBv=np.zeros(Npts)
Snin=np.zeros(Npts)
SnindBv=np.zeros(Npts)
Sninf=np.zeros(Npts)
SninfdBv=np.zeros(Npts)
Sninw=np.zeros(Npts)
SninwdBv=np.zeros(Npts)
for k in range(0,Npts):
  fsim[k]=df_noise.iloc[Nvar*k,1]
  Vnin[k]=df_noise.iloc[Nvar*k+1,0]
  VnoutM1[k]=df_noise.iloc[Nvar*k+2,0]
  VnoutM1f[k]=df_noise.iloc[Nvar*k+3,0]
  VnoutM1w[k]=df_noise.iloc[Nvar*k+4,0]
  VnoutM2[k]=df_noise.iloc[Nvar*k+5,0]
  VnoutM2f[k]=df_noise.iloc[Nvar*k+6,0]
  VnoutM2w[k]=df_noise.iloc[Nvar*k+7,0]
  Vnout[k]=df_noise.iloc[Nvar*k+14,0]
  SnoutM1[k]=VnoutM1[k]**2
  SnoutM1f[k]=VnoutM1f[k]**2
  SnoutM1w[k]=VnoutM1w[k]**2
  SnoutM2[k]=VnoutM2[k]**2
  SnoutM2f[k]=VnoutM2f[k]**2
  SnoutM2w[k]=VnoutM2w[k]**2
  A[k]=Vnout[k]/Vnin[k]
  SninM1[k]=SnoutM1[k]/A[k]**2
  SninM1f[k]=SnoutM1f[k]/A[k]**2
  SninM1w[k]=SnoutM1w[k]/A[k]**2
  SninM1dBv[k]=10*log(SninM1[k])
  SninM1fdBv[k]=10*log(SninM1f[k])
  SninM1wdBv[k]=10*log(SninM1w[k])
  SninM2[k]=SnoutM2[k]/A[k]**2
  SninM2f[k]=SnoutM2f[k]/A[k]**2
  SninM2w[k]=SnoutM2w[k]/A[k]**2
  SninM2dBv[k]=10*log(SninM2[k])
  SninM2fdBv[k]=10*log(SninM2f[k])
  SninM2wdBv[k]=10*log(SninM2w[k])
  Snin[k]=SninM1[k]+SninM2[k]
  Sninf[k]=SninM1f[k]+SninM2f[k]
  Sninw[k]=SninM1w[k]+SninM2w[k]
  SnindBv[k]=10*log(Snin[k])
  SninfdBv[k]=10*log(Sninf[k])
  SninwdBv[k]=10*log(Sninw[k])

Gm1=mos_df.at['M1','Gm']
Gms2=mos_df.at['M2','Gms']
Gds1=mos_df.at['M1','Gds']
n2=mos_df.at['M2','n']
gamman1=mos_df.at['M1','gamman']
gamman2=mos_df.at['M2','gamman']
deltan2=gamman2/n2
RninthM1=gamman1/Gm1
SninthM1=4*kT*RninthM1
RninthM2=(Gds1/Gm1)**2*deltan2/Gms2
SninthM2=4*kT*RninthM2

W1=mos_df.at['M1','W']
L1=mos_df.at['M1','L']
W2=mos_df.at['M2','W']
L2=mos_df.at['M2','L']

Weff1=W1+DWn
Leff1=L1+DLn
Weff2=W2+DWn
Leff2=L2+DLn
WLeff1=Weff1*Leff1
WLeff2=Weff2*Leff2
RninflM11Hz=rhon/WLeff1
RninflM21Hz=(Gds1/(n2*Gm1))**2*rhon/WLeff2

fthe=fsim

SninM1dBvthe=np.zeros(Npts)
SninM2dBvthe=np.zeros(Npts)
SnindBvthe=np.zeros(Npts)

for k in range(0,Npts):
  RninM1=RninthM1+RninflM11Hz/fthe[k]
  SninM1=4*kT*RninM1
  SninM1dBvthe[k]=10*log(SninM1)
  RninM2=RninthM2+RninflM21Hz/fthe[k]
  SninM2=4*kT*RninM2
  SninM2dBvthe[k]=10*log(SninM2)
  Sninthe=SninM1+SninM2
  SnindBvthe[k]=10*log(Sninthe)

fmin=fsim[0]
fmax=fsim[Npts-1]

plt.semilogx(fthe,SninM1dBvthe,'b-', label='M1 (theory)')
plt.semilogx(fsim,SninM1dBv,'bo', markevery=25, markersize=4, label='M1 (sim.)')
plt.semilogx(fthe,SninM2dBvthe,'g-', label='M2 (theory)')
plt.semilogx(fsim,SninM2dBv,'go', markevery=25, markersize=4, label='M2 (sim.)')
plt.semilogx(fthe,SnindBvthe,'r-', label='Total (theory)')
plt.semilogx(fsim,SnindBv,'ro', markevery=25, markersize=4, label='Total (sim.)')
plt.xlim(fmin,fmax)
plt.xticks([1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8,1e9])
plt.xlabel('Frequency [Hz]')
plt.ylim(-200,-80)
#plt.yticks(range(-155,-135,5))
plt.ylabel('Input Noise $[dBv/\\sqrt{Hz}]$')
#plt.legend(loc='lower left')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
#textstr = '\n'.join((
#  f'$S_{{nin,th}} =$ {SninthewdBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ (theory)',
#  f'$S_{{nin,th}} =$ {SninwdBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ (sim.)'))
#  f'$f_k =$ {fkthe/1e3:.1f} kHz (theory)'))
#    f'$f_k =$ {fksim/1e3:.1f} kHz (sim.)'))
#plt.text(1.05, 0.05, textstr, ha='left', va='bottom', size=textFontSize, transform=plt.gca().transAxes,
#         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
if savePlots: saveFigures(savePlotPath, otaName + '_input_white_noise_sim')
plt.show()
```

The simulation confirms that, below the gain-bandwidth product $GBW =$ `{python} f'{GBWspec/1e6:.0f}'` $MHz$, the square root of the input-referred flicker noise of M~2~ is about 40 dB lower than that of M~1~.

# Regulated cascode

![Schematic of the regulated cascode @bib:sackinger:jssc:25:1:1990.](Figures/reg_cascode_schematic.png){#fig-reg_cascode_schematic}

The schematic of @fig-reg_cascode_schematic corresponds to the regulated cascode, where an additional CS gain stage M~3~ is added @bib:sackinger:jssc:25:1:1990.

## Small-signal analysis

![Small-signal schematic of the regulated cascode.](Figures/reg_cascode_small-signal_schematic.png){#fig-reg_cascode_small-signal_schematic}

The small-signal equivalent schematic of the regulated cascode of @fig-reg_cascode_schematic is presented in @fig-reg_cascode_small-signal_schematic.

Solving the KCL equations accounting for all the output conductances leads to the full expression of the equivalent transconductance given by
\begin{equation}
  G_{meq} = G_{m1}\cdot\frac{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds2})}{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds2}+G_{ds1})}
\end{equation}
which for $G_{ms2} \gg G_{ds2}, G_{ds1}$ reduces to
\begin{equation}
  G_{meq} \cong G_{m1}.
\end{equation}
This is the same result as the cascode stage.

The output conductance accounting for all transistor's output conductances is given by
\begin{equation}
  G_{out} = G_{ds1} \cdot \frac{G_{ds2}\,G_{ds3}}{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds2}+G_{ds1})},
\end{equation}
which for $G_{ms2} \gg G_{ds2}, G_{ds1}$ reduces to
\begin{equation}\label{eqn:sol2:gout}
  G_{out} \cong G_{ds1}\cdot\frac{G_{ds2}}{G_{m2}}\cdot\frac{G_{ds3}}{G_{m3}}.
\end{equation}
Equation \eqref{eqn:sol2:gout} shows that the output conductance of the regulated cascode is equal to the output conductance of M~1~ divided by the square of a transistor self-gain, i.e. $(G_m/G_{ds})^2$, this is $G_m/G_{ds}$ smaller than that of the simple cascode.

The voltage gain is given by
\begin{equation}
  A_v \triangleq \frac{\Delta V_{out}}{\Delta V_{in}} = -\frac{G_{meq}}{G_{out}}
  = -G_{m1}\cdot\frac{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds2})}{G_{ds1}\,G_{ds2}\,G_{ds3}},
\end{equation}
which for $G_{ms2} \gg G_{ds2}, G_{ds1}$ and reduces to
\begin{equation}\label{eqn:sol2:av}
  A_v \cong -\frac{G_{m1}}{G_{ds1}}\cdot\frac{G_{m2}}{G_{ds2}}\cdot\frac{G_{m3}}{G_{ds3}}.
\end{equation}
Equation \eqref{eqn:sol2:av} shows that the voltage gain of the regulated cascode is equivalent of that of a cascade of three stages, i.e. $(G_m/G_{ds})^3$. However, this gain is achieved at lower current since the driver transistor M~1~ and the cascode transistor M~2~ share the same current (current reuse).

## Noise analysis

![Small-signal schematic of the regulated cascode including the noise sources.](Figures/reg_cascode_small-signal_schematic_noise.png){#fig-reg_cascode_small-signal_schematic_noise}

The equivalent small-signal circuit of the regulated cascode including all the noise current sources for the calculation of the noise is shown in @fig-reg_cascode_small-signal_schematic_noise. The output noise current is given by
\begin{equation}
  I_{nout} = H_{n1}\cdot I_{n1} + H_{n2} \cdot I_{n2} + H_{n3} \cdot I_{n3}.
\end{equation}
where
\begin{align}
  H_{n1} &= \frac{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds2})}{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds1}+G_{ds2})}
  \cong 1,\label{eqn:sol2:hn1}\\
  H_{n2} &= \frac{G_{ds1}\,G_{ds3}}{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds1}+G_{ds2})}
  \cong \frac{G_{ds1}\,G_{ds3}}{G_{m2}\,G_{m3}},\label{eqn:sol2:hn2}\\
  H_{n3} &= -\frac{G_{m2}\,G_{ds1}}{G_{m2}\,G_{m3}+G_{ds3}\,(G_{ms2}+G_{ds1}+G_{ds2})}
  \cong -\frac{G_{ds1}}{G_{m3}}.\label{eqn:sol2:hn3}
\end{align}
Equation \eqref{eqn:sol2:hn1} shows that the noise of M~1~ is transferred directly to the output. Equation \eqref{eqn:sol2:hn2} shows that the noise of M~2~ is divided by the square of a transistor self-gain, i.e. divided by $(G_m/G_{ds})^2$. Finally, the noise coming from M~3~ is divided by $G_m/G_{ds}$. This means that the noise coming from M~2~ and M~3~ are much smaller than the noise coming from M~1~ and can usually be neglected. We still will derive the output and input-referred noise accounting for all contributions.

The output noise conductance is given by
\begin{equation}
  G_{nout} = |H_{n1}|^2 \cdot G_{n1} + |H_{n2}|^2 \cdot G_{n2} + |H_{n3}|^2 \cdot G_{n3}
\end{equation}
where $G_{ni}$ is given by \eqref{eqn:Gni}.

The input-referred noise is obtained by dividing $G_{nout}$ by $G_{meq}^2$, resulting in
\begin{equation}
  R_{nin} = \frac{G_{nout}(f)}{G_{meq}^2}.
\end{equation}
It can be decomposed into a thermal and flicker noise component according to
\begin{equation}
  R_{nin} = R_{nt} + R_{nf}(f)
\end{equation}
$R_{nt}$ is the total input-referred thermal noise resistance which can be written as
\begin{equation}
  R_{nt} = \frac{\gamma_{n1}}{G_{m1}} \cdot (1+\eta_{th}),
\end{equation}
where $\eta_{th}$ represents the contributions of M~2~ and M~3~ to $R_{nt}$ relative to that of M~1~
\begin{equation}\label{eqn:sol2:etath}
  \eta_{th} \cong \frac{\gamma_{n2}}{\gamma_{n1}} \cdot \frac{G_{ds1}^2\,G_{ds3}^2}{G_{m1}\,G_{m2}\,G_{m3}^2}
  + \frac{\gamma_{n3}}{\gamma_{n1}} \cdot \frac{G_{ds1}^2}{G_{m1}\,G_{m3}}.
\end{equation}
$\eta_{th}$ allows to compare the contributions of M~2~ and M~3~ to that of M~1~. It confirms what we already have mentioned above, namely that the contribution of M~2~ (first term in \eqref{eqn:sol2:etath}) is completely negligible since it is inversely proportional to $(G_m/G_{ds})^4$. The contribution of M~3~ (second term in \eqref{eqn:sol2:etath}) is also very small since it is inversely proportional to $(G_m/G_{ds})^2$.

The input-referred flicker noise resistance is given by
\begin{equation}
  R_{nf} = \frac{\rho_n}{f\,W_1\,L_1} \cdot (1 + \eta_{fl}),
\end{equation}
where $\eta_{fl}$ represents the contributions of M~2~ and M~3~ to $R_{nf}$ relative to the that of M~1~
\begin{equation}\label{eqn:sol2:etafl}
  \eta_{fl} \cong \left(\frac{G_{ds1}}{G_{m1}} \cdot \frac{G_{ds3}}{G_{m3}}\right)^2 \cdot \frac{W_1\,L_1}{W_2\,L_2}
  + \left(\frac{G_{ds1}}{G_{m1}}\right)^2 \cdot \frac{W_1\,L_1}{W_3\,L_3}.
\end{equation}
Similarly to the thermal noise, assuming that M~1~, M~2~ and M~3~ have all about the same gate area, \eqref{eqn:sol2:etafl} shows that the contribution of M~2~ (first term in \eqref{eqn:sol2:etafl}) is completely negligible since it is inversely proportional to $(G_m/G_{ds})^4$. The contribution of M~3~ (second term in \eqref{eqn:sol2:etafl}) is also very small since it is inversely proportional to $(G_m/G_{ds})^2$.

The equivalent noise excess factor of the regulated cascode can then be written as
\begin{equation}\label{eqn:rc:gammaneq}
  \gamma_{neq} \triangleq G_{meq} \cdot R_{nt} \cong \gamma_{n1} \cdot (1 + \eta_{th}) \cong \gamma_{n1}.
\end{equation}

This shows that, similarly to the cascode gain stage, the noise of the regulated cascode is dominated by the contribution of the driver transistor M~1~.

# Gain boosting

::: {.callout-note}
This section is not part of the exercises, however it is an extension to Problem 2 and is therefore good to know.
:::

![The principle of gain-boosting @bib:bult:jssc:25:6:dec:1990 @bib:bult:aicsp:1991.](Figures/gain_boosting_schematic.png){#fig-gain_boosting_schematic}

The regulated cascode is a special case of the more general concept of gain boosting illutrated in @fig-gain_boosting_schematic  @bib:bult:jssc:25:6:dec:1990 @bib:bult:aicsp:1991, where the amplifier is implemented by the simple CS source stage M~3~ of @fig-reg_cascode_schematic. It has basically the same properties than the regulated cascode except that the gain $A$ can be made much larger than that of a simple CS stage.

## Small-signal analysis

![Small-signal equivalent circuit used for the calculation of the equivalent transconductance.](Figures/gain_boosting_small-signal_gm.png){#fig-gain_boosting_small-signal_gm}

Using the small-signal equivalent circuit shown in @fig-gain_boosting_small-signal_gm, it is easy to show that the equivalent transconductance of the gain-boosting circuit of @fig-gain_boosting_schematic is given by
\begin{equation}\label{eqn:gb:gm}
  G_{meq} = G_{m1} \cdot \frac{G_{ds2}+G_{m2}\,(A+n_2)}{G_{ds1}+G_{ds2}+G_{m2}\,(A+n_2)}.
\end{equation}
For $A \gg n_2$, the output conductances can be neglected $G_{ds1},G_{ds2} \ll A\,G_{m2}$ and \eqref{eqn:gb:gm} reduces to
\begin{equation}
  G_{meq} \cong G_{m1}.
\end{equation}
Similarly to the cascode and regulated cascode, the equivalent transconductance of the gain boosting circuit is simply equal to the transconductance of the driver transistor M~1~.

![Small-signal equivalent circuit used for the calculation of the output conductance.](Figures/gain_boosting_small-signal_gout.png){#fig-gain_boosting_small-signal_gout}

The output conductance can be calculated using the small-signal circuit shown in @fig-gain_boosting_small-signal_gout, which leads to
\begin{equation}
  G_{out} = G_{ds1} \cdot \frac{G_{ds2}}{G_{m2}\,(A+n_2)} \cong G_{ds1} \cdot \frac{G_{ds2}}{G_{m2}} \cdot \frac{1}{A},
\end{equation}
which corresponds to the output conductance of the regulated cascode in case $A=G_{m3}/G_{ds3}$, but can be made smaller with a larger amplifier gain $A$. It can therefore be made very small.

![Small-signal equivalent circuit used for the calculation of the voltage gain.](Figures/gain_boosting_small-signal_av.png){#fig-gain_boosting_small-signal_av}

The voltage gain can be calculated with the help of the schematic shown in @fig-gain_boosting_small-signal_av resulting in
\begin{equation}
  A_v = -\frac{G_{m1}\,(G_{ds2}+G_{m2}\,(A+n_2))}{G_{ds1}\,G_{ds2}} 
  = -\frac{G_{m1}}{G_{ds1}} \cdot \left(\frac{G_{m2}}{G_{ds2}} \cdot (A+n_2)+1\right),
\end{equation}
which for $A \gg n_2$ simplifies to
\begin{equation}\label{eqn:Av:gb}
  A_v \cong -\frac{G_{m1}}{G_{ds1}} \cdot \frac{G_{m2}}{G_{ds2}} \cdot A.
\end{equation}
Equation \eqref{eqn:Av:gb} corresponds to the voltage gain of the regulated cascode in case $A=G_{m3}/G_{ds3}$, but can be made larger with a larger amplifier gain $A$.

## Noise analysis

![Small-signal equivalent circuit used for the calculation of the noise.](Figures/gain_boosting_small-signal_noise.png){#fig-gain_boosting_small-signal_noise}

The noise can be analyzed by means of the small-signal circuit shown in @fig-gain_boosting_small-signal_noise. It is easy to show that the input-referred noise resistance is given by
\begin{equation}\label{eqn:gb:rnin}
  R_{nin} \cong \frac{G_{n1}}{G_{m1}^2} + \left(\frac{G_{ds1}}{A\,G_{m1}\,G_{m2}}\right)^2 \cdot G_{n2} + \left(\frac{G_{ds1}}{G_{m1}}\right)^2 \cdot R_{noa},
\end{equation}
where $R_{noa}$ is the amplifier equivalent input-referred noise resistance. Similarly to the regulated cascode, the noise of the cascode transistor M~2~ is divided by the square of the gain $A\,G_{m1}/G_{ds1}$ and can therefore be completely neglected. The impact of the noise coming from the amplifier depends very much on how this amplifier is implemented. If we want to make its contribution corresponding to the last term in \eqref{eqn:gb:rnin} smaller than the contribution of M~1~, we need to have
\begin{equation}\label{eqn:gb:rnoa}
  \frac{G_{n1}}{G_{ds1}^2} > R_{noa}.
\end{equation}
If we only consider the thermal noise, then $G_{n1} = \gamma_{n1}\,G_{m1}$ and $R_{oa} = \gamma_{noa}/G_{moa}$ and \eqref{eqn:gb:rnoa} becomes
\begin{equation}
  \gamma_{noa} < \gamma_{n1}\,\frac{G_{m1}\,G_{moa}}{G_{ds1}^2}
\end{equation}
which should not be too difficult to achieve assuming the gain of the driver $G_{m1}/G_{ds1}$ is large enough.

# Conclusion
Cascoding is an eeficient way to increase the output resistance and the voltage gain without spending more current and without degrading the noise at the cost of a slightly reduced output voltage swing. A simple cascode stage achieves the same gain as the cascade of two CS stages, but with less current and less noise.

# References