Add interposer extensions for LBE,TSV,pin and forbidden - #19
Add interposer extensions for LBE,TSV,pin and forbidden#19yegappan-ihp wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Thanks for this PR. The pin and forbidden decks are clean ports, the rule-name slicing for M4/M5/TM1/TM2 is exactly right, and LBE.f closes a gap we had left open. I ran the whole thing against a few fixtures of my own and found two things in the TSV_G half that I think have to change before it lands, plus a handful of smaller asks.
Since TSV is on our roadmap for the interposer, this table isn't speculative and the deck belongs here, which is precisely why I'd like these two fixed now rather than after people start drawing DeepVia.
Everything below is reproducible with the fixture generator at the bottom of this comment (KLayout 0.30.5). The fixture is a TSV built from the device geometry (outer 25.0 um, wall 3.0 um, hole 19.0 um) with its enclosures (PWell:block at outer+2.5, Metal1 at outer+1.5), sitting in an 800x800 um prBoundary.
Blocking
A conforming TSV can't pass a default DRC run
$ run_drc.py --path tsv_one_conforming.gds --topcell tsv_one_conforming
Violated rules: ['TSV_G.d', 'deepvia_drw_Acute']
TSV_G.a/.b/.c/.e/.f/.g are all clean on that shape, so the deck itself agrees it conforms. Two rules from two different decks still reject it, and there's no geometry that clears both.
TSV_G.d measures each ring against its own opening
10_1_tsv_g.drc:57 runs deepvia_ring.space(...) on the annulus, which still carries its hole, and Region#space is notch ∪ isolated, so a polygon's own hole counts. The split makes it obvious:
| fixture | space(25) |
notch(25) |
isolated(25) |
|---|---|---|---|
| one conforming ring | 992 | 992 | 0 |
| two rings, 35 um apart (legal) | 1984 | 1984 | 0 |
| two rings, 10 um apart (illegal) | 2412 | 1984 | 428 |
1984 is exactly 2 x 992, i.e. zero inter-ring contribution when the rings are legally spaced. There's also an internal contradiction worth pointing at: line 36 derives the mandated inner diameter as TSV_G_c - 2*TSV_G_b = 19.0 and TSV_G.b rejects anything else, while TSV_G.d rejects 19.0. Nothing can satisfy both.
Once you've had a look, the check I'd use is that a lone conforming ring and two conforming rings 35 um apart both give zero TSV_G.d markers, while two rings 10 um apart still fail. tsv_one_conforming, tsv_pair_legal and tsv_pair_too_close from the generator below are those three cases.
deepvia_drw_Acute fires on every conforming ring
This one comes from the 3-line hunk in 3_2_angle.drc:116-118. Putting deepvia_drw in ang_acute_scope exposes DeepVia to a check whose circle exemption, get_circle (intm4tm2.drc:86-93), is built for solid discs: it applies .without_holes and with_area_ratio(1.270, 1.276), i.e. the 4/pi bbox-to-area ratio of a filled circle. An annulus comes out at 3.01 and never gets exempted. On top of that the rule uses .raw, so the keyhole seam GDS uses to store a donut survives as a degenerate corner.
--deck angle, solid 25 um disc -> DRC PASSED: No violations detected
--deck angle, conforming ring -> Violated rules: ['deepvia_drw_Acute'] (2 markers)
So the exemption works for the one DeepVia shape TSV_G.a forbids, and fails for the only one it allows. What I'd look for afterwards is a conforming ring passing --deck angle, without the exemption growing so wide that a DeepVia with a genuine acute corner slips through too.
The smoke suite can't tell a violation from a deck that never ran
run_interposer_smoke.py:55 only compares subprocess.run(...).returncode against expect_exit, and run_drc.py returns 1 for violations and for every error path alike. I swapped topmetal2_pin for an undefined name in 7_4_pin.drc and got the full CalledProcessError traceback followed by Executed 1 interposer smoke case(s) successfully. and exit 0. Same result if I delete a fixture, or delete all but one rule from 3_2_forbidden.drc.
I think this is the actual reason the two above slipped through: nothing in the PR can observe which rules fired. run_regression.py already compares violated-rule sets per top cell, so the machinery is there. Ideally the suite fails when a deck aborts and when a rule stops emitting its markers; those four mutations are a decent check.
Smaller things, before merge
- Each new fixture could use a
*_cleantop cell. All 16 existing fixtures ship a<table>_clean/<table>_violpair andtesting/README.mdasks for it. Right now nothing asserts that legal geometry passes, which is how the two blockers got through. pin_violationandforbidden_violationare missingtopcellininterposer_smoke_tests.json. They lean on single-top-cell auto-detection, so as soon as a_cleancell exists the runner aborts with "multiple top cells", and because of the point above the case still reports success.LBE.fnever actually executes.lbe.gdsonly has 157/0, 41/0, 9/0 and 63/0, no Activ 1/0, so the second operand is empty by construction andGOLDEN['lbe']is unchanged.lbealready has a generator and a golden entry, so of all the new rules this is the cheapest one to pin down.TSV_G.iandTSV_G.jare global density rules sitting on the default path. They normalise againstCHIP(the layout extent), while every other chip-normalised rule usesdensity.drc's prBoundary > EdgeSeal-boundary > extent selection (density.drc:54-71), andDEFAULT_SKIP_DECKSis still{'density'}(intm4tm2.drc:155,run_drc.py:39).LBE.iis the precedent here: it belongs to spec table 9.1 but lives indensity.drc.TSV_G.athroughTSV_G.gshould stay default-on regardless.- It'd be good to keep the JSON provenance checkable. The old
descriptionlisted the keys with no upstream JSON counterpart; it's now "approved SG13G2-derived interposer extensions", in the same commit that adds eight more such keys, and there's no changelog for the 1.4 to 1.5 bump.drc/docs/extra_rules.mdcoversTSV_G.a/d/f/g/i/j, butTSV_G_b,TSV_G_candTSV_G_edon't have a source I can follow. A line in the deck header would do it. libs.tech/klayout/README.mdlists the rule-deck files and a deck table in two places, and both still say 20 decks.DeepVia 152/0isn't inintm4tm2.lyp. It's the primary input of the whole new table, and 24 of the 25 newlayers_def.drcdeclarations are missing from the technology layer set, so today someone can be told their DeepVia violates nine rules and still not be able to display or select the layer. With TSV coming, DeepVia needs to become drawable, but that's a change to the layer set rather than to this PR and it's on us; I'm flagging it here so the deck and the layer set don't drift apart quietly. The other 23 are check-only inputs and it'd help iflayers_def.drcsaid so.
One question
deepvia_drw went into 3_2_angle.drc's scope but not 3_1_offgrid.drc's. Was that deliberate? testcases/interposer/tsv.gds has 116 vertices off the 5 nm grid on 152/0, and with TSV in scope the grid check will start mattering for real geometry.
Not blocking, just noting
interacting(deepvia_drw.holes)picks polygons that touch a hole rather than ones that have one, soTSV_G.amisses a solid DeepVia sitting inside another ring's opening. Same idiom is already in9_1_lbe.drc:66though, so this is ours as much as yours.TSV_G.btests the hole's bounding box, which pins the mean wall but not concentricity: a displaced hole keeps a 19.000 x 19.000 extent while one wall thins out. Under-detection only, so not urgent.- Five comments in files you didn't touch (
6_10_sealring.drc:29,6_9_pad.drc:71,density.drc:29and:352,7_3_metalslits.drc:67) still say Activ/pSD/GatPoly/Cont/Metal1 don't exist in this stack. You already fixed the sixth one in9_1_lbe.drc. LBE.b2.umto.um2reads better, though it isn't a behaviour change (30000.um == 30000.um2 == 30000.0).LBE.b1on line 46 still uses.umfor an area if you feel like being consistent.10_1_tsv_g.drc:93-104is indented a level deeper than the rest of the deck, andtsvg_bbox = nilon line 103 is dead.
Fixtures and probe used above (no dependency on the repo, save and run)
mk_tsv_fixtures.py, run with any Python that has the klayout module:
#!/usr/bin/env python3
"""Minimal fixtures for reviewing the TSV_G deck.
Geometry comes from the TSV device: outer radius 12.5 um, inner radius 9.5 um,
so outer diameter 25.0 = TSV_G_c, wall 3.0 = TSV_G_b, hole 19.0. Enclosures as
the device draws them: PWell:block at outer+2.5 (TSV_G_f), Metal1 at outer+1.5
(TSV_G_g). Every cell is a conforming TSV unless its name says otherwise.
"""
import math
import sys
import klayout.db as db
DBU = 0.001
R_OUT = 12.5
R_IN = 9.5
NSIDES = 64 # matches a polygonal-circle donut
PRB = 400.0 # prBoundary half-size: keeps the DeepVia density rules quiet
def ring_pts(r, n=NSIDES):
"""Circle approximation with a vertex on +x, so bbox == 2*r exactly."""
return [db.DPoint(r * math.cos(2 * math.pi * k / n),
r * math.sin(2 * math.pi * k / n)) for k in range(n)]
def donut(cx=0.0, cy=0.0):
p = db.DPolygon(ring_pts(R_OUT))
p.insert_hole(ring_pts(R_IN))
return p.moved(cx, cy)
def disc(cx=0.0, cy=0.0):
return db.DPolygon(ring_pts(R_OUT)).moved(cx, cy)
def square_ring(cx=0.0, cy=0.0):
p = db.DPolygon(db.DBox(-R_OUT, -R_OUT, R_OUT, R_OUT))
p.insert_hole(db.DBox(-R_IN, -R_IN, R_IN, R_IN))
return p.moved(cx, cy)
def new_layout(cell_name):
ly = db.Layout()
ly.dbu = DBU
top = ly.create_cell(cell_name)
lay = {
"deepvia": ly.layer(152, 0),
"pwell_block": ly.layer(46, 21),
"metal1": ly.layer(8, 0),
"prboundary": ly.layer(235, 0),
}
top.shapes(lay["prboundary"]).insert(db.DBox(-PRB, -PRB, PRB, PRB))
return ly, top, lay
def add_context(top, lay, cx=0.0, cy=0.0):
"""Enclosures a conforming TSV carries: TSV_G_f = 2.5, TSV_G_g = 1.5."""
top.shapes(lay["pwell_block"]).insert(
db.DBox(cx - R_OUT - 2.5, cy - R_OUT - 2.5, cx + R_OUT + 2.5, cy + R_OUT + 2.5))
top.shapes(lay["metal1"]).insert(
db.DBox(cx - R_OUT - 1.5, cy - R_OUT - 1.5, cx + R_OUT + 1.5, cy + R_OUT + 1.5))
def write(outdir, name, build):
ly, top, lay = new_layout(name)
build(top, lay)
ly.write("%s/%s.gds" % (outdir, name))
print("wrote %s/%s.gds (topcell %s)" % (outdir, name, name))
def main():
outdir = sys.argv[1] if len(sys.argv) > 1 else "."
# One conforming TSV, alone, with its enclosures. Should be clean.
def f_one(top, lay):
top.shapes(lay["deepvia"]).insert(donut())
add_context(top, lay)
write(outdir, "tsv_one_conforming", f_one)
# Same as an axis-aligned square annulus: on the 5 nm grid, and it isolates
# TSV_G.d from any circle-discretisation effect.
def f_sq(top, lay):
top.shapes(lay["deepvia"]).insert(square_ring())
add_context(top, lay)
write(outdir, "tsv_one_square", f_sq)
# Control for the angle-deck circle exemption: a SOLID disc, i.e. the one
# DeepVia shape TSV_G.a forbids.
def f_disc(top, lay):
top.shapes(lay["deepvia"]).insert(disc())
write(outdir, "tsv_solid_disc", f_disc)
# Two conforming TSVs, 35 um edge to edge (legal: TSV_G_d = 25.0).
def f_far(top, lay):
for cx in (0.0, 60.0):
top.shapes(lay["deepvia"]).insert(donut(cx))
add_context(top, lay, cx)
write(outdir, "tsv_pair_legal", f_far)
# Two conforming TSVs, 10 um edge to edge (genuine TSV_G.d violation).
def f_near(top, lay):
for cx in (0.0, 35.0):
top.shapes(lay["deepvia"]).insert(donut(cx))
add_context(top, lay, cx)
write(outdir, "tsv_pair_too_close", f_near)
if __name__ == "__main__":
main()Commands, from libs.tech/klayout/tech/drc on this branch:
python3 mk_tsv_fixtures.py /tmp/tsvfix
python3 run_drc.py --path /tmp/tsvfix/tsv_one_conforming.gds --topcell tsv_one_conforming --threads 1
python3 run_drc.py --path /tmp/tsvfix/tsv_solid_disc.gds --topcell tsv_solid_disc --deck angle --threads 1
python3 run_drc.py --path /tmp/tsvfix/tsv_one_conforming.gds --topcell tsv_one_conforming --deck angle --threads 1
python3 run_drc.py --path /tmp/tsvfix/tsv_one_square.gds --topcell tsv_one_square --deck tsv_g --threads 1
python3 run_drc.py --path /tmp/tsvfix/tsv_pair_legal.gds --topcell tsv_pair_legal --deck tsv_g --threads 1
python3 run_drc.py --path /tmp/tsvfix/tsv_pair_too_close.gds --topcell tsv_pair_too_close --deck tsv_g --threads 1probe_space.drc, for the notch/isolated split in the table above. Run it with klayout -b -zz -r probe_space.drc -rd input=/tmp/tsvfix/tsv_one_conforming.gds:
# Shows that Region#space == notch (intra-polygon) + isolated (inter-polygon).
src = source($input)
dv = src.polygons(152, 0)
ring = dv.interacting(dv.holes)
puts "#{$input.split('/').last}: space=#{ring.space(25.0.um, euclidian).count} " \
"notch=#{ring.notch(25.0.um, euclidian).count} " \
"isolated=#{ring.isolated(25.0.um, euclidian).count}"
tsv_review_fixtures.zip |
|
One more thing on the TSV_G table, separate from the
|
| Metal1 in the layout | TSV_G.g |
|---|---|
| none at all | clean |
| square at half-size 14.0, i.e. exactly 1.5 enclosure | clean |
| square at half-size 13.0, i.e. 0.5 enclosure | fires |
| covers only the left half of the ring | fires |
| 4x4 square floating inside the ring hole | clean |
| well clear of the ring, 27.5 um away | clean |
TSV_G.f mirrors this against PWell:block, I ran the same six cases there too. So the rules are satisfiable and they do discriminate on enclosure width, which is the part that works. What they cannot do is fail when there is no enclosing polygon at all: enclosed() only forms edge pairs where the enclosed shape actually sits inside the enclosing one, so total non-overlap produces no pairs and therefore no output. Partial overlap is caught, total absence is not.
That distinction matters more here than it would elsewhere, because the enclosing Metal1 is not user routing, it is the landing metal the via needs in order to be a via. If the intended flow is that a TSV device draws its own Metal1 and PWell:block at exactly the rule values, then within this deck TSV_G.g is the only check that the landing metal arrived at all, and as written that is the one thing it cannot do.
The deck already carries the idiom for this case. 6_11_mim.drc:75-79 joins a mim_drw.not(metal5_drw) term onto the enclosure result, under the comment "No-landing miss branch: MIM area with no Metal5 above it at all (enclosed() is blind to full non-overlap)", and 6_9_solderbump.drc:109-111 does the same for Padb.c. TSV_G.f and TSV_G.g do not have it. One detail if you take that route: both of those call .polygons on the enclosed() side and these two do not, so joining a miss branch straight on stops the run with "'join': Requires input of the same kind".
The check I would use afterwards: a ring with no Metal1 at all, and a ring whose Metal1 sits well clear of it, both produce TSV_G.g; a square Metal1 at exactly 1.5 stays clean; and the 0.5 row above still fires, so the enclosure check survives rather than being replaced by the miss branch. The one row that has to be decided rather than preserved is the 4x4 square in the hole: a miss branch anchored on the ring alone turns it into a violation, one anchored on the ring plus its opening leaves it clean. Same shape for TSV_G.f against PWell:block at 2.5.
TSV_G.e is correct as written, and should not get the same treatment
Flagging this because the miss branch is tempting to apply table-wide and here it would be wrong. TSV_G.e is a spacing rule, so a vacuous pass against absent Activ, GatPoly and Cont is the arithmetically right answer: "no Activ within 5 um" is true when there is no Activ. I checked that it discriminates properly when the operands do exist, Activ at 0.5 um from the ring fires it and Activ at 6.0 um does not.
Worth checking before anyone writes a testcase for it, though. 10_1_tsv_g.drc:63 builds tsvg_e_targets from activ_drw, activ_filler, gatpoly_drw, gatpoly_filler and cont_drw, so if placed TSVs stake out their keepout with the nofill purposes rather than with these, the rule never sees an operand from a device instance and a testcase has to place the geometry by hand.
The layer declarations, expanding on point 7
More detail on point 7 of the review rather than a new ask. layers_def.drc grows by twenty-five declarations, on the eighteen layer numbers 1, 3, 5, 6, 8, 11, 13, 14, 35, 46, 53, 55, 57, 58, 62, 71, 139, 152 plus 99/32. Only Recog.tsv 99/32 has an entry in the layer authorities; the other twenty-four are absent from intm4tm2_layers.txt, intm4tm2.lyp, intm4tm2.map, intm4tm2.lyt and the LVS layer definitions, and intm4tm2_tests/test_layer_parity.py only compares the lyp against the canonical map, so nothing ties the deck's intake to either.
Eleven of them feed 3_2_forbidden.drc, and that part is coherent, they are declared precisely so they can be rejected. Six have no consumer anywhere in the deck: psd_drw 14/0, pwell_drw 46/0, activ_pin 1/2, gatpoly_pin 5/2, metal1_pin 8/2 and recog_tsv 99/32.
The remaining eight are the live ones: Activ 1/0 and 1/22, GatPoly 5/0 and 5/22, Cont 6/0, Metal1 8/0, PWell:block 46/21 and DeepVia 152/0 itself. Reading them is a decision that this geometry is legitimate in an interposer layout, and that decision is not written down anywhere in the diff. This is a question about the whole table rather than an argument against any particular rule, and I do not think the answer is obvious. It just should not be implicit.
Criterion here is only that it be visible: either the layer authorities get extended alongside the deck, with intm4tm2_layers.txt and the parity test moving together, or the deck states in a comment why it reads layers the rest of the kit does not declare.
Reproducing this takes more than the zip on the previous comment. Its generator draws a conforming Metal1 and PWell:block on every ring it builds, so the no-Metal1 and 0.5-enclosure rows need that context made optional rather than added to, and the other three Metal1 placements are new. The TSV_G.e numbers need Activ fixtures, which that generator never draws at all. Happy to attach a second zip if that is easier than rebuilding it.
|
Heads up that this now shows as blocked, and that is on our side rather than Would it be possible to retarget it to |

Added additional rules for interposer with respect to the recent discussions.Made changes compatible to accommodate the new rule in the run DRC file as well.Ran simple smoke tests to detect the changes in the added rule sets.Testcases are also updated.