Example 3.4: Intrinsic Gain Stage, with constant A_{v0}

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.4: Intrinsic Gain Stage, with constant \(A_{v0}\)#

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

  • \(C_L\) = 1 pF

  • \(|A_{v0}|\) = 50

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

  1. Max unity gain frequency

  2. minimum current consumption for a design that hits 80% of the max UGF

Assume:

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

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

  • \(FO\) = 10

Circuit analysis#

The flow here should look a lot like example 3.3; we’ll start by plotting \(A_{v0}\) and \(f_t\) against \(\frac{g_m}{I_d}\):

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 \(A_{v0} = 50\):

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
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()

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)
    
    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=50, dimension='width', y_range_name='gain'))

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

show(ft_plot)

Great! Each point where the solid lines intersect with the horizontal line represent a combo of \(L\) and \(\frac{g_m}{I_d}\) where \(A_{v0}\) is 50.

Looking at it in table form, we get:

Hide code cell source
# let's grab nch data that gives us f_t of 10 GHz,
# with V_ds=0.6 and V_sb=0.0

biasing_mask = (
    (nch_data_df['VDS'] == 0.6) &
    (nch_data_df['VSB'] == 0.0) &
    (nch_data_df['GM_ID'] > 2.5)
    )
# lookup_df = nch_data_df[lookup_mask].copy()
# 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='GM_GDS', target=50)
The target of 5.00e+01 for GM_GDS is outside                  the existing data for length 0.18; skipping
The target of 5.00e+01 for GM_GDS is outside                  the existing data for length 0.2; skipping
The target of 5.00e+01 for GM_GDS is outside                  the existing data for length 0.22; skipping
Hide code cell source
# display the same data as the plots, but in table form
cols = ['L', 'ID', 'GM', 'GM_ID', 'GM_GDS', 'GM_CGG', 'VGS']
caption = f"Design points with intrinsic gain of 50"
display(
    pretty_table(
        df=lookup_df,
        cols=cols,
        caption=caption
    )
)
Design points with intrinsic gain of 50
L ID GM GM_ID A_v0 f_t VGS
0.240 36.85u 417.16u 19 50 34.26G 509.37m
0.260 115.05u 1,033.32u 9 50 79.04G 689.16m
0.280 144.44u 1,085.45u 8 50 78.73G 725.31m
0.300 166.12u 1,094.42u 7 50 75.52G 753.83m
0.320 181.41u 1,085.29u 6 50 71.42G 776.88m
0.340 192.15u 1,066.92u 6 50 67.11G 795.82m
0.360 199.40u 1,044.08u 5 50 62.90G 811.55m
0.380 203.65u 1,019.16u 5 50 58.91G 824.58m
0.400 206.48u 993.43u 5 50 55.19G 835.85m
0.420 207.57u 967.74u 5 50 51.75G 845.41m
0.440 207.72u 942.57u 5 50 48.58G 853.76m
0.460 207.27u 918.18u 4 50 45.68G 861.19m
0.480 206.14u 894.69u 4 50 43.01G 867.75m
0.500 204.55u 872.17u 4 50 40.57G 873.62m
0.600 194.64u 774.25u 4 50 30.99G 896.94m
0.700 183.82u 696.82u 4 50 24.48G 914.15m
0.800 173.39u 634.36u 4 50 19.86G 927.87m
0.900 163.92u 582.78u 4 50 16.45G 939.40m
1.000 155.10u 539.31u 3 50 13.87G 949.10m
1.100 147.21u 502.09u 3 50 11.85G 957.58m
1.200 139.93u 469.79u 3 50 10.25G 964.90m
1.300 133.21u 441.45u 3 50 8.96G 971.27m
1.400 127.06u 416.37u 3 50 7.90G 976.90m
1.500 121.47u 394.02u 3 50 7.01G 981.98m
1.600 116.29u 373.95u 3 50 6.27G 986.47m
1.700 111.48u 355.83u 3 50 5.64G 990.48m
1.800 107.01u 339.37u 3 50 5.10G 994.05m
1.900 102.86u 324.37u 3 50 4.63G 997.26m
2.000 98.99u 310.62u 3 50 4.23G 1,000.15m

The table above shows us entries from our device data where \(A_{v0}\) is 50, with the other measurements interpolated.

Note

