Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 77 additions & 6 deletions src/glayout/primitives/fet.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@
from decimal import Decimal
from glayout.routing.straight_route import straight_route
from glayout.spice import Netlist
from warnings import warn


def __comp_min_width(pdk: MappedPDK) -> float:
"""Minimum diffusion height that can host a contact (rule CO.4 in gf180).

The COMP region must enclose the contact by min_enclosure on every side:

mcon.width + 2 * mcon-active_diff.min_enclosure

which is 0.22 + 2*0.07 = 0.36um in gf180. Below that the *channel* may
still be narrower, but the diffusion under the contact may not: that is
the classic "dogbone" geometry, and it is what the PDK's own pcell does
(gf180mcu::nfet_03v3_draw declares wmin=0.22 and lays out a 0.22um device
with the COMP at two different heights).

Derived from the PDK rules rather than hardcoded, so it carries over to
other technologies. Returns 0.0 if the rules are unavailable, which keeps
the previous behaviour.
"""
try:
return pdk.snap_to_2xgrid(
pdk.get_grule("mcon")["width"]
+ 2 * pdk.get_grule("mcon", "active_diff")["min_enclosure"]
)
except Exception:
return 0.0


@validate_arguments
Expand All @@ -27,11 +54,24 @@ def __gen_fingers_macro(pdk: MappedPDK, rmult: int, fingers: int, length: float,
poly_spacing = max(sd_viaxdim, poly_spacing)
met1_minsep = pdk.get_grule("met1")["min_separation"]
poly_spacing += met1_minsep if length < met1_minsep else 0
# On a dogbone the pads stand proud of the channel, so the gap between two
# of them is a notch in the diffusion and owes COMP-to-COMP spacing. Left
# alone it comes out at 0.26um against a 0.28um rule. Widening the gate
# pitch fixes it without touching the channel, so the device stays the
# width that was asked for and only grows along x.
if max(width, __comp_min_width(pdk)) > width:
pad_xdim = sd_viaxdim + 2 * pdk.get_grule("mcon", "active_diff")["min_enclosure"]
comp_space = pdk.get_grule("active_diff")["min_separation"]
poly_spacing = max(poly_spacing, pad_xdim + comp_space - length)
# create a single finger
finger = Component("finger")
gate = finger << rectangle(size=(length, poly_height), layer=pdk.get_glayer("poly"), centered=True)
sd_viaarr = via_array(pdk, "active_diff", "met1", size=(sd_viaxdim, width), minus1=True, lay_bottom=False).copy()
interfinger_correction = via_array(pdk,"met1",inter_finger_topmet, size=(None, width),lay_every_layer=True, num_vias=(1,None))
# Contacts are sized by the *contact* width, not the channel width: a
# narrow channel does not prevent contacting, the diffusion simply widens
# under the contact (dogbone). Without this, small W violates CO.4.
contact_width = max(width, __comp_min_width(pdk))
sd_viaarr = via_array(pdk, "active_diff", "met1", size=(sd_viaxdim, contact_width), minus1=True, lay_bottom=False, no_exception=True).copy()
interfinger_correction = via_array(pdk,"met1",inter_finger_topmet, size=(None, contact_width),lay_every_layer=True, num_vias=(1,None), no_exception=True)
sd_viaarr << interfinger_correction
sd_viaarr_ref = finger << sd_viaarr
sd_viaarr_ref.movex((poly_spacing+length) / 2)
Expand All @@ -50,8 +90,26 @@ def __gen_fingers_macro(pdk: MappedPDK, rmult: int, fingers: int, length: float,
# create diffusion and +doped region
multiplier = rename_ports_by_orientation(centered_farray)
diff_extra_enc = 2 * pdk.get_grule("mcon", "active_diff")["min_enclosure"]
diff_dims =(diff_extra_enc + evaluate_bbox(multiplier)[0], width)
diff = multiplier << rectangle(size=diff_dims,layer=pdk.get_glayer("active_diff"),centered=True)
# The channel is `width` tall; the diffusion only has to widen where a
# contact sits on it, so that CO.4's enclosure is met. Widening the whole
# strip instead would be simpler to draw and would silently build a wider
# device than the caller asked for -- a 0.22um request coming out as
# 0.36um, with no error to notice it by. Hence the dogbone: a narrow strip
# the full length, with a pad at each source/drain contact.
comp_w = max(width, __comp_min_width(pdk))
diff_len = diff_extra_enc + evaluate_bbox(multiplier)[0]
diff_dims = (diff_len, comp_w)
diff = multiplier << rectangle(size=(diff_len, width),
layer=pdk.get_glayer("active_diff"),
centered=True)
if comp_w > width:
pad_dims = (sd_viaxdim + diff_extra_enc, comp_w)
pitch = poly_spacing + length
for contact in range(fingers + 1):
pad = multiplier << rectangle(size=pad_dims,
layer=pdk.get_glayer("active_diff"),
centered=True)
pad.movex((contact - fingers / 2) * pitch)
sd_diff_ovhg = pdk.get_grule(sdlayer, "active_diff")["min_enclosure"]
sdlayer_dims = [dim + 2*sd_diff_ovhg for dim in diff_dims]
sdlayer_ref = multiplier << rectangle(size=sdlayer_dims, layer=pdk.get_glayer(sdlayer),centered=True)
Expand Down Expand Up @@ -210,10 +268,23 @@ def multiplier(
min_length = pdk.get_grule("poly")["min_width"]
length = min_length if (length or min_length) <= min_length else length
length = pdk.snap_to_2xgrid(length)
min_width = max(min_length, pdk.get_grule("active_diff")["min_width"])
# The floor on width is the diffusion's own min width -- 0.22um in gf180,
# matching the wmin the foundry pcell declares. It used to be max(that,
# min_length), but min_length is the poly's min width, which constrains
# the channel *length*: using it here confused two different dimensions
# and quietly turned a requested 0.22um device into a 0.28um one.
min_width = pdk.get_grule("active_diff")["min_width"]
if width and width < min_width:
warn(f"width {width}um is below the {min_width}um minimum for "
f"{pdk.name}; building a {min_width}um device instead")
width = min_width if (width or min_width) <= min_width else width
width = pdk.snap_to_2xgrid(width)
poly_height = width + 2 * pdk.get_grule("poly", "active_diff")["overhang"]
# The end cap has to clear the *widest* diffusion the poly runs beside,
# which on a dogbone is the contact pad, not the channel. Sizing this on
# `width` alone leaves the poly 0.17um short at the pad corners and trips
# PL.4. Note this does not widen the device: electrical width is poly
# over COMP, and COMP is still `width` tall under the gate.
poly_height = max(width, __comp_min_width(pdk)) + 2 * pdk.get_grule("poly", "active_diff")["overhang"]
# call finger array
multiplier = __gen_fingers_macro(pdk, interfinger_rmult, fingers, length, width, poly_height, sdlayer, inter_finger_topmet)
# route all drains/ gates/ sources
Expand Down
4 changes: 4 additions & 0 deletions tests/drc/run_cell_drc.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class CellSpec:
"diff_pair_ibias": "glayout.cells.composite:diff_pair_ibias",
"low_voltage_cmirror": "glayout.cells.composite:low_voltage_cmirror",
"opamp": "glayout.cells.composite:opamp",
# Narrow primitives: the diffusion under a contact cannot shrink with the
# channel (CO.4 in gf180), so these exercise the dogbone path in fet.py.
"nmos_narrow": "glayout.primitives.fet:nmos",
"pmos_narrow": "glayout.primitives.fet:pmos",
}


Expand Down
2 changes: 2 additions & 0 deletions tests/parameters/ci_drc_gf180.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ differential_to_single_ended_converter,"{""rmult"": 3, ""half_pload"": [3.0, 1.0
diff_pair_ibias,"{""half_diffpair_params"": [5.0, 1.0, 1], ""diffpair_bias"": [5.0, 2.0, 1], ""rmult"": 2, ""with_antenna_diode_on_diffinputs"": 0}"
low_voltage_cmirror,"{""width"": [4.0, 1.5], ""length"": 2.0, ""fingers"": [2, 1], ""multipliers"": [1, 1]}"
opamp,"{""half_diffpair_params"": [5.0, 1.0, 1], ""diffpair_bias"": [5.0, 2.0, 1], ""half_common_source_params"": [7.0, 1.0, 10, 5], ""half_common_source_bias"": [6.0, 2.0, 8, 4], ""half_pload"": [6.0, 1.0, 5], ""add_output_stage"": false, ""with_antenna_diode_on_diffinputs"": 0, ""rmult"": 1}"
nmos_narrow,"{""width"": 0.22, ""length"": 0.28}"
pmos_narrow,"{""width"": 0.22, ""length"": 0.28}"
2 changes: 2 additions & 0 deletions tests/parameters/ci_drc_sky130.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ differential_to_single_ended_converter,"{""rmult"": 1, ""half_pload"": [3.0, 1.0
diff_pair_ibias,"{""half_diffpair_params"": [5.0, 1.0, 1], ""diffpair_bias"": [5.0, 2.0, 1], ""rmult"": 1, ""with_antenna_diode_on_diffinputs"": 0}"
low_voltage_cmirror,"{""width"": [4.0, 1.5], ""length"": 2.0, ""fingers"": [2, 1], ""multipliers"": [1, 1]}"
opamp,"{""half_diffpair_params"": [6, 1, 4], ""diffpair_bias"": [6, 2, 4], ""half_common_source_params"": [7, 1, 10, 3], ""half_common_source_bias"": [6, 2, 8, 2], ""output_stage_params"": [5, 1, 16], ""output_stage_bias"": [6, 2, 4], ""half_pload"": [6, 1, 6], ""mim_cap_size"": [12, 12], ""mim_cap_rows"": 3, ""rmult"": 2, ""with_antenna_diode_on_diffinputs"": 0, ""add_output_stage"": false}"
nmos_narrow,"{""width"": 0.15, ""length"": 0.15}"
pmos_narrow,"{""width"": 0.15, ""length"": 0.15}"