Example 3.2: Intrinsic Gain Stage, with constant g_m / I_d

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, LogScale
from bokeh.layouts import layout

output_notebook(hide_banner=True)
Hide code cell source
# 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.2: Intrinsic Gain Stage, with constant \(g_m / I_d\)#

Consider an IGS, similar to example 3.1, with:

  • \(C_L\) = 1 pF

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

Find combos of \(L\), \(W\), and \(I_d\) that achieve \(f_u\) = 100 MHz and compute the corresponding DC gain and fan-out.

Assume:

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

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

Circuit analysis#

As before, we can define \(f_u\) as:

\[ f_u = \frac{\omega_u}{2 * pi} = \frac{g_m}{C_L * 2 * pi} \]

So if we need \(f_u\) to be 100 MHz and \(C_L\) is 1 pF, that means:

Hide code cell source
gmid_spec = 15
f_u = 100e6
c_l = 1e-12
gm_spec = f_u * c_l * 2 * 3.14159
print(f"The required gm is {gm_spec*1e3:.3f} mS")
The required gm is 0.628 mS

We’ll use that later when we denormalize to find \(W\) and \(I_d\); for now, let’s plot intrinsic gain and transit frequency against \(L\) and \(\frac{g_m}{I_d}\), and see what values of \(L\) give us a transit frequency of 1 GHz at \(\frac{g_m}{I_d}\) of 15:

Hide code cell source
TOOLTIPS = [
            ("gm/Id", "$x"),
            ("y", "$y"),
            ("L", "@L")
        ]

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)

# create a figure
gmid_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)",
    y_axis_type="log",
    tooltips=TOOLTIPS,
    width=800,
)

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

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)
    
    gmid_plot.line(x='GM_ID', y='GM_CGG', source=data, 
                       legend_label=f"l={len}", line_color=length_color_dict[len],
                       line_dash="dashed")
    
    gmid_plot.line(x='GM_ID', y='GM_GDS', source=data, 
                       legend_label=f"l={len}", line_color=length_color_dict[len],
                       line_dash="solid", y_range_name='gain')
    
gmid_plot.y_range = Range1d(1e8, 100e9)

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

show(gmid_plot)
Hide code cell source
# display the same data as the plots, but in table form

# let's grab nch data that gives us gm/Id of 15

biasing_mask = (
    (nch_data_df['VDS'] == 0.6) &
    (nch_data_df['VSB'] == 0.0)
    )
lookup_df, interp_df = lookup(
    df=nch_data_df[biasing_mask],
    param='GM_ID',
    target=gmid_spec
    )
cols = ['L', 'ID', 'GM', 'GM_ID', 'GM_GDS', 'GM_CGG']
caption = f"Design points with gm/Id = {gmid_spec}"
display(pretty_table(
    df=lookup_df,
    cols=cols,
    caption=caption,
))
Design points with gm/Id = 15
L ID GM GM_ID A_v0 f_t
0.180 56.78u 850.55u 15 37 84.28G
0.200 50.57u 756.06u 15 42 70.06G
0.220 45.56u 680.21u 15 47 59.20G
0.240 41.46u 617.96u 15 53 50.71G
0.260 38.02u 565.93u 15 58 43.93G
0.280 35.11u 521.77u 15 63 38.43G
0.300 32.60u 483.80u 15 68 33.91G
0.320 30.41u 450.79u 15 72 30.13G
0.340 28.46u 421.81u 15 76 26.95G
0.360 26.72u 396.16u 15 79 24.24G
0.380 25.15u 373.28u 15 83 21.91G
0.400 23.71u 352.75u 15 85 19.90G
0.420 22.40u 334.21u 15 88 18.15G
0.440 21.19u 317.40u 15 90 16.62G
0.460 20.22u 302.50u 15 92 15.29G
0.480 19.38u 289.00u 15 94 14.11G
0.500 18.59u 276.61u 15 96 13.07G
0.600 15.32u 227.18u 15 103 9.24G
0.700 12.84u 192.07u 15 109 6.86G
0.800 11.20u 166.76u 15 115 5.31G
0.900 9.93u 147.30u 15 120 4.23G
1.000 8.89u 131.78u 15 125 3.45G
1.100 8.02u 119.13u 15 131 2.86G
1.200 7.28u 108.63u 15 136 2.41G
1.300 6.66u 99.79u 15 142 2.06G
1.400 6.18u 92.46u 15 147 1.78G
1.500 5.78u 86.13u 15 153 1.56G
1.600 5.42u 80.61u 15 159 1.38G
1.700 5.10u 75.74u 15 165 1.22G
1.800 4.81u 71.42u 15 171 1.09G
1.900 4.55u 67.56u 15 177 0.98G
2.000 4.32u 64.09u 15 183 0.89G

The table above shows us entries from our device data where \(g_m / I_d\) is 15.

For each entry in this table, I can scale appropriately to get the device’s \(g_m\) at the spec’d value of 0.623 mMohs, and from that get the width and \(I_d\) needed for each value of \(L\).

Hide code cell source
scale_factor = gm_spec / lookup_df['GM']
scaled_df = scale(
    df=lookup_df,
    scale_factor=scale_factor
)
cols = ['W', 'L', 'ID', 'GM', 'GM_ID', 'GM_GDS', 'GM_CGG']
caption = "Design points after scaling / denormalization"
display(pretty_table(
    df=scaled_df,
    cols=cols,
    caption=caption,
))
Design points after scaling / denormalization
W L ID GM GM_ID A_v0 f_t
3.693620 0.180 41.95u 628.32u 15 37 84.28G
4.155230 0.200 42.03u 628.32u 15 42 70.06G
4.618548 0.220 42.09u 628.32u 15 47 59.20G
5.083778 0.240 42.15u 628.32u 15 53 50.71G
5.551170 0.260 42.22u 628.32u 15 58 43.93G
6.020993 0.280 42.28u 628.32u 15 63 38.43G
6.493528 0.300 42.34u 628.32u 15 68 33.91G
6.969062 0.320 42.38u 628.32u 15 72 30.13G
7.447862 0.340 42.39u 628.32u 15 76 26.95G
7.930173 0.360 42.38u 628.32u 15 79 24.24G
8.416200 0.380 42.33u 628.32u 15 83 21.91G
8.906103 0.400 42.24u 628.32u 15 85 19.90G
9.400001 0.420 42.11u 628.32u 15 88 18.15G
9.897979 0.440 41.94u 628.32u 15 90 16.62G
10.385510 0.460 42.00u 628.32u 15 92 15.29G
10.870413 0.480 42.13u 628.32u 15 94 14.11G
11.357568 0.500 42.23u 628.32u 15 96 13.07G
13.828370 0.600 42.37u 628.32u 15 103 9.24G
16.356338 0.700 42.01u 628.32u 15 109 6.86G
18.839113 0.800 42.19u 628.32u 15 115 5.31G
21.327329 0.900 42.36u 628.32u 15 120 4.23G
23.839500 1.000 42.37u 628.32u 15 125 3.45G
26.371473 1.100 42.28u 628.32u 15 131 2.86G
28.919608 1.200 42.12u 628.32u 15 136 2.41G
31.480883 1.300 41.91u 628.32u 15 142 2.06G
33.979349 1.400 42.02u 628.32u 15 147 1.78G
36.474470 1.500 42.13u 628.32u 15 153 1.56G
38.975004 1.600 42.21u 628.32u 15 159 1.38G
41.480043 1.700 42.27u 628.32u 15 165 1.22G
43.988826 1.800 42.32u 628.32u 15 171 1.09G
46.500748 1.900 42.35u 628.32u 15 177 0.98G
49.015334 2.000 42.36u 628.32u 15 183 0.89G

From the table above, we can see that:

  • intrinsic gain ranges from 37 to 183,

  • transit frequency ranges from 84.94 GHz -> 1.45 GHz

If we follow our guideline that \(f_t\) should always be greater than 10x \(f_u\), we should limit the possible lengths to \(f_t\) greater than 1 GHz (we wanted \(f_u\) >= 100 MHz):

Hide code cell source
min_ft = f_u * 10
ft_mask = (scaled_df['GM_CGG'] > 1e9)
soln_df = scaled_df[ft_mask]
print(f"The maximum gate length that achieves f_u greater than 100 MHz is {soln_df['L'].max()} um.")
The maximum gate length that achieves f_u greater than 100 MHz is 1.8 um.

Plotting \(f_t\), \(A_{vo}\) and \(W\) vs. \(L\) gives us:

Hide code cell source
metrics_plot = bh.create_bokeh_plot(
    title="IGS metrics vs. device length",
    x_axis_label="L (um)",
    y_axis_type='log',
    y_axis_label='Transit Frequency',
    width=800,
)

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

metrics_plot.y_range = Range1d(1e9, 1e11)

data = ColumnDataSource(soln_df)

metrics_plot.line(
    x='L', y='GM_GDS', source=data,
    legend_label='A_v0', line_color='red',
    y_range_name='gain'
)

metrics_plot.line(
    x='L', y='GM_CGG', source=data,
    legend_label='f_t', line_color='blue'
)

metrics_plot.line(
    x='L', y='W', source=data,
    legend_label='W', line_color='green',
    y_range_name='gain'
)

metrics_plot.legend.location='top_left'
show(metrics_plot)

One thing that jumps out is that up to \(L\) ~ 0.5um, you get good bang for you buck in terms of gain vs. area

In the book, the intrinsic gain flattens more than it does here, and so the authors conclude that there’s not much point in increasing \(L\) past the \(L_{max}\) they find based on \(C_L\) and \(FO=10\).

But that’s not so obvious here; it looks like gain still increases, though at a lower rate, for longer channel lengths without much saturation.