Example 3.5: Sizing in weak inversion with a width constraint

Contents

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, Turbo256, linear_palette
from bokeh.transform import linear_cmap
from bokeh.models import LogAxis, Span, LinearScale
from bokeh.layouts import layout
output_notebook(hide_banner=True)

# load up device data
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])

Example 3.5: Sizing in weak inversion with a width constraint#

There are applications where the required \(f_t\) (or \(f_u\)) is quite low, and in such cases, we could push for devices with high \(g_m\over{I_d}\).

In those cases, if we push far enough, \(g_m\over{I_d}\) can flatten out. In such cases, our methodology doesn’t work because there will be multiple points with identical \(g_m\over{I_d}\). In those scenarios, we can use this methodology instead.

Consider an ultra-low power IGS, similar to example 3.1 & 3.2, with:

  • \(C_L\) = 1 pF

  • \(I_d\) = 5 uA

Find the value of \(L\) that gives a low gain frequency of about 150, and assume that the minimum desired \(W\) is 5um.

Compute:

  1. \(V_{gs}\)

  2. \(f_t\)

  3. the circuit’s unity gain frequency

Assume:

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

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

Solution#

Let’s start by computing the current density:

Hide code cell source
# I don't love how the matlab data comes with widths in units
# of um instead of meters; I should consider changing this in the
# future.
w = 5
i_d = 0.0008e-6
j_d = i_d / w
print(f"The desired current density is {j_d:.3e} A/um, or {j_d*1e9:.3f} nA/um")
The desired current density is 1.600e-10 A/um, or 0.160 nA/um

Given that current density, we can plot intrinsic gain vs. current density to find the \(L\) that gives a gain of ~150 at 0.160 nA/um:

Hide code cell source
cmap = linear_palette(Turbo256, nch_data_df['L'].nunique())
length_color_pairs = list(zip(nch_data_df['L'].unique(), cmap))
length_color_dict = dict(length_color_pairs)

TOOLTIPS = [
            ("x", "$x"),
            ("y", "$y"),
            ("L", "@L")
        ]

# create a figure
gain_plot = bh.create_bokeh_plot(
    title="Intrinsic gain vs. current density and L",
    x_axis_label="Id / W (A/um)",
    y_axis_label="Intrinsic gain",
    x_axis_type="log",
    tooltips=TOOLTIPS,
    width=800,
)

plot_mask = ((nch_data_df['VDS'] == 0.6) &
    (nch_data_df['VSB'] == 0.0))

for len, len_group in nch_data_df[plot_mask].groupby('L'):
    # display(len_group)
    data = ColumnDataSource(len_group)
    
    gain_plot.line(x='JD', y='GM_GDS', source=data, 
                       legend_label=f"l={len}", line_color=length_color_dict[len],
                       line_dash="solid")
        
# gain_plot.y_range = Range1d(1e8, 100e9)

# add vertical line for gmId = 15
gain_plot.add_layout(Span(location=j_d, dimension='height'))

# make the legend interactive
gain_plot.legend.click_policy = 'hide'

show(gain_plot)

From the plot above, we can see that a gate length of 1.5um gives us a gain of roughly 150 at the desired drain current of 0.8 nA.

To solve the rest of the problelm, we’ll interpolate for the point where:

  • L = 1.5 um

  • JD (current density) = 1.6e-10 A/um

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

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

With those constraints, we get:

Hide code cell source
# let's grab nch data that gives us jd of 1.6e-10 A/um,
# L = 1.5um, with V_ds=0.6 and V_sb=0.0,
# and we set gmId to be greater than 2.5 to avoid
# weirdness with nonmonotonicity

biasing_mask = (
    (nch_data_df['VDS'] == 0.6) &
    (nch_data_df['VSB'] == 0.0) &
    (nch_data_df['L'] == 1.5) &
    (nch_data_df['GM_ID'] > 2.5)
    )

# use our interpolation function to get data with f_t of exactly 10 GHz
lookup_df, interp_df = lookup(df=nch_data_df[biasing_mask], param='JD', target=1.6e-10)

# display the results
cols = ['GM_GDS', 'GM_ID', 'GM_CGG', 'GM', 'VGS',]
caption = f"Design point for L=1.5 um and intrinsic gain of 150"
display(
    pretty_table(
        df=lookup_df,
        cols=cols,
        caption=caption
    )
)
Design point for L=1.5 um and intrinsic gain of 150
A_v0 GM_ID f_t GM VGS
151 27 0.00G 0.02u 141.71m

The only thing the table doesn’t give us is \(f_u\), since we’re not basing that off of a fanout spec. Instead, we’ll calculate that as \(\frac{g_m}{C_L}\):

Hide code cell source
c_l = 1e-12
f_u = lookup_df['GM'].values[0] / c_l
print(f"The unity gain frequency, f_u, is {f_u/1e3:0.2f} KHz")
The unity gain frequency, f_u, is 21.63 KHz