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)