Example 3.3: Intrinsic Gain Stage, with constant f_t

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
output_notebook(hide_banner=True)

Example 3.3: Intrinsic Gain Stage, with constant \(f_t\)#

Consider an IGS, similar to example 3.1 & 3.2, with:

  • \(C_L\) = 1 pF

  • \(f_u\) = 1 GHz

Find the combination of L and \(g_m\over{I_d}\) that achieve either:

  1. Max low-frequency gain

  2. minimum current consumption

Assume:

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

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

  • \(FO\) = 10

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 1 GHz and \(C_L\) is 1 pF, that means:

Hide code cell source
f_u = 1e9
c_l = 1e-12
fo = 10
f_t = f_u * fo
gm_spec = f_u * c_l * 2 * 3.14159
print(f"The required gm is {gm_spec*1e3:.3f} mMohs")
The required gm is 6.283 mMohs

Additionally, if \(f_u\) is 1 GHz and \(FO\) is 10, that tells us we want:

\(f_t\) = 10 GHz

Next we’ll load up our data and filter by our \(V_{ds}\) and \(V_{sb}\) assumptions:

Hide code cell source
# load up device data
nch_data_df = load_mat_data("../../Book-on-gm-ID-design-main/starter_kit/180nch.mat")

# filter by our assumptions. Also filtering out very low
# values of gm/id, because things get weird for low values.
biasing_mask = (
    (nch_data_df['VDS'] == 0.6) &
    (nch_data_df['VSB'] == 0.0) &
    (nch_data_df['GM_ID'] > 2.5)
    )

filtered_df = nch_data_df[biasing_mask]
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 the data loaded up, we’ll plot \(A_{v0}\) and \(f_t\) vs. \(\frac{g_m}{I_d}\), and find points that intersect with a horizontal line at \(f_t = 10e9\):

Hide code cell source
# create a color map; use gate length as the color modulator
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)

# when we hover over plots, show this info
TOOLTIPS = [
            ("x", "$x"),
            ("y", "$y"),
            ("L", "@L")
        ]

# create a figure
ft_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
ft_plot.extra_y_ranges['gain'] = Range1d(0, 200)
ft_plot.extra_y_scales['gain'] = LinearScale()

for len, len_group in filtered_df.groupby('L'):
    # display(len_group)
    data = ColumnDataSource(len_group)
    
    ft_plot.line(x='GM_ID', y='GM_CGG', source=data, 
                       legend_label=f"l={len}", line_color=length_color_dict[len],
                       line_dash="dashed")
    
    ft_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')
    
ft_plot.y_range = Range1d(1e8, 100e9)

ax2 = LinearAxis(y_range_name='gain', axis_label="Intrinsic Gain",)
ft_plot.add_layout(ax2, 'right')

# add vertical line for gmId = 15
ft_plot.add_layout(Span(location=10e9, dimension='width'))

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

show(ft_plot)

Great! Each point where the dashed lines intersect with the horizontal line represent a combo of \(L\) and \(\frac{g_m}{I_d}\) where \(f_t\) is 10 GHz.

Looking at it in table form, we get:

Hide code cell source
# use our interpolation function to get data with f_t of exactly 10 GHz
lookup_df, interp_df = lookup(df=filtered_df, param='GM_CGG', target=f_t)

# display the same data as the plots, but in table form
# display(
#     lookup_df[['L', 'ID', 'GM', 'GM_ID', 'GM_GDS', 'GM_CGG']]
#     .rename(columns={'GM_GDS':'A_v0', 'GM_CGG':'f_t'})
#     .style
#     .format(col_formats)
#     .set_caption("Design points that satisfy f_t = 10 GHz")
#     .hide()
#     )

caption = f"Design points that satisfy f_t = {f_t/1e6} MHz"
show_cols = ['L', 'ID', 'GM', 'GM_ID', 'GM_GDS', 'GM_CGG']
display(pretty_table(
    df=lookup_df,
    cols=show_cols,
    caption=caption
))
The target of 1.00e+10 for GM_CGG is outside                  the existing data for length 1.4; skipping
The target of 1.00e+10 for GM_CGG is outside                  the existing data for length 1.5; skipping
The target of 1.00e+10 for GM_CGG is outside                  the existing data for length 1.6; skipping
The target of 1.00e+10 for GM_CGG is outside                  the existing data for length 1.7; skipping
The target of 1.00e+10 for GM_CGG is outside                  the existing data for length 1.8; skipping
The target of 1.00e+10 for GM_CGG is outside                  the existing data for length 1.9; skipping
The target of 1.00e+10 for GM_CGG is outside                  the existing data for length 2.0; skipping
Design points that satisfy f_t = 10000.0 MHz
L ID GM GM_ID A_v0 f_t
0.180 3.30u 76.39u 23 37 10.00G
0.200 3.63u 83.20u 23 42 10.00G
0.220 3.96u 90.25u 23 47 10.00G
0.240 4.36u 98.17u 23 52 10.00G
0.260 4.81u 106.73u 22 58 10.00G
0.280 5.27u 115.29u 22 63 10.00G
0.300 5.72u 123.85u 22 68 10.00G
0.320 6.25u 132.63u 21 73 10.00G
0.340 6.82u 141.47u 21 77 10.00G
0.360 7.40u 150.19u 20 81 10.00G
0.380 7.99u 158.80u 20 85 10.00G
0.400 8.59u 167.32u 20 88 10.00G
0.420 9.23u 175.73u 19 90 10.00G
0.440 9.99u 183.99u 19 92 10.00G
0.460 10.75u 192.16u 18 94 10.00G
0.480 11.54u 200.24u 18 96 10.00G
0.500 12.34u 208.23u 17 98 10.00G
0.600 17.36u 246.75u 14 103 10.00G
0.700 24.40u 283.55u 12 104 10.00G
0.800 35.28u 319.12u 9 103 10.00G
0.900 51.14u 354.05u 7 97 10.00G
1.000 72.88u 388.69u 5 87 10.00G
1.100 99.63u 423.32u 4 72 10.00G
1.200 131.97u 458.10u 3 54 10.00G
1.300 174.20u 493.36u 3 32 10.00G

The table above shows us entries from our device data where \(f_t\) is 10 GHz to satisfy our condition that \(f_u\) is 1 GHz and \(FO\) is 10.

Note that \(L\) = 1.3um is the maximum length that can satisfy this condition; other lengths aren’t shown.

We can plot this to see what gain and \(\frac{g_m}{I_d}\) looks like across \(L\):

Hide code cell source
compare_plot = bh.create_bokeh_plot(
    title="IGS metrics vs. device length for design points with f_t = 10 GHz",
    x_axis_label="L (um)",
    width=800,
)

compare_plot.y_range = Range1d(0, 200)

data = ColumnDataSource(lookup_df)

compare_plot.line(
    x='L', y='GM_GDS', source=data,
    legend_label='A_v0', line_color='red'
)
compare_plot.scatter(
    x='L', y='GM_GDS', source=data,
    color='red'
)

compare_plot.line(
    x='L', y='GM_ID', source=data,
    legend_label='gm/Id', line_color='blue'
)
compare_plot.scatter(
    x='L', y='GM_ID', source=data,
    color='blue'
)

compare_plot.legend.location='top_right'
show(compare_plot)

From this we can see that we’ll get the most intrinsic gain with a length of 0.7um.

If we want minimum current to satisfy our \(f_u\) spec, we should use a length of 0.180um. Because that length has the highest \(\frac{g_m}{I_d}\), we’d need to use the least current to get the \(g_m\) needed to hit \(f_u\).

If we use our sizing methodology for these two design points to size the transitors, we’d get:

Hide code cell source
scale_factor = gm_spec / lookup_df['GM']
scaled_df = scale(
    df=lookup_df,
    scale_factor=scale_factor,
)

# we only want to look at two lengths; 0.180u and 0.6u
len_mask = scaled_df['L'].isin([0.180, 0.7])
soln_df = scaled_df[len_mask]
# recompute GM_GDS (intrinsic gain) and GM_CGG (transit frequency) to make
# sure we don't miss anything
# scaled_df['GM_GDS'] = scaled_df['GM'] / scaled_df['GDS']
# scaled_df['GM_CGG'] = scaled_df['GM'] / scaled_df['CGG']

display(soln_df[['W', 'L', 'ID', 'GM', 'GM_ID', 'GM_GDS', 'GM_CGG']])
W L ID GM GM_ID GM_GDS GM_CGG
19 411.25 180.00m 271.29u 6.28m 23.23 37.26 10.00G
25 110.79 700.00m 540.65u 6.28m 11.62 104.45 10.00G

If we want to compare these two options, we could plot \(A_{v0}\) and \(W\) vs. \(I_d\):

The dashed line annotates the min current solution, and the solid line annotates the max gain solution

Hide code cell source
compare_plot = bh.create_bokeh_plot(
    title="IGS metrics vs. drain current",
    x_axis_label="Id (A)",
    width=800,
)

compare_plot.y_range = Range1d(0, 200)

data = ColumnDataSource(scaled_df)

compare_plot.line(
    x='ID', y='GM_GDS', source=data,
    legend_label='A_v0', line_color='red'
)
compare_plot.scatter(
    x='ID', y='GM_GDS', source=data,
    color='red'
)

compare_plot.line(
    x='ID', y='W', source=data,
    legend_label='Device Width', line_color='blue'
)
compare_plot.scatter(
    x='ID', y='W', source=data,
    color='blue'
)

compare_plot.legend.location='top_right'

# add some annotations to show the two design points we found earlier
id_0p180 = soln_df.set_index('L').loc[0.180, 'ID']
id_0p6 = soln_df.set_index('L').loc[0.7, 'ID']
compare_plot.add_layout(Span(dimension='height', location=id_0p180, name='Min Current', line_dash='solid'))
compare_plot.add_layout(Span(dimension='height', location=id_0p6, name='Max Gain', line_dash='dashed'))

show(compare_plot)

The book notes that it wouldn’t take much more current to dramatically increase the gain and reduce the device area, so we might want to avoid the min current solution.

But if we weren’t too worried about area, we could some large power savings by scaling back just a bit from the maximum gain solution; somewhere around 380 uA gives us a gain of ~98 (compared to 104) and saves ~120 uA of current, or roughly 20%.