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
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:
Max low-frequency gain
minimum current consumption
Assume:
\(V_{ds}\) = 0.6V
\(V_{sb}\) = 0.0V
\(FO\) = 10
Circuit analysis#
As before, we can define \(f_u\) as:
So if we need \(f_u\) to be 1 GHz and \(C_L\) is 1 pF, that means:
Show 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:
Show 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\):
Show 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:
Show 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
| 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\):
Show 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:
Show 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
Show 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%.