Hide code cell source
import pandas as pd
import numpy as np
import sys, os
import schemdraw

# use engineering format in pandas tables
pd.set_eng_float_format(accuracy=2, use_eng_prefix=True)

# import my helper functions
sys.path.append('../helpers')
from xtor_data_helpers import load_mat_data, lookup, scale
import bokeh_helpers as bh
from pandas_helpers import pretty_table

from bokeh.plotting import figure, show
from bokeh.io import output_notebook
from bokeh.models import ColumnDataSource, LinearAxis, Range1d
from bokeh.palettes import Turbo10
from bokeh.transform import linear_cmap
output_notebook(hide_banner=True)

Example 3.1: Intrinsic Gain Stage#

We’ll use an IGS as a way to walk through the systematic design flow. Since the IGS is the root of both common-source amplifiers and differential amplifiers, we should be able to leverage what we learn here for more complex circuits.

circuit diagram of an intrinsic gain stage

Circuit analysis#

If we insert the small signal model of the transitor into the above schematic, we get:

We’ll use some simplifiying assumptions:

  • \(C_{db} << C_L\)

  • \(C_{gd} << C_L\)

  • Junction capacitances can be ignored (for now)

Under those conditions, the frequency response can be approximated by:

\[ A_v(j\omega) = \frac{v_{out}}{v_{in}} \cong \frac{A_{v0}}{1 + j \frac{\omega}{\omega_c}} \]

Where:

\[ A_{v0} = - \frac{g_m}{g_{ds}} = -A_{intr} \]

and

\[ \omega_c = \frac{g_{ds}}{C_L} \]

Another thing to note is the unity gain frequency (\(\omega_u\)), given by:

\[ \omega_u = \frac{g_m}{C_L} \]

It’s useful to measure the IGS’ fanout (FO), or the ratio of the capacitance it can drive to it’s input capacitance:

\[ FO = \frac{C_L}{C_{gs} + C_{gd} + C_{gb}} = \frac{C_L}{C_{gg}} \]

We could rewrite this as:

\[ FO = \frac{g_m / C_{gg}}{g_m / C_L} = \frac{\omega_T}{\omega_u} \]

Because the quasi-static transistor model used above breaks down as frequency approaches about 1/10th of \(f_t\) in moderate or strong inversion, we want to keep FO > 10. (i.e., keep \(\omega_u\) under 1/10th of \(\omega_t\))

Sizing considerations#

So if we know the gain and frequency response, we want to do the following for some given combination of \(C_L\) and \(f_u\) target:

  • Find drain current

  • Find device length

  • Find device width

We can use this flow:

  1. Determine \(g_m\) (from design specs)

  2. Pick L:

    1. Short channel: high speed, low area

    2. Long channel: high intrinsic gain, improved matching

  3. Pick \(g_m\over{I_d}\):

    1. large \(g_m\over{I_d}\): low mpower, large signal swing (low \(V_{dsat}\))

    2. small \(g_m\over{I_d}\): high speed, small area

  4. Determine \(I_d\) (from \(g_m\) and \(g_m\over{I_d}\))

  5. Determine W (from \(I_d\over{W}\))

If we are given a spec for \(\omega_u\), then \(g_m\) is fixed by \(\omega_u = \frac{g_m}{C_L}\).

But, we need more constraints to pick L and \(\frac{g_m}{I_d}\).

We could say that \(I_d = \frac{g_m}{g_m / I_d}\). Based on that, maybe we’d say we want to maximize \({g_m / I_d}\) to minimize drain current. But doing that requires a large device and a low \(f_t\), which may lead to violating the guideline that FO < 10.

This leads to the following: determining the right inversion level is complicated, and we need to take a lot of different factors into account. So we’re going to have to work out different constraints to help us pick a suitable inversion level.

For now, let’s talk about how to pick W given a L and \(g_m\over{I_d}\).

Sizing for given L and \(g_m\over{I_d}\)#

If we know those, finding W becomes:

