diff --git a/src/glayout/primitives/fet.py b/src/glayout/primitives/fet.py index 9f4ac6d3..c83a1607 100644 --- a/src/glayout/primitives/fet.py +++ b/src/glayout/primitives/fet.py @@ -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 @@ -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) @@ -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) @@ -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 diff --git a/tests/drc/run_cell_drc.py b/tests/drc/run_cell_drc.py index 65fb5c9b..2fb9369d 100644 --- a/tests/drc/run_cell_drc.py +++ b/tests/drc/run_cell_drc.py @@ -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", } diff --git a/tests/parameters/ci_drc_gf180.csv b/tests/parameters/ci_drc_gf180.csv index 165214f7..00f4b6ca 100644 --- a/tests/parameters/ci_drc_gf180.csv +++ b/tests/parameters/ci_drc_gf180.csv @@ -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}" diff --git a/tests/parameters/ci_drc_sky130.csv b/tests/parameters/ci_drc_sky130.csv index b4a4f46d..5c61030c 100644 --- a/tests/parameters/ci_drc_sky130.csv +++ b/tests/parameters/ci_drc_sky130.csv @@ -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}"