Show 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:
Max unity gain frequency
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\):
Show 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:
Show 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
Show 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
)
)
| 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\):
Show 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:
Show 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:
Show 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:
Show 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.