feat(precheck): add pin checks

Checks for missing or invalid pins, wrong pin dimensions or layers.
This commit is contained in:
htfab
2024-05-01 00:55:53 +03:00
committed by Uri Shaked
parent 51e5747c09
commit 97e65e74f6
5 changed files with 419 additions and 4 deletions
+268
View File
@@ -0,0 +1,268 @@
import logging
import re
import gdstk
from precheck_failure import PrecheckFailure
def parsefp3(value: str):
# parse fixed-point numbers with 3 digits after the decimal point
# e.g. '20.470' to 20470
ip, fp = value.split(".")
mul = ip + fp[:3].rjust(3, "0")
return int(mul)
def pin_check(gds: str, lef: str, template_def: str, toplevel: str):
logging.info("Running pin check...")
# parse pins from template def
diearea_re = re.compile(r"DIEAREA \( (\S+) (\S+) \) \( (\S+) (\S+) \) ;")
pins_re = re.compile(r"PINS (\d+) ;")
pin_re = re.compile(r" *- (\S+) \+ NET (\S+) \+ DIRECTION (\S+) \+ USE (\S+)")
layer_re = re.compile(r" *\+ LAYER (\S+) \( (\S+) (\S+) \) \( (\S+) (\S+) \)")
placed_re = re.compile(r" *\+ PLACED \( (\S+) (\S+) \) (\S+) ;")
def_pins = {}
die_width = 0
die_height = 0
with open(template_def) as f:
for line in f:
if line.startswith("DIEAREA "):
match = diearea_re.match(line)
lx, by, rx, ty = map(int, match.groups())
if (lx, by) != (0, 0):
raise PrecheckFailure("Wrong die origin in template DEF")
die_width = rx
die_height = ty
elif line.startswith("PINS "):
match = pins_re.match(line)
pin_count = int(match.group(1))
break
for i in range(pin_count):
line = next(f)
match = pin_re.match(line)
pin_name, net_name, direction, use = match.groups()
if pin_name != net_name:
raise PrecheckFailure(
"Inconsistent pin name and net name in template DEF"
)
line = next(f)
if not line.strip().startswith("+ PORT"):
raise PrecheckFailure("Unexpected token in template DEF")
line = next(f)
match = layer_re.match(line)
layer, lx, by, rx, ty = match.groups()
lx, by, rx, ty = map(int, (lx, by, rx, ty))
line = next(f)
match = placed_re.match(line)
ox, oy, direction = match.groups()
ox, oy = map(int, (ox, oy))
if pin_name in def_pins:
raise PrecheckFailure("Duplicate pin in template DEF")
def_pins[pin_name] = (layer, ox + lx, oy + by, ox + rx, oy + ty)
line = next(f)
if not line.startswith("END PINS"):
raise PrecheckFailure("Unexpected token in template DEF")
# parse pins from user lef
origin_re = re.compile(r"ORIGIN (\S+) (\S+) ;")
size_re = re.compile(r"SIZE (\S+) BY (\S+) ;")
layer_re = re.compile(r"LAYER (\S+) ;")
rect_re = re.compile(r"RECT (\S+) (\S+) (\S+) (\S+) ;")
macro_active = False
pins_expected = set(def_pins).union({"VPWR", "VGND"})
pins_seen = set()
current_pin = None
pin_rects = 0
lef_errors = 0
lef_ports = {}
with open(lef) as f:
for line in f:
line = line.strip()
if line.startswith("MACRO "):
if line == "MACRO " + toplevel:
macro_active = True
else:
macro_active = False
elif macro_active:
if line.startswith("ORIGIN "):
match = origin_re.match(line)
lx, by = map(parsefp3, match.groups())
if lx != 0 or by != 0:
raise PrecheckFailure("Wrong die origin in LEF")
elif line.startswith("SIZE "):
match = size_re.match(line)
rx, ty = map(parsefp3, match.groups())
if (rx, ty) != (die_width, die_height):
raise PrecheckFailure(
f"Inconsistent die area between LEF and template DEF: ({rx}, {ty}) != ({die_width}, {die_height})"
)
elif line.startswith("PIN "):
if current_pin is not None:
raise PrecheckFailure("Unexpected token in LEF")
current_pin = line.removeprefix("PIN ")
pins_seen.add(current_pin)
if current_pin not in pins_expected:
logging.error(f"Unexpected pin {current_pin} in {lef}")
lef_errors += 1
pin_rects = 0
elif line == "PORT":
if current_pin is None:
raise PrecheckFailure("Unexpected token in LEF")
line = next(f).strip()
while line.startswith("LAYER "):
match = layer_re.match(line)
layer = match.group(1)
line = next(f).strip()
while line.startswith("RECT "):
match = rect_re.match(line)
lx, by, rx, ty = map(parsefp3, match.groups())
pin_rects += 1
if current_pin not in lef_ports:
lef_ports[current_pin] = []
lef_ports[current_pin].append((layer, lx, by, rx, ty))
line = next(f).strip()
if line != "END":
raise PrecheckFailure("Unexpected token in LEF")
elif current_pin is not None and line.startswith("END " + current_pin):
if pin_rects < 1:
logging.error(f"No ports for pin {current_pin} in {lef}")
lef_errors += 1
current_pin = None
for current_pin in def_pins:
if current_pin not in lef_ports:
logging.error(f"Pin {current_pin} not found in {lef}")
lef_errors += 1
else:
if len(lef_ports[current_pin]) > 1:
logging.error(f"Too many rectangles for pin {current_pin} in {lef}")
lef_errors += 1
lef_layer, *lef_rect = lef_ports[current_pin][0]
def_layer, *def_rect = def_pins[current_pin]
if lef_layer != def_layer:
logging.error(
f"Port {current_pin} on layer {lef_layer} in {lef} but on layer {def_layer} in {template_def}"
)
lef_errors += 1
elif lef_rect != def_rect:
logging.error(
f"Port {current_pin} has different dimensions in {lef} and {template_def}"
)
lef_errors += 1
pin_widths = {}
for current_pin in ("VPWR", "VGND"):
if current_pin not in lef_ports:
logging.error(f"Pin {current_pin} not found in {lef}")
lef_errors += 1
else:
for layer, lx, by, rx, ty in lef_ports[current_pin]:
width, height = rx - lx, ty - by
if current_pin in pin_widths:
if pin_widths[current_pin] != width:
logging.error(
f"Multiple {current_pin} rectangles with different widths in {lef}: {pin_widths[current_pin]/1000} != {width/1000} um"
)
lef_errors += 1
pin_widths[current_pin] = width
if layer != "met4":
logging.error(
f"Port {current_pin} has wrong layer in {lef}: {layer} != met4"
)
lef_errors += 1
if width < 1200:
logging.error(
f"Port {current_pin} has too small width in {lef}: {width/1000} < 1.2 um"
)
lef_errors += 1
if width > 2000:
logging.error(
f"Port {current_pin} has too large width in {lef}: {width/1000} > 2.0 um"
)
lef_errors += 1
if height < die_height * 0.95:
logging.error(
f"Port {current_pin} has too small height in {lef}: {height/1000} < {die_height*0.95/1000} um"
)
lef_errors += 1
if lx < 0 or rx > die_width or by < 0 or ty > die_height:
logging.error(
f"Port {current_pin} not entirely within project area in {lef}"
)
lef_errors += 1
if (
"VPWR" in pin_widths
and "VGND" in pin_widths
and pin_widths["VPWR"] != pin_widths["VGND"]
):
vpwr_width = pin_widths["VPWR"]
vgnd_width = pin_widths["VGND"]
logging.error(
f"VPWR and VGND have different widths in {lef}: {vpwr_width/1000} != {vgnd_width/1000} um"
)
lef_errors += 1
# check gds for the ports being present
lib = gdstk.read_gds(gds)
top = [cell for cell in lib.top_level() if cell.name == toplevel]
if not top:
raise PrecheckFailure("Wrong cell at GDS top-level")
top = top[0]
gds_layers = {
"met1.pin": (68, 16),
"met2.pin": (69, 16),
"met3.pin": (70, 16),
"met4.pin": (71, 16),
}
gds_errors = 0
for current_pin, lef_rects in sorted(lef_ports.items()):
for layer, lx, by, rx, ty in lef_rects:
assert layer + ".pin" in gds_layers, "Unexpected port layer in LEF"
pin_layer = gds_layers[layer + ".pin"]
pin_ok = False
for poly in top.polygons:
poly_layer = (poly.layer, poly.datatype)
if poly.contain_all(
((lx + 1) / 1000, (by + 1) / 1000),
((rx - 1) / 1000, (by + 1) / 1000),
((lx + 1) / 1000, (ty - 1) / 1000),
((rx - 1) / 1000, (ty - 1) / 1000),
):
if poly_layer == pin_layer:
pin_ok = True
if not pin_ok:
logging.error(
f"Port {current_pin} missing from layer {layer}.pin in {gds}"
)
gds_errors += 1
if lef_errors > 0 or gds_errors > 0:
err_list = []
if lef_errors > 0:
err_list.append(f"{lef_errors} LEF error" + ("s" if lef_errors > 1 else ""))
if gds_errors > 0:
err_list.append(f"{gds_errors} GDS error" + ("s" if gds_errors > 1 else ""))
err_desc = " and ".join(err_list)
raise PrecheckFailure(
f"Some ports are missing or have wrong dimensions, see {err_desc} above"
)
+26 -4
View File
@@ -10,7 +10,10 @@ import xml.etree.ElementTree as ET
import gdstk
import klayout.db as pya
import klayout.rdb as rdb
import yaml
from klayout_tools import parse_lyp_layers
from pin_check import pin_check
from precheck_failure import PrecheckFailure
PDK_ROOT = os.getenv("PDK_ROOT")
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
@@ -22,10 +25,6 @@ if not PDK_ROOT:
exit(1)
class PrecheckFailure(Exception):
pass
def has_sky130_devices(gds: str):
for cell_name in gdstk.read_rawcells(gds):
if cell_name.startswith("sky130_fd_"):
@@ -131,6 +130,8 @@ def klayout_checks(gds: str):
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--gds", required=True)
parser.add_argument("--lef", required=False)
parser.add_argument("--template-def", required=False)
parser.add_argument("--top-module", required=False)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
@@ -141,6 +142,26 @@ def main():
else:
top_module = os.path.splitext(os.path.basename(args.gds))[0]
if args.lef:
lef = args.lef
else:
lef = os.path.splitext(args.gds)[0] + ".lef"
if args.template_def:
template_def = args.template_def
else:
yaml_dir = os.path.dirname(os.path.dirname(args.gds))
yaml_file = f"{yaml_dir}/info.yaml"
yaml_data = yaml.safe_load(open(yaml_file))
logging.info("info.yaml data:" + str(yaml_data))
tiles = yaml_data.get("project", {}).get("tiles", "1x1")
is_analog = yaml_data.get("project", {}).get("analog_pins", 0) > 0
if is_analog:
template_def = f"../def/analog/tt_block_{tiles}_pg_ana.def"
else:
template_def = f"../def/tt_block_{tiles}_pg.def"
logging.info(f"using def template {template_def}")
checks = [
["Magic DRC", lambda: magic_drc(args.gds, top_module)],
["KLayout FEOL", lambda: klayout_drc(args.gds, "feol")],
@@ -156,6 +177,7 @@ def main():
],
["KLayout zero area", lambda: klayout_zero_area(args.gds)],
["KLayout Checks", lambda: klayout_checks(args.gds)],
["Pin check", lambda: pin_check(args.gds, lef, template_def, top_module)],
]
testsuite = ET.Element("testsuite", name="Tiny Tapeout Prechecks")
+2
View File
@@ -0,0 +1,2 @@
class PrecheckFailure(Exception):
pass
+1
View File
@@ -1,3 +1,4 @@
klayout==0.29.0
gdstk==0.9.50
pytest==7.4.3
PyYAML==6.0.1
+122
View File
@@ -1,4 +1,6 @@
import os
import subprocess
import textwrap
import klayout.db as pya
import klayout_tools
@@ -105,6 +107,102 @@ def gds_invalid_macro_name(tmp_path_factory: pytest.TempPathFactory):
return str(gds_file)
def generate_analog_example(
tcl_file: str,
gds_file: str,
lef_file: str,
toplevel: str,
vpwr_layer: str,
vpwr_box: str,
vgnd_layer: str,
vgnd_box: str,
):
with open(tcl_file, "w") as f:
f.write(
textwrap.dedent(
f"""
def read ../def/analog/tt_block_1x2_pg_ana.def
cellname rename tt_um_template {toplevel}
# VPWR
box {vpwr_box}
paint {vpwr_layer}
label VPWR FreeSans {vpwr_layer}
port VPWR make n
port VPWR use power
port VPWR class bidirectional
port conn n s e w
# VGND
box {vgnd_box}
paint {vgnd_layer}
label VGND FreeSans {vgnd_layer}
port VGND make n
port VGND use ground
port VGND class bidirectional
port conn n s e w
# Export
gds write {gds_file}
lef write {lef_file} -pinonly
"""
)
)
magic = subprocess.run(
[
"magic",
"-noconsole",
"-dnull",
"-rcfile",
f"{PDK_ROOT}/{PDK_NAME}/libs.tech/magic/{PDK_NAME}.magicrc",
tcl_file,
],
)
assert magic.returncode == 0
@pytest.fixture(scope="session")
def gds_lef_analog_example(tmp_path_factory: pytest.TempPathFactory):
"""Creates a GDS and LEF using the 1x2 analog template."""
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_example.tcl"
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_example.gds"
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_example.lef"
generate_analog_example(
str(tcl_file),
str(gds_file),
str(lef_file),
"TEST_analog_example",
"met4",
"100 500 250 22076",
"met4",
"4900 500 5050 22076",
)
return str(gds_file), str(lef_file)
@pytest.fixture(scope="session")
def gds_lef_analog_wrong_vgnd(tmp_path_factory: pytest.TempPathFactory):
"""Creates a GDS and LEF using the 1x2 analog template, with wrong VGND layer & dimensions."""
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_wrong_vgnd.tcl"
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_wrong_vgnd.gds"
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_wrong_vgnd.lef"
generate_analog_example(
str(tcl_file),
str(gds_file),
str(lef_file),
"TEST_analog_wrong_vgnd",
"met4",
"100 500 250 22076",
"met3",
"4900 500 5250 12076",
)
return str(gds_file), str(lef_file)
def test_magic_drc_pass(gds_valid: str):
precheck.magic_drc(gds_valid, "TEST_valid")
@@ -166,3 +264,27 @@ def test_klayout_zero_area_drc_pass(gds_valid: str):
def test_klayout_zero_area_drc_fail(gds_zero_area: str):
with pytest.raises(precheck.PrecheckFailure, match="Klayout zero_area failed"):
precheck.klayout_zero_area(gds_zero_area)
def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_example
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_block_1x2_pg_ana.def",
"TEST_analog_example",
)
def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]):
with pytest.raises(
precheck.PrecheckFailure,
match="Some ports are missing or have wrong dimensions",
):
gds_file, lef_file = gds_lef_analog_wrong_vgnd
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_block_1x2_pg_ana.def",
"TEST_analog_wrong_vgnd",
)