From e77c05867dfd89a73cb95c1429554540242d36d8 Mon Sep 17 00:00:00 2001 From: euler Date: Sat, 8 Aug 2026 22:05:47 -0500 Subject: [PATCH] diff_pair: fix gf180 LVS pin labels and dummy net Pin markers were drawn on hardcoded sky130 layer numbers -- met1_pin (67,16), met1_label (67,5), met2_pin (68,16), met2_label (68,5) -- which mean nothing on gf180. They now come from the PDK. They were also sized and placed so that the marker carried only the label layer, tangent to the port's edge rather than centred on it. The extractor then finds no conductor under the text, the net comes out unnamed, and LVS reports the pin as missing from the layout. The marker is now sized to the routable metal's minimum width and aligned centre-to-centre, so it lands inside metal the router already placed. Rebased onto current main. The `no_pin_labels()` context manager this branch originally added is dropped: #105 landed `with_pin_labels` on diff_pair, and #112 handles the general case in the LVS runner, where it needs no cooperation from any cell and does not depend on ambient state that `@cell` cannot see. --- .../diff_pair_cmirrorbias.py | 18 +++--- .../cells/elementary/diff_pair/diff_pair.py | 61 ++++++++++--------- 2 files changed, 40 insertions(+), 39 deletions(-) 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 a00d6a58..276fc176 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 @@ -195,17 +195,13 @@ 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. - # Extraction shows the merged cmirror dummy as `B B B B` on gf180 too: - # with_tie=True draws a welltie ring that IS the bulk net, and the dummy - # contacts land on it. The old sky130-only condition described a difference - # that does not exist. + # Current mirror netlist. The dummies sit inside the composite's shared + # pwell/tap context, so both extractors report their G/S/D on the bulk + # net: klayout merges the interdigitized dummy fingers into one B-tied + # device, and magic absorbs them into the bulk during parallel-device + # merging. Keeping them tied to the bulk here matches what is extracted + # on both PDKs; declaring a separate floating net would add a net the + # layout does not have. cmirror.info['netlist'] = current_mirror_netlist( pdk, width=diffpair_bias[0], diff --git a/src/glayout/cells/elementary/diff_pair/diff_pair.py b/src/glayout/cells/elementary/diff_pair/diff_pair.py index 72856ba4..1400a872 100644 --- a/src/glayout/cells/elementary/diff_pair/diff_pair.py +++ b/src/glayout/cells/elementary/diff_pair/diff_pair.py @@ -33,46 +33,50 @@ def add_df_labels(df_in: Component, ) -> Component: df_in.unlock() - met1_pin = (67,16) - met1_label = (67,5) - met2_pin = (68,16) - met2_label = (68,5) + def _pin(met: str, text: str, size: float): + """Label marker sized to the routable metal's minimum width. + + The marker is aligned centre-to-centre on its port so it lands inside + the metal the router already placed, instead of tangent to its edge. + A marker that only carries the label layer floats: the extractor finds + no conductor under the text, the net comes out unnamed, and LVS reports + the pin as missing from the layout. + """ + min_w = pdk.get_grule(met)["min_width"] + side = max(size, min_w) + marker = rectangle(layer=pdk.get_glayer(met + "_label"), size=(side, side), centered=True).copy() + marker.add_label(text=text, layer=pdk.get_glayer(met + "_label")) + return marker # list that will contain all port/comp info move_info = list() # create labels and append to info list # vtail - vtaillabel = rectangle(layer=pdk.get_glayer("met2_pin"),size=(0.27,0.27),centered=True).copy() - vtaillabel.add_label(text="VTAIL",layer=pdk.get_glayer("met2_label")) + vtaillabel = _pin("met2", "VTAIL", 0.27) move_info.append((vtaillabel,df_in.ports["bl_multiplier_0_source_S"],None)) # vdd1 - vdd1label = rectangle(layer=pdk.get_glayer("met2_pin"),size=(0.27,0.27),centered=True).copy() - vdd1label.add_label(text="VDD1",layer=pdk.get_glayer("met2_label")) + vdd1label = _pin("met2", "VDD1", 0.27) move_info.append((vdd1label,df_in.ports["tl_multiplier_0_drain_N"],None)) # vdd2 - vdd2label = rectangle(layer=pdk.get_glayer("met2_pin"),size=(0.27,0.27),centered=True).copy() - vdd2label.add_label(text="VDD2",layer=pdk.get_glayer("met2_label")) + vdd2label = _pin("met2", "VDD2", 0.27) move_info.append((vdd2label,df_in.ports["tr_multiplier_0_drain_N"],None)) # VB - vblabel = rectangle(layer=pdk.get_glayer("met1_pin"),size=(0.5,0.5),centered=True).copy() - vblabel.add_label(text="B",layer=pdk.get_glayer("met1_label")) + vblabel = _pin("met1", "B", 0.5) move_info.append((vblabel,df_in.ports["tap_N_top_met_S"], None)) # VP - vplabel = rectangle(layer=pdk.get_glayer("met2_pin"),size=(0.27,0.27),centered=True).copy() - vplabel.add_label(text="VP",layer=pdk.get_glayer("met2_label")) + vplabel = _pin("met2", "VP", 0.27) move_info.append((vplabel,df_in.ports["br_multiplier_0_gate_S"], None)) # VN - vnlabel = rectangle(layer=pdk.get_glayer("met2_pin"),size=(0.27,0.27),centered=True).copy() - vnlabel.add_label(text="VN",layer=pdk.get_glayer("met2_label")) + vnlabel = _pin("met2", "VN", 0.27) move_info.append((vnlabel,df_in.ports["bl_multiplier_0_gate_S"], None)) # move everything to position for comp, prt, alignment in move_info: - alignment = ('c','b') if alignment is None else alignment + alignment = ('c','c') if alignment is None else alignment compref = align_comp_to_port(comp, prt, alignment=alignment) df_in.add(compref) return df_in.flatten() @@ -85,17 +89,18 @@ def diff_pair_netlist(fetL: Component, fetR: Component, pdk: Optional[MappedPDK] # the right device). Model that explicitly in the reference netlist so LVS # compares against the same effective device count/width. # - # DUM maps to the dummies' G/S/D net. Standalone: - # * gf180 klayout extracts the four dummies' diffusion fingers as one - # shared floating net (the inter-dummy contacts merge them), so we - # map DUM→'dum' (a local subckt-level net). - # * sky130 magic+netgen absorbs the floating dummies into the bulk - # during parallel-device merging, so the schematic must put them on B - # directly — leaving DUM as a separate `dum` net there counts an extra - # net on the schematic side and trips the LVS comparison. - # `dum_net` lets a composite parent override this when the surrounding - # layout context (extra tap rings, shared pwell paths) physically forces - # the dummies onto a different net than the standalone-cell extraction. + # DUM maps to the dummies' G/S/D net, and which net that is follows the + # layout rather than the PDK. When a substrate tap is placed, the four + # dummies are routed to the tap ring (the + # `multiplier_0_dummy_*_gsdcon_top_met_W` -> `tap_*` routes below), so both + # extractors report them on the bulk: gf180 klayout merges the four parallel + # fingers into a single B-tied device, and sky130 magic+netgen absorbs them + # into the bulk during parallel-device merging. Declaring a separate `dum` + # net in that case adds a net the layout does not have and trips LVS. + # Without the tap ring there is nothing tying them down, so they stay on a + # floating subckt-level net. + # `dum_net` lets a caller override this when the surrounding layout context + # forces the dummies onto a different net than the standalone extraction. if dum_net is None: dum_net = 'B' if substrate_tap else 'dum' for net, fet in (('VDD1', fetL), ('VDD1', fetL), ('VDD2', fetR), ('VDD2', fetR)):