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.7: Iterative sizing to account for self-loading#
Thus far, we’ve ignored the effect of extrinsic caps (i.e., caps that aren’t needed for device operation, like $C_{db}, C_{sb}, C_{gd}). But in reality, these caps will effect our device’s transit frequency, and the IGS’ unity gain bandwidth.
For low-frequency designs, it might not matter much, but it’ll play a major role for higher-speed designs.
For what we’re doing, we’ll be concerned with \(C_{db}\) and \(C_{gd}\) (\(C_{gs}\) and \(C_{gb}\) are shorted by our ideal input source). And we’ll ignore the feedforward path created by \(C_{gd}\), since that only plays a part at frequencies past the device’s transit frequency.
We’ll call the drain capacitances \(C_{dd}\): $\( C_{dd} = C_{gd} + C_{db} \)$
The tricky bit here is that we don’t know the value of these caps until we’ve sized the device, so we can’t include them in the initial sizing. So, we’ll take a 3 step approach:
Initial sizing without extrinsic caps
Find the value of \(C_{dd}\) (we’ll call this \(C_{dd1}\))
Scale the device width and current by this scaling factor:
We can derive \(S\) by examing how \(\omega_u\) scales with device width and current:
Solving for S, we get the equation from a few lines up.
This works well for circuits were device width (and associated parasitic caps) scale linearly with the device transconductance that sets the UGF. But that’s not always going to be the case; more complicated circuits will require an iterative approach instead:
Start by assuming \(C_{dd}\) is 0
Size the circuit to meet BW specs for \(C + C_{dd}\) (we’re assuming \(C_{dd}=0\))
Estimate \(C_{dd}\) for the obtained design, using device width from step 2
Repeat step two with new \(C_{dd}\) estimate
Repeat until convergence, i.e. until resulting gain, BW, etc. settle to some value
So, let’s repeat example 3.3 using this approach:
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 achieves minimum current consumption.
Assume:
\(V_{ds}\) = 0.6V
\(V_{sb}\) = 0.0V
\(FO\) = 10
Solution#
As before, let’s find design points that satisfy our \(f_u\) spec given \(C_L\) = 1pF:
Show code cell source
f_u = 1e9
c_l = 1e-12
gm_spec = f_u * 2 * 3.14159 * c_l
print(f"The required gm is: {gm_spec*1e3:0.2f} mS")
The required gm is: 6.28 mS
Additionally, if \(f_u\) is 1 GHz and \(FO\) is 10, then \(f_t\) is 10 GHz.
So, we can:
Lookup data points with \(f_t >= 10 GHz\), then
scale those data points to find combinations with \(g_m\) equal to our spec, and pick the one with minimum current:
Show code cell source
f_t = 10e9
# 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]
lookup_df, interp_df = lookup(df=filtered_df, param='GM_CGG', target=f_t)
lookup_df = lookup_df.reset_index(drop=True)
caption = f"Design points that satisfy f_t = {f_t/1e6} MHz"
show_cols = ['L', 'W', 'VGS', 'ID', 'GM', 'GM_ID', 'GM_GDS', 'GM_CGG', 'CDD']
display(pretty_table(
df=lookup_df,
cols=show_cols,
caption=caption
))
scale_factors = gm_spec / lookup_df['GM']
# display(lookup_df)
# display(scale_factors)
scaled_df = scale(df=lookup_df, scale_factor=scale_factors)
caption = f"Design points that satisfy f_t = {f_t/1e6} MHz, scaled to give gm={gm_spec*1e3:0.2f} mS"
display(pretty_table(
df=scaled_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 | W | VGS | ID | GM | GM_ID | A_v0 | f_t | CDD |
|---|---|---|---|---|---|---|---|---|
| 0.180 | 5.000000 | 454.69m | 3.30u | 76.39u | 23 | 37 | 10.00G | 6.30f |
| 0.200 | 5.000000 | 462.33m | 3.63u | 83.20u | 23 | 42 | 10.00G | 6.30f |
| 0.220 | 5.000000 | 470.80m | 3.96u | 90.25u | 23 | 47 | 10.00G | 6.30f |
| 0.240 | 5.000000 | 478.27m | 4.36u | 98.17u | 23 | 52 | 10.00G | 6.30f |
| 0.260 | 5.000000 | 484.60m | 4.81u | 106.73u | 22 | 58 | 10.00G | 6.30f |
| 0.280 | 5.000000 | 490.99m | 5.27u | 115.29u | 22 | 63 | 10.00G | 6.30f |
| 0.300 | 5.000000 | 497.37m | 5.72u | 123.85u | 22 | 68 | 10.00G | 6.30f |
| 0.320 | 5.000000 | 502.70m | 6.25u | 132.63u | 21 | 73 | 10.00G | 6.30f |
| 0.340 | 5.000000 | 507.31m | 6.82u | 141.47u | 21 | 77 | 10.00G | 6.30f |
| 0.360 | 5.000000 | 511.95m | 7.40u | 150.19u | 20 | 81 | 10.00G | 6.30f |
| 0.380 | 5.000000 | 516.64m | 7.99u | 158.80u | 20 | 85 | 10.00G | 6.30f |
| 0.400 | 5.000000 | 521.39m | 8.59u | 167.32u | 20 | 88 | 10.00G | 6.30f |
| 0.420 | 5.000000 | 526.00m | 9.23u | 175.73u | 19 | 90 | 10.00G | 6.30f |
| 0.440 | 5.000000 | 530.06m | 9.99u | 183.99u | 19 | 92 | 10.00G | 6.30f |
| 0.460 | 5.000000 | 534.29m | 10.75u | 192.16u | 18 | 94 | 10.00G | 6.30f |
| 0.480 | 5.000000 | 538.70m | 11.54u | 200.24u | 18 | 96 | 10.00G | 6.30f |
| 0.500 | 5.000000 | 543.31m | 12.34u | 208.23u | 17 | 98 | 10.00G | 6.30f |
| 0.600 | 5.000000 | 568.42m | 17.36u | 246.75u | 14 | 103 | 10.00G | 6.31f |
| 0.700 | 5.000000 | 600.15m | 24.40u | 283.55u | 12 | 104 | 10.00G | 6.32f |
| 0.800 | 5.000000 | 643.04m | 35.28u | 319.12u | 9 | 103 | 10.00G | 6.34f |
| 0.900 | 5.000000 | 700.47m | 51.14u | 354.05u | 7 | 97 | 10.00G | 6.37f |
| 1.000 | 5.000000 | 772.38m | 72.88u | 388.69u | 5 | 87 | 10.00G | 6.42f |
| 1.100 | 5.000000 | 854.89m | 99.63u | 423.32u | 4 | 72 | 10.00G | 6.53f |
| 1.200 | 5.000000 | 947.82m | 131.97u | 458.10u | 3 | 54 | 10.00G | 6.73f |
| 1.300 | 5.000000 | 1,058.81m | 174.20u | 493.36u | 3 | 32 | 10.00G | 7.20f |
| L | W | VGS | ID | GM | GM_ID | A_v0 | f_t | CDD |
|---|---|---|---|---|---|---|---|---|
| 0.180 | 411.254728 | 454.69m | 271.29u | 6,283.18u | 23 | 37 | 10.00G | 517.94f |
| 0.200 | 377.575952 | 462.33m | 273.86u | 6,283.18u | 23 | 42 | 10.00G | 475.53f |
| 0.220 | 348.083584 | 470.80m | 275.81u | 6,283.18u | 23 | 47 | 10.00G | 438.40f |
| 0.240 | 320.029146 | 478.27m | 279.10u | 6,283.18u | 23 | 52 | 10.00G | 403.08f |
| 0.260 | 294.351016 | 484.60m | 283.28u | 6,283.18u | 22 | 58 | 10.00G | 370.75f |
| 0.280 | 272.496703 | 490.99m | 286.96u | 6,283.18u | 22 | 63 | 10.00G | 343.24f |
| 0.300 | 253.658337 | 497.37m | 290.39u | 6,283.18u | 22 | 68 | 10.00G | 319.53f |
| 0.320 | 236.862237 | 502.70m | 296.09u | 6,283.18u | 21 | 73 | 10.00G | 298.39f |
| 0.340 | 222.067833 | 507.31m | 303.05u | 6,283.18u | 21 | 77 | 10.00G | 279.77f |
| 0.360 | 209.173583 | 511.95m | 309.71u | 6,283.18u | 20 | 81 | 10.00G | 263.55f |
| 0.380 | 197.827028 | 516.64m | 316.19u | 6,283.18u | 20 | 85 | 10.00G | 249.27f |
| 0.400 | 187.759733 | 521.39m | 322.58u | 6,283.18u | 20 | 88 | 10.00G | 236.61f |
| 0.420 | 178.776067 | 526.00m | 330.11u | 6,283.18u | 19 | 90 | 10.00G | 225.31f |
| 0.440 | 170.743339 | 530.06m | 341.00u | 6,283.18u | 19 | 92 | 10.00G | 215.21f |
| 0.460 | 163.487305 | 534.29m | 351.62u | 6,283.18u | 18 | 94 | 10.00G | 206.09f |
| 0.480 | 156.894660 | 538.70m | 362.02u | 6,283.18u | 18 | 96 | 10.00G | 197.81f |
| 0.500 | 150.873484 | 543.31m | 372.23u | 6,283.18u | 17 | 98 | 10.00G | 190.25f |
| 0.600 | 127.320088 | 568.42m | 442.11u | 6,283.18u | 14 | 103 | 10.00G | 160.70f |
| 0.700 | 110.794612 | 600.15m | 540.65u | 6,283.18u | 12 | 104 | 10.00G | 140.06f |
| 0.800 | 98.444169 | 643.04m | 694.60u | 6,283.18u | 9 | 103 | 10.00G | 124.78f |
| 0.900 | 88.733698 | 700.47m | 907.65u | 6,283.18u | 7 | 97 | 10.00G | 113.00f |
| 1.000 | 80.824454 | 772.38m | 1,178.15u | 6,283.18u | 5 | 87 | 10.00G | 103.84f |
| 1.100 | 74.212692 | 854.89m | 1,478.78u | 6,283.18u | 4 | 72 | 10.00G | 96.89f |
| 1.200 | 68.578667 | 947.82m | 1,810.04u | 6,283.18u | 3 | 54 | 10.00G | 92.34f |
| 1.300 | 63.676820 | 1,058.81m | 2,218.46u | 6,283.18u | 3 | 32 | 10.00G | 91.67f |
The minimum current solution is in the first row, with \(I_d = 271.29uA\) and \(L = 180 nm\).
Let’s see what happens if we include that design points’ \(C_{dd}\):
Show code cell source
f_u_self = gm_spec / (2 * 3.14159 * (c_l + scaled_df.loc[0, 'CDD']))
print(f"Including self loading, the unity gain bandwidth drops to {f_u_self/1e9:0.3f} GHz")
Including self loading, the unity gain bandwidth drops to 0.659 GHz
Quite a drop in UGBW! To get that back, we’ll have to scale up the device’s width and drain current.
We’d probably write a loop to do this a few times, but first let’s just walk through the initial iteration:
Show code cell source
gm_spec_mk2 = f_u * (2 * 3.14159 * (c_l + scaled_df.loc[0, 'CDD']))
print(f"Including self loading, the required gm becomes: {gm_spec_mk2*1e3:0.3f} mS")
Including self loading, the required gm becomes: 9.537 mS
Show code cell source
# scale up the design points that we previously found with
# our updated gm spec
scale_factors_mk2 = gm_spec_mk2 / lookup_df['GM']
# display(lookup_df)
# display(scale_factors)
scaled_mk2_df = scale(df=lookup_df, scale_factor=scale_factors_mk2)
caption = f"Design points that satisfy f_t = {f_t/1e6} MHz, scaled to give gm={gm_spec_mk2*1e3:0.2f} mS"
display(pretty_table(
df=scaled_mk2_df,
cols=show_cols,
caption=caption
))
| L | W | VGS | ID | GM | GM_ID | A_v0 | f_t | CDD |
|---|---|---|---|---|---|---|---|---|
| 0.180 | 624.260438 | 454.69m | 411.80u | 9,537.50u | 23 | 37 | 10.00G | 786.20f |
| 0.200 | 573.138041 | 462.33m | 415.71u | 9,537.50u | 23 | 42 | 10.00G | 721.83f |
| 0.220 | 528.370365 | 470.80m | 418.66u | 9,537.50u | 23 | 47 | 10.00G | 665.47f |
| 0.240 | 485.785381 | 478.27m | 423.66u | 9,537.50u | 23 | 52 | 10.00G | 611.85f |
| 0.260 | 446.807492 | 484.60m | 430.00u | 9,537.50u | 22 | 58 | 10.00G | 562.78f |
| 0.280 | 413.633933 | 490.99m | 435.59u | 9,537.50u | 22 | 63 | 10.00G | 521.02f |
| 0.300 | 385.038405 | 497.37m | 440.80u | 9,537.50u | 22 | 68 | 10.00G | 485.03f |
| 0.320 | 359.542915 | 502.70m | 449.44u | 9,537.50u | 21 | 73 | 10.00G | 452.94f |
| 0.340 | 337.085882 | 507.31m | 460.01u | 9,537.50u | 21 | 77 | 10.00G | 424.68f |
| 0.360 | 317.513170 | 511.95m | 470.12u | 9,537.50u | 20 | 81 | 10.00G | 400.05f |
| 0.380 | 300.289769 | 516.64m | 479.96u | 9,537.50u | 20 | 85 | 10.00G | 378.38f |
| 0.400 | 285.008207 | 521.39m | 489.66u | 9,537.50u | 20 | 88 | 10.00G | 359.16f |
| 0.420 | 271.371532 | 526.00m | 501.09u | 9,537.50u | 19 | 90 | 10.00G | 342.01f |
| 0.440 | 259.178324 | 530.06m | 517.62u | 9,537.50u | 19 | 92 | 10.00G | 326.68f |
| 0.460 | 248.164093 | 534.29m | 533.73u | 9,537.50u | 18 | 94 | 10.00G | 312.84f |
| 0.480 | 238.156847 | 538.70m | 549.52u | 9,537.50u | 18 | 96 | 10.00G | 300.26f |
| 0.500 | 229.017056 | 543.31m | 565.02u | 9,537.50u | 17 | 98 | 10.00G | 288.78f |
| 0.600 | 193.264389 | 568.42m | 671.09u | 9,537.50u | 14 | 103 | 10.00G | 243.94f |
| 0.700 | 168.179691 | 600.15m | 820.68u | 9,537.50u | 12 | 104 | 10.00G | 212.61f |
| 0.800 | 149.432447 | 643.04m | 1,054.37u | 9,537.50u | 9 | 103 | 10.00G | 189.41f |
| 0.900 | 134.692523 | 700.47m | 1,377.76u | 9,537.50u | 7 | 97 | 10.00G | 171.53f |
| 1.000 | 122.686757 | 772.38m | 1,788.36u | 9,537.50u | 5 | 87 | 10.00G | 157.62f |
| 1.100 | 112.650492 | 854.89m | 2,244.71u | 9,537.50u | 4 | 72 | 10.00G | 147.08f |
| 1.200 | 104.098375 | 947.82m | 2,747.54u | 9,537.50u | 3 | 54 | 10.00G | 140.16f |
| 1.300 | 96.657659 | 1,058.81m | 3,367.49u | 9,537.50u | 3 | 32 | 10.00G | 139.15f |
Again, the minimum current solution is in the first row, with \(I_d = 411.8uA\) and \(L = 180 nm\).
Let’s run a few iterations of the sizing routine in a loop, and see if we converge on something:
Show code cell source
# iterate 5 times and hopefully converge on a design point
gm_specs = [gm_spec]
num_iters = 10
for i in range(num_iters):
soln_col = scaled_df.loc[0,:]
self_load_cap = soln_col['CDD']
gm_spec = f_u * (2*3.14159*(c_l + self_load_cap))
gm_specs.append(gm_spec)
print(f"The new gm_spec is: {gm_spec*1e3:0.2f}, scaling devices...")
scaling_factor = gm_spec / lookup_df['GM']
scaled_df = scale(df=lookup_df, scale_factor=scaling_factor)
# print(f"The updated solution is:")
# caption = f"Design points that satisfy f_t = {f_t/1e6} MHz, scaled to give gm={gm_spec*1e3:0.2f} mS"
# display(pretty_table(
# df=scaled_df,
# cols=show_cols,
# caption=caption
# ))
print(f"After {num_iters} iterations the solution is:")
caption = f"Design points that satisfy f_t = {f_t/1e6} MHz, scaled to give gm={gm_spec*1e3:0.2f} mS"
display(pretty_table(
df=scaled_df,
cols=show_cols,
caption=caption
))
The new gm_spec is: 9.54, scaling devices...
The new gm_spec is: 11.22, scaling devices...
The new gm_spec is: 12.10, scaling devices...
The new gm_spec is: 12.55, scaling devices...
The new gm_spec is: 12.78, scaling devices...
The new gm_spec is: 12.90, scaling devices...
The new gm_spec is: 12.97, scaling devices...
The new gm_spec is: 13.00, scaling devices...
The new gm_spec is: 13.02, scaling devices...
The new gm_spec is: 13.02, scaling devices...
After 10 iterations the solution is:
| L | W | VGS | ID | GM | GM_ID | A_v0 | f_t | CDD |
|---|---|---|---|---|---|---|---|---|
| 0.180 | 852.507358 | 454.69m | 562.37u | 13,024.67u | 23 | 37 | 10.00G | 1,073.66f |
| 0.200 | 782.693196 | 462.33m | 567.70u | 13,024.67u | 23 | 42 | 10.00G | 985.76f |
| 0.220 | 721.557216 | 470.80m | 571.74u | 13,024.67u | 23 | 47 | 10.00G | 908.78f |
| 0.240 | 663.401982 | 478.27m | 578.57u | 13,024.67u | 23 | 52 | 10.00G | 835.56f |
| 0.260 | 610.172697 | 484.60m | 587.23u | 13,024.67u | 22 | 58 | 10.00G | 768.55f |
| 0.280 | 564.869965 | 490.99m | 594.85u | 13,024.67u | 22 | 63 | 10.00G | 711.52f |
| 0.300 | 525.819119 | 497.37m | 601.97u | 13,024.67u | 22 | 68 | 10.00G | 662.37f |
| 0.320 | 491.001772 | 502.70m | 613.77u | 13,024.67u | 21 | 73 | 10.00G | 618.54f |
| 0.340 | 460.333824 | 507.31m | 628.20u | 13,024.67u | 21 | 77 | 10.00G | 579.95f |
| 0.360 | 433.604786 | 511.95m | 642.00u | 13,024.67u | 20 | 81 | 10.00G | 546.32f |
| 0.380 | 410.084032 | 516.64m | 655.44u | 13,024.67u | 20 | 85 | 10.00G | 516.73f |
| 0.400 | 389.215108 | 521.39m | 668.69u | 13,024.67u | 20 | 88 | 10.00G | 490.48f |
| 0.420 | 370.592487 | 526.00m | 684.31u | 13,024.67u | 19 | 90 | 10.00G | 467.06f |
| 0.440 | 353.941104 | 530.06m | 706.87u | 13,024.67u | 19 | 92 | 10.00G | 446.12f |
| 0.460 | 338.899764 | 534.29m | 728.88u | 13,024.67u | 18 | 94 | 10.00G | 427.22f |
| 0.480 | 325.233591 | 538.70m | 750.44u | 13,024.67u | 18 | 96 | 10.00G | 410.05f |
| 0.500 | 312.752039 | 543.31m | 771.61u | 13,024.67u | 17 | 98 | 10.00G | 394.37f |
| 0.600 | 263.927207 | 568.42m | 916.46u | 13,024.67u | 14 | 103 | 10.00G | 333.13f |
| 0.700 | 229.670848 | 600.15m | 1,120.74u | 13,024.67u | 12 | 104 | 10.00G | 290.34f |
| 0.800 | 204.069092 | 643.04m | 1,439.87u | 13,024.67u | 9 | 103 | 10.00G | 258.66f |
| 0.900 | 183.939843 | 700.47m | 1,881.51u | 13,024.67u | 7 | 97 | 10.00G | 234.25f |
| 1.000 | 167.544436 | 772.38m | 2,442.23u | 13,024.67u | 5 | 87 | 10.00G | 215.25f |
| 1.100 | 153.838634 | 854.89m | 3,065.43u | 13,024.67u | 4 | 72 | 10.00G | 200.85f |
| 1.200 | 142.159627 | 947.82m | 3,752.12u | 13,024.67u | 3 | 54 | 10.00G | 191.41f |
| 1.300 | 131.998378 | 1,058.81m | 4,598.74u | 13,024.67u | 3 | 32 | 10.00G | 190.02f |
Let’s take a look at the gm spec across iterations:
Show code cell source
gm_spec_plot = bh.create_bokeh_plot(
title="gm spec across iterations",
x_axis_label='Iteration',
y_axis_label='gm spec (S)',
)
xs = [i for i in range(num_iters+1)]
gm_spec_plot.line(x=xs, y=gm_specs,)
show(gm_spec_plot)
Lovely! We’ve converged on a solution!!
To drive a 1pF loading cap and achive a unity gain frequency of 1 GHz while minimizing bias current, we’d need this device:
\(g_m = 13.02 mS\)
\(I_d = 562uA\)
\(W=852um\)
\(L=180nm\)