\[ W = \frac{I_d}{J_d} = \frac{I_d}{I_d / W} \]

We can call this flow “denormalization”, and it looks like this:

denormalization flow

Once we know \(g_m / I_d\), L, \(V_{ds}\) and \(V_{sb}\), we can plug those into our lookup table and find the correpsonding \(J_d = I_d / W \). And when we know \(I_d\) from the required \(\omega_u\), \(C_L\) and \(g_m / I_d\), we can scale the W appropriately.

Note that we’re assuming that drain current scales linearly with width, which ignores narrow-width effects. If that doesn’t hold, we’d ideally have lookup tables with different widths and corresponding drain currents, or some non-linear scaling formula. This could be a good place to insert some ML; we could train a model to predict device performance based on physical parameters.

Let’s look at a concrete example…

Example: basic IGS sizing#

Suppose we want to design an IGS with:

  • \(f_u\) = 1 GHz

  • \(C_L\) = 1 pF

Assume:

  • L = 180nm

  • \(g_m / I_d\) = 15 S/A (moderate inversion)

  • \(V_{ds}\) = 0.6 V

  • \(V_{sb}\) = 0 V

Find the low frequency gain and the Early voltage of the transistor.

Solution:#

We’ll start by loading in our transitor data, supplied by Professor Murmann through his GitHub repo:

Hide code cell source
# first, load up the nch dataset
nch_data_df = load_mat_data("../../Book-on-gm-ID-design-main/starter_kit/180nch.mat")
Loading data from ../../Book-on-gm-ID-design-main/starter_kit/180nch.mat
Found the following columns: ['ID', 'VT', 'GM', 'GMB', 'GDS', 'CGG', 'CGS', 'CGD', 'CGB', 'CDD', 'CSS', 'STH', 'SFL', 'INFO', 'CORNER', 'TEMP', 'VGS', 'VDS', 'VSB', 'L', 'W', 'NFING']
/Users/sean/.pyenv/versions/3.10.4/lib/python3.10/site-packages/pandas/core/internals/construction.py:576: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  values = np.array([convert(v) for v in values])

With that in place, we can look in our dataset to find transitors that match our givens, and interpolate for \(\frac{g_m}{I_d}\) = 15:

Hide code cell source
# since we're given values for L, gm/Id, Vds and Vsb, I can lookup the associated Jd
# first, grab the subsection of data that matches the givens
biasing_mask = (
    (nch_data_df['VDS'] == 0.6) &
    (nch_data_df['VSB'] == 0.0) &
    (nch_data_df['L'] == 0.18)
    )

# now interpolate at gm/id = 15
lookup_df,interp_df = lookup(df=nch_data_df[biasing_mask], param='GM_ID', target=15)

cols = ['W', 'L', 'ID', 'GM', 'GM_ID', 'JD', 'VGS', 'VDS', 'VSB']
caption = "NMOS device data matching givens"

display(
    pretty_table(
        df=lookup_df,
        cols=cols,
        caption=caption
    )
    )
NMOS device data matching givens
W L ID GM GM_ID JD VGS VDS VSB
5.000000 0.180 56.78u 850.55u 15 11.36u 599.15m 600.00m 0.00m

Since we know that \(f_u\) is 1 GHz and \(C_L\) is 1pF, we can derive the required \(g_m\):

\[ \omega_u = \frac{g_m}{C_L} \]
\[ 2\pi * 1e9 = \frac{g_m}{1e-12} \]
Hide code cell source
gm_spec = 2 * 3.14159 * 1e9 * 1e-12
print(f"The required gm is {gm_spec*1e3:.2f} mS")
The required gm is 6.28 mS

Given a \(g_m\over{I_d}\) of 15 and a \(g_m\) of 6.283e-3, our required drain current is:

Hide code cell source
id_spec = gm_spec / lookup_df['GM_ID'].values[0]
print(f"The required drain current is {id_spec*1e6:.2f} uA")

scale_factor = id_spec / lookup_df['ID'].values[0]
print(f"This means we'll need to scale the device by {scale_factor:.2f}")
The required drain current is 418.88 uA
This means we'll need to scale the device by 7.38
Hide code cell source
scaled_df = scale(
    df=lookup_df,
    scale_factor=scale_factor
)
cols=['W', 'L', 'ID', 'GM', 'GM_ID', 'JD', 'GDS', 
      'CGG', 'CGS', 'CGD', 'VGS', 'VDS']
caption = "Scaled device dimensions and parameters"
display(pretty_table(
    df=scaled_df,
    cols=cols,
    caption=caption,
    transpose=True)
)
Scaled device dimensions and parameters
W 36.88
L 180.00m
ID 418.88u
GM 6.27m
GM_ID 15.00
JD 11.36u
GDS 170.57u
CGG 74.44f
CGS 52.80f
CGD 17.84f
VGS 599.15m
VDS 600.00m

Ok, now let’s calculate what the question actually asked for; intrinsic gain and early voltage:

Hide code cell source
Av0 = -scaled_df['GM'].values[0] / scaled_df['GDS'].values[0]
print(f"The low frequency gain is {Av0:.2f}")

ft = scaled_df['GM'].values[0] / scaled_df['CGG'].values[0] / (2*3.14159)
print(f"The transit frequency is {ft/1e9:.2f} GHz")
The low frequency gain is -36.78
The transit frequency is 13.41 GHz

And remember that Early voltage is defined as:

\[ V_A = \frac{I_d}{g_{ds}} \]
Hide code cell source
early_voltage = scaled_df['ID'].values[0] / scaled_df['GDS'].values[0]
print(f"The early voltage is {early_voltage:.2f}")
The early voltage is 2.46

Tradeoff exploration#

What happens if we don’t assume \(g_m\over{I_d}\) and L?

Let’s plot intrinsic gain and transit frequency vs. \(g_m\over{I_d}\) and L:

Hide code cell source
TOOLTIPS = [
            ("x", "$x"),
            ("y", "$y")
        ]

# first, select the data that we want:
length_filter = [0.180, 0.240, 0.300, 0.360]
length_colors = {
    0.180: "blue",
    0.240: "green",
    0.300: "yellow",
    0.360: "red"
}
tradeoff_mask = ((nch_data_df['VDS'] == 0.6) & 
                 (nch_data_df['VSB'] == 0) &
                 (nch_data_df['L'].isin(length_filter))
                 )
tradeoff_df = nch_data_df[tradeoff_mask]

# create a figure
tradeoff_plot = bh.create_bokeh_plot(
    title="Intrinsic gain and transit frequency vs. gm/Id and L",
    x_axis_label="gm / Id (S/A)",
    y_axis_label="ft (GHz)",
    tooltips=TOOLTIPS,
    width=800,
)

# create a secondary y-axis
tradeoff_plot.extra_y_ranges['gain'] = Range1d(0, 100)
ax2 = LinearAxis(y_range_name='gain', axis_label="Intrinsic Gain")
tradeoff_plot.add_layout(ax2, 'right')

# plot lines for each of the gate lengths we selected

for len, len_group in tradeoff_df.groupby('L'):
    data = ColumnDataSource(len_group)
    
    tradeoff_plot.line(x='GM_ID', y='GM_CGG', source=data, 
                       legend_label=f"l={len}", line_color=length_colors[len])
    
    tradeoff_plot.line(x='GM_ID', y='GM_GDS', source=data, 
                       y_range_name='gain', line_dash="dashed", 
                       line_color=length_colors[len])

show(tradeoff_plot)

From the plot, we can see two things very clearly:

  • Transit frequency drops off with increasing \(\frac{g_m}{I_d}\)

  • Intrinsic gain trades off with transit frequency as we vary the device length

In general, we’ll have to manage these tradeoffs to meet design / system specs.

That’s where modeling comes in - we should explore how to budget the system through models and system-level simulations to help us hone in on how to balance [speed / gain / power / area / etc.]