From 08896fbda797565d1f2303e265d4a916bf7c5925 Mon Sep 17 00:00:00 2001 From: euler Date: Sun, 16 Aug 2026 20:33:29 -0500 Subject: [PATCH] gf180 opamp: route the mimcap on the PDK's own layers __add_mimcap_arr hardcoded met4/met5 for the cap plates, which is where sky130 puts the MIM. On gf180 option A it sits on met2/met3, so the routes landed two levels above the plates with no via to step down and the whole compensation array floated -- zero via3 anywhere near it. The layers now come from pdk.get_grule("capmet"), so both PDKs work. diff_to_single described an internal node the cell does not build. Its netlist gave TOP1 and BOT1 a node of their own, V1; in the layout that node and the output are one net, which the extractor reports with five terminals. Deleting the whole strip where the two rails overlap still leaves them connected, so this is the cell's structure rather than a stray short. VSS2, the counterpart node, already matched on all three terminals. The comp fill between the centre multipliers used the plusdoped ports, which are on the implant layer, so at their own width the fill's edges came out level with the implant instead of inside it: 0.01um of extension where PP.5b/PP.5dii ask for 0.16. Insetting it by the implant enclosure clears all eight violations in the opamp and four in the converter, and still covers the 0.17um comp gap it exists for. The output stage's well was not tied to its source, and diff_pair_ibias's pin labels could not be suppressed the way diff_pair's can, so they leaked into the parent as extra top-level pins. --- .../differential_to_single_ended_converter.py | 24 +++++++++-- .../diff_pair_cmirrorbias.py | 43 +++++++++++-------- .../opamp/diff_pair_stackedcmirror.py | 10 ++++- .../cells/composite/opamp/opamp_twostage.py | 20 +++++---- ...mplifier_diff_to_single_ended_converter.py | 13 ++++++ 5 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py b/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py index 92fc2f95..cd43dfea 100644 --- a/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py +++ b/src/glayout/cells/composite/differential_to_single_ended_converter/differential_to_single_ended_converter.py @@ -100,8 +100,20 @@ def __route_sharedgatecomps(pdk: MappedPDK, shared_gate_comps, via_location, pto # between i=1/i=2 (gf180 DF.3a min comp space = 0.28um). All four are # PCOMP-outside-nwell at the same psub potential, so the rule allows # butting them — fill the gap with comp on the active_diff layer. - shared_gate_comps << route_quad(LRplusdopedPorts[1], LRplusdopedPorts[2], layer=pdk.get_glayer("active_diff")) - shared_gate_comps << route_quad(LRplusdopedPorts[5], LRplusdopedPorts[6], layer=pdk.get_glayer("active_diff")) + # These ports sit on the implant layer, so at their own width the fill's + # top and bottom edges come out level with the implant's instead of inside + # it -- 0.01um of extension where PP.5b/PP.5dii ask for 0.16. Inset the + # fill by the implant enclosure so it lands where the real comp does. + _pp_enclosure = pdk.get_grule("p+s/d", "active_diff")["min_enclosure"] + _comp_min_w = pdk.get_grule("active_diff")["min_width"] + + def _inset_to_comp(port): + narrowed = port.copy() + narrowed.width = max(port.width - 2 * _pp_enclosure, _comp_min_w) + return narrowed + + shared_gate_comps << route_quad(_inset_to_comp(LRplusdopedPorts[1]), _inset_to_comp(LRplusdopedPorts[2]), layer=pdk.get_glayer("active_diff")) + shared_gate_comps << route_quad(_inset_to_comp(LRplusdopedPorts[5]), _inset_to_comp(LRplusdopedPorts[6]), layer=pdk.get_glayer("active_diff")) # connect drain of the left 2 and right 2, short sources of all 4 shared_gate_comps << route_quad(LRdrainsPorts[0],LRdrainsPorts[3],layer=LRdrainsPorts[0].layer) shared_gate_comps << route_quad(LRdrainsPorts[4],LRdrainsPorts[7],layer=LRdrainsPorts[0].layer) @@ -171,13 +183,17 @@ def differential_to_single_ended_converter_netlist(pdk: MappedPDK, half_pload: t # as a PMOS with D=G=S=B=VSS. Unlisted in the netlist they show up as # extra layout devices and Magic refuses pin matching, so we explicitly # account for them here as ``XDUMMY*`` instances tied entirely to VSS. + # TOP1/BOT1 used to meet at a node of their own (``V1``); the layout puts + # them, BOT2's drain and the next stage's gate on one net. Structure, not + # a stray overlap: deleting the whole strip where the rails overlap still + # leaves them connected, and VSS2 already matched on all three terminals. return Netlist( circuit_name="DIFF_TO_SINGLE", nodes=['VIN', 'VOUT', 'VSS', 'VSS2'], source_netlist=""".subckt {circuit_name} {nodes} """ + f'l={half_pload[1]} w={half_pload[0]} mt={4*2} mb={2 * half_pload[2]} ' + """ -XTOP1 V1 VIN VSS VSS {model} l={{l}} w={{w}} m={{mt}} +XTOP1 VOUT VIN VSS VSS {model} l={{l}} w={{w}} m={{mt}} XTOP2 VSS2 VIN VSS VSS {model} l={{l}} w={{w}} m={{mt}} -XBOT1 VIN VIN V1 VSS {model} l={{l}} w={{w}} m={{mb}} +XBOT1 VIN VIN VOUT VSS {model} l={{l}} w={{w}} m={{mb}} XBOT2 VOUT VIN VSS2 VSS {model} l={{l}} w={{w}} m={{mb}} XDUMMY1 VSS VSS VSS VSS {model} l={{l}} w={{w}} XDUMMY2 VSS VSS VSS VSS {model} l={{l}} w={{w}} diff --git a/src/glayout/cells/composite/diffpair_cmirror_bias/diff_pair_cmirrorbias.py b/src/glayout/cells/composite/diffpair_cmirror_bias/diff_pair_cmirrorbias.py index c1a47743..e5e979ce 100644 --- a/src/glayout/cells/composite/diffpair_cmirror_bias/diff_pair_cmirrorbias.py +++ b/src/glayout/cells/composite/diffpair_cmirror_bias/diff_pair_cmirrorbias.py @@ -25,6 +25,7 @@ print_ports, set_port_orientation, rename_component_ports, + no_pin_labels, ) from glayout.util.snap_to_grid import component_snap_to_grid from pydantic import validate_arguments @@ -89,14 +90,17 @@ def diff_pair_ibias( # rings so klayout extracts the dummies' G/S/D on bulk (B). sky130 # always wants 'B' too — passing it unconditionally is correct on # both PDKs because it matches the magic-merged extraction. - center_diffpair_comp = diff_pair( - pdk, - width=half_diffpair_params[0], - length=half_diffpair_params[1], - fingers=half_diffpair_params[2], - rmult=rmult, - dum_net='B', - ) + # VTAIL is a top-level pin of a standalone diff_pair but an internal net + # here, so the inherited label would extract as an extra top-level pin. + with no_pin_labels(): + center_diffpair_comp = diff_pair( + pdk, + width=half_diffpair_params[0], + length=half_diffpair_params[1], + fingers=half_diffpair_params[2], + rmult=rmult, + dum_net='B', + ) # add antenna diodes if that option was specified diffpair_centered_ref = prec_ref_center(center_diffpair_comp) diffpair_i_.add(diffpair_centered_ref) @@ -194,23 +198,16 @@ def diff_pair_ibias( viaoffset=None, ) cmirror.add_ports(srcshort.get_ports_list(), prefix="purposegndports") - # current mirror netlist — gf180 needs `dummies_tied_to_bulk=False` - # because here we use raw two_nfet_interdigitized + custom routing, - # NOT current_mirror, so the standalone-cell's straight_route from - # dummy gsdcon to welltie never gets drawn; klayout extracts the - # cmirror dummies on a per-cell floating net. sky130 magic merges - # the floating dummies into the bulk so the schematic must keep - # them tied to VB or magic counts an extra net. - ## HACK: Note that this is a hack for magic LVS, and it's likely incorrect - ## we probably want to fix it properly - _dummies_tied = (pdk.name.lower() == "sky130") + # Current mirror netlist. The dummies share the composite's pwell/tap + # context, so both extractors report their G/S/D on the bulk net. Tying + # them to it here matches that; a separate net would not exist in layout. cmirror.info['netlist'] = current_mirror_netlist( pdk, width=diffpair_bias[0], length=diffpair_bias[1], fingers=1, multipliers=diffpair_bias[2], - dummies_tied_to_bulk=_dummies_tied, + dummies_tied_to_bulk=True, ) # add cmirror — bump y-offset enough that the LVPWELL paddings of the @@ -277,6 +274,14 @@ def diff_pair_ibias( ("VSS", "ibias_purposegndport", "met4"), ("B", "tap_N_top_met_S", "met1"), ] + # These labels let the cell pass LVS on its own. Inside a composite the + # same nets are internal, so inheriting the labels extracts them as + # top-level pins the parent's schematic does not have -- that is what + # leaves the opamp with VDD1, VDD2, VN|VP, IBIAS and B to spare. Same + # switch diff_pair, current_mirror and fvf already honour. + import os as _os_pins + if _os_pins.environ.get("GLAYOUT_NO_PIN_LABELS"): + _pin_specs = [] for _text, _portname, _glayer in _pin_specs: _port = diffpair_i_.ports[_portname] _alignment = _orient_to_align[round(_port.orientation) % 360] diff --git a/src/glayout/cells/composite/opamp/diff_pair_stackedcmirror.py b/src/glayout/cells/composite/opamp/diff_pair_stackedcmirror.py index abe009db..7211dccc 100644 --- a/src/glayout/cells/composite/opamp/diff_pair_stackedcmirror.py +++ b/src/glayout/cells/composite/opamp/diff_pair_stackedcmirror.py @@ -17,6 +17,7 @@ from glayout.cells.composite.diffpair_cmirror_bias import diff_pair_ibias from glayout.cells.composite.stacked_current_mirror import stacked_nfet_current_mirror +from glayout.util.port_utils import no_pin_labels from glayout.cells.composite.differential_to_single_ended_converter import differential_to_single_ended_converter from glayout.cells.composite.opamp.row_csamplifier_diff_to_single_ended_converter import row_csamplifier_diff_to_single_ended_converter @@ -24,7 +25,9 @@ @validate_arguments def __add_diff_pair_and_bias(pdk: MappedPDK, toplevel_stacked: Component, half_diffpair_params: tuple[float, float, int], diffpair_bias: tuple[float, float, int], rmult: int, with_antenna_diode_on_diffinputs: int) -> Component: clear_cache() - diffpair_i_ref = diff_pair_ibias(pdk, half_diffpair_params, diffpair_bias, rmult, with_antenna_diode_on_diffinputs) + # Inside the opamp, diff_pair_ibias's pins are internal nets. + with no_pin_labels(): + diffpair_i_ref = diff_pair_ibias(pdk, half_diffpair_params, diffpair_bias, rmult, with_antenna_diode_on_diffinputs) toplevel_stacked.add(diffpair_i_ref) toplevel_stacked.add_ports(diffpair_i_ref.get_ports_list(),prefix="diffpair_") @@ -76,7 +79,10 @@ def __route_bottom_ncomps_except_drain_nbias(pdk: MappedPDK, toplevel_stacked: C # leaving a sliver gap that trips m2.2a. Stamp an m2 patch at each # corner that overlaps both polygons so they merge in DRC. if pdk.name.lower() == "gf180": - from gdsfactory.components.rectangle import rectangle as _rect + # use the backend's rectangle (already imported at module level), not + # gdsfactory's: importing it directly bypasses the backend abstraction + # and yields a component the active backend cannot reference. + _rect = rectangle _m2 = pdk.get_glayer("met2") # Use cmirror_ref_L's drain_E port (center.x is the inner edge of # cmirror_ref_L's leftmost drain column on m2; mirror for R) and diff --git a/src/glayout/cells/composite/opamp/opamp_twostage.py b/src/glayout/cells/composite/opamp/opamp_twostage.py index 5ac43f91..9d408f4c 100644 --- a/src/glayout/cells/composite/opamp/opamp_twostage.py +++ b/src/glayout/cells/composite/opamp/opamp_twostage.py @@ -122,18 +122,22 @@ def __add_mimcap_arr(pdk: MappedPDK, opamp_top: Component, mim_cap_size, mim_cap displace_fact = max(max_metalsep,pdk.get_grule("capmet")["min_separation"]) + 0.3 # Hack mimcaps_ref.movex(pdk.snap_to_2xgrid(opamp_top.xmax + displace_fact + mim_cap_size[0]/2)) mimcaps_ref.movey(pdk.snap_to_2xgrid(ymin + mim_cap_size[1]/2)) - # connect mimcap. Match OpenFASOC reference: use the cap plates' native - # routing layers — V2 (cap_metalbottom) is glayout met4, V1 (cap_metaltop) - # is glayout met5. The c_route to V2 lands on met4 and the L_route to V1 - # lands on met5, so both via stacks contact the cap plates directly. + # connect mimcap. The routes have to land on the layers the cap plates are + # actually drawn on, and those come from the PDK: sky130 puts the MIM + # between met4 and met5, gf180's option A puts it between met2 and met3. + # Hardcoding met4/met5 left the whole array floating on gf180 -- the routes + # arrived two levels above the plates with no via3 to step down. + capmettop = pdk.layer_to_glayer(pdk.get_grule("capmet")["capmettop"]) + capmetbottom = pdk.layer_to_glayer(pdk.get_grule("capmet")["capmetbottom"]) + # the corridor layer has to stay clear of both plates + cglayer = "met4" if capmettop != "met4" else "met5" port1 = opamp_top.ports["pcomps_mimcap_connection_con_N"] port2 = mimcaps_ref.ports["row"+str(int(mim_cap_rows)-1)+"_col0_top_met_N"] cref2_extension = max_metalsep + opamp_top.ymax - max(port1.center[1], port2.center[1]) - opamp_top << c_route(pdk,port1,port2, extension=cref2_extension, fullbottom=True, e1glayer="met3", e2glayer="met5", cglayer="met4", width2=5.0) # A Hack + opamp_top << c_route(pdk,port1,port2, extension=cref2_extension, fullbottom=True, e1glayer="met3", e2glayer=capmettop, cglayer=cglayer, width2=5.0) # A Hack intermediate_output = set_port_orientation(n_to_p_output_route.ports["con_S"],"N") - # opamp_top << L_route(pdk, mimcaps_ref.ports["row0_col0_top_met_N"], intermediate_output, hwidth=1, hglayer="met4", vglayer="met4") - # C route up right up to reach the mimcap port, extension is - opamp_top << L_route(pdk, intermediate_output, mimcaps_ref.ports["row0_col0_bottom_met_E"], fullbottom=True, vglayer="met5", hglayer="met4") + # C route up right up to reach the mimcap port, extension is + opamp_top << L_route(pdk, intermediate_output, mimcaps_ref.ports["row0_col0_bottom_met_E"], fullbottom=True, vglayer=cglayer, hglayer=capmetbottom) opamp_top.add_ports(mimcaps_ref.get_ports_list(),prefix="mimcap_") # add the cs output as a port opamp_top.add_port(name="commonsource_output_E", port=intermediate_output) diff --git a/src/glayout/cells/composite/opamp/row_csamplifier_diff_to_single_ended_converter.py b/src/glayout/cells/composite/opamp/row_csamplifier_diff_to_single_ended_converter.py index 07623d7b..8fcbbc68 100644 --- a/src/glayout/cells/composite/opamp/row_csamplifier_diff_to_single_ended_converter.py +++ b/src/glayout/cells/composite/opamp/row_csamplifier_diff_to_single_ended_converter.py @@ -5,6 +5,7 @@ from glayout.cells.elementary.diff_pair import diff_pair from glayout.primitives.guardring import tapring from glayout.primitives.mimcap import mimcap_array, mimcap +from glayout.routing.straight_route import straight_route from glayout.routing.L_route import L_route from glayout.routing.c_route import c_route from glayout.primitives.via_gen import via_stack, via_array @@ -84,6 +85,18 @@ def row_csamplifier_diff_to_single_ended_converter(pdk: MappedPDK, diff_to_singl ) halfMultp_ref = pmos_comps << halfMultp halfMultp_ref.movex(direction * abs(x_dim_center + halfMultp_ref.xmax+1)) + # Well to the source potential. This cell's netlist declares B = S = + # VSS and __connect_cs_netlist assumes the welltie ring already does + # that, but extraction puts the ring on a net of its own and the + # output pfets end up with a floating well. The ring sits against the + # device and both ports are met2, so this is a straight run. + side = "W" if direction < 0 else "E" + far = "E" if direction < 0 else "W" + pmos_comps << straight_route( + pdk, + halfMultp_ref.ports["multiplier_0_source_" + side], + halfMultp_ref.ports["tie_" + side + "_top_met_" + far], + ) label = "L_" if direction==-1 else "R_" # this special marker is used to rename these ports in the opamp to commonsource_Pamp_ pmos_comps.add_ports(halfMultp_ref.get_ports_list(),prefix="halfpspecialmarker_"+label)