There’s a bug here: something funny is happening with the interpolation. From the plot, the design point for L=0.240um should have a gmId around 12 for an intrinsic gain of 50, but we’re getting 19. I sorta/kinda think this is related to the fact that gmId isn’t monotonic here, and my lookup function isn’t handing that properly, but I’m not 100%. I’ll add this an issue, and fix once I have an initial build of the Book.

We can plot this to see what \(f_t\) 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",
    x_axis_label="L (um)",
    width=800,
)

compare_plot.y_range = Range1d(0, 100)

data = ColumnDataSource(lookup_df)
data.data['GM_CGG'] = data.data['GM_CGG'] / 1e9

compare_plot.line(
    x='L', y='GM_CGG', source=data,
    legend_label='f_t', line_color='red'
)
compare_plot.scatter(
    x='L', y='GM_CGG', 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, the design solution with max \(f_t\) is at \(L = 0.280um\).

But we can also see that the device is in pretty strong inversion; \(\frac{g_m}{I_d}\) is only ~9.

We can get another look at this by flipping the plots around a bit:


Hide code cell source
ft_plot = bh.create_bokeh_plot(
    title="F_t vs. gm/Id",
    x_axis_label="gm/Id (mOhs)",
    width=800,
)

# compare_plot.y_range = Range1d(0, 100)

data = ColumnDataSource(lookup_df)
# data.data['GM_CGG'] = data.data['GM_CGG'] / 1e9

ft_plot.line(
    x='GM_ID', y='GM_CGG', source=data,
    legend_label='f_t', line_color='red'
)
ft_plot.scatter(
    x='GM_ID', y='GM_CGG', source=data,
    color='red'
)

len_plot = bh.create_bokeh_plot(
    title="gate length vs. gm/Id",
    x_axis_label="gm/Id (mOhs)",
    width=800,
)

# compare_plot.y_range = Range1d(0, 100)

len_plot.line(
    x='GM_ID', y='L', source=data,
    legend_label='gate length', line_color='red'
)
len_plot.scatter(
    x='GM_ID', y='L', source=data,
    color='red'
)

# compare_plot.legend.location='top_right'
show(layout(ft_plot, len_plot))

At the design point with max \(f_u\), we find:

Hide code cell source
max_fu_idx = lookup_df['GM_CGG'].idxmax()
max_ft = lookup_df.loc[max_fu_idx,'GM_CGG']
max_fu = max_ft / 10
print(f"The maximum f_u is {max_fu / 1e9:.2f} GHz")

max_fu_len = lookup_df.loc[max_fu_idx, 'L']
print(f"The gate length for max f_u is {max_fu_len*1e3:.0f} nm")
The maximum f_u is 7.90 GHz
The gate length for max f_u is 260 nm

Given this, our solutions and the associated sizings look like:


Hide code cell source
c_l = 1e-12
lookup_df['F_U'] = lookup_df['GM_CGG'] / 10
lookup_df['GM_SPEC'] = lookup_df['F_U'] * c_l * np.pi
lookup_df['scale_factor'] = lookup_df['GM_SPEC'] / lookup_df['GM']
scaled_df = scale(
    df=lookup_df,
    scale_factor=lookup_df['scale_factor'],
)

soln_mask = scaled_df['F_U'] > 0.8 * max_fu
soln_df = scaled_df[soln_mask]
display(
    soln_df[['W', 'L', 'ID', 'GM', 'GM_SPEC', 'scale_factor', 'GM_ID', 'GM_GDS', 'GM_CGG', 'F_U']]
)
W L ID GM GM_SPEC scale_factor GM_ID GM_GDS GM_CGG F_U
17 120.16 260.00m 2.76m 24.83m 24.83m 24.03 9.05 50.00 79.04G 7.90G
15 113.94 280.00m 3.29m 24.73m 24.73m 22.79 7.52 50.00 78.73G 7.87G
14 108.39 300.00m 3.60m 23.72m 23.72m 21.68 6.60 50.00 75.52G 7.55G
13 103.37 320.00m 3.75m 22.44m 22.44m 20.67 5.99 50.00 71.42G 7.14G
13 98.80 340.00m 3.80m 21.08m 21.08m 19.76 5.56 50.00 67.11G 6.71G

Since we’re looking for the solution with minimum drain current (for 80% of max \(f_u\)), we’d select \(L\) = 260nm.

Note

Again, I think this is a bug; there should be some data points to the left of the max f_u (i.e., points with higher gmId and thus lower req’d drain current) that are within 80% of max f_u.