mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
feat(precheck): add extra prechecks
- no shapes outside project area - VPWR/VGND present, VAPWR present iff uses_3v3 - no invalid layers - cell names don't have '#' or '/' - urpm to nwell spacing satisfies DRC - analog pins have adjacent metal iff marked as used
This commit is contained in:
+172
-52
@@ -2,6 +2,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -16,6 +17,7 @@ import yaml
|
|||||||
from klayout_tools import parse_lyp_layers
|
from klayout_tools import parse_lyp_layers
|
||||||
from pin_check import pin_check
|
from pin_check import pin_check
|
||||||
from precheck_failure import PrecheckFailure
|
from precheck_failure import PrecheckFailure
|
||||||
|
from tech_data import analog_pin_pos, layer_map, valid_layers
|
||||||
|
|
||||||
PDK_ROOT = os.getenv("PDK_ROOT")
|
PDK_ROOT = os.getenv("PDK_ROOT")
|
||||||
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
|
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
|
||||||
@@ -59,23 +61,18 @@ def magic_drc(gds: str, toplevel: str):
|
|||||||
raise PrecheckFailure("Magic DRC failed")
|
raise PrecheckFailure("Magic DRC failed")
|
||||||
|
|
||||||
|
|
||||||
def klayout_drc(gds: str, check: str, script=f"{PDK_NAME}_mr.drc"):
|
def klayout_drc(gds: str, check: str, script=f"{PDK_NAME}_mr.drc", extra_vars=[]):
|
||||||
logging.info(f"Running klayout {check} on {gds}")
|
logging.info(f"Running klayout {check} on {gds}")
|
||||||
report_file = f"{REPORTS_PATH}/drc_{check}.xml"
|
report_file = f"{REPORTS_PATH}/drc_{check}.xml"
|
||||||
klayout = subprocess.run(
|
script_vars = [
|
||||||
[
|
f"{check}=true",
|
||||||
"klayout",
|
f"input={gds}",
|
||||||
"-b",
|
f"report={report_file}",
|
||||||
"-r",
|
]
|
||||||
f"tech-files/{script}",
|
klayout_args = ["klayout", "-b", "-r", f"tech-files/{script}"]
|
||||||
"-rd",
|
for v in script_vars + extra_vars:
|
||||||
f"{check}=true",
|
klayout_args.extend(["-rd", v])
|
||||||
"-rd",
|
klayout = subprocess.run(klayout_args)
|
||||||
f"input={gds}",
|
|
||||||
"-rd",
|
|
||||||
f"report={report_file}",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
if klayout.returncode != 0:
|
if klayout.returncode != 0:
|
||||||
raise PrecheckFailure(f"Klayout {check} failed")
|
raise PrecheckFailure(f"Klayout {check} failed")
|
||||||
|
|
||||||
@@ -128,12 +125,128 @@ def klayout_checks(gds: str, expected_name: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def boundary_check(gds: str):
|
||||||
|
"""Ensure that there are no shapes outside the project area."""
|
||||||
|
lib = gdstk.read_gds(gds)
|
||||||
|
tops = lib.top_level()
|
||||||
|
if len(tops) != 1:
|
||||||
|
raise PrecheckFailure("GDS top level not unique")
|
||||||
|
top = tops[0]
|
||||||
|
boundary = top.copy("test_boundary")
|
||||||
|
boundary.filter([(235, 4)], False)
|
||||||
|
if top.bounding_box() != boundary.bounding_box():
|
||||||
|
raise PrecheckFailure("Shapes outside project area")
|
||||||
|
|
||||||
|
|
||||||
|
def power_pin_check(verilog: str, lef: str, uses_3v3: bool):
|
||||||
|
"""Ensure that VPWR / VGND are present,
|
||||||
|
and that VAPWR is present if and only if 'uses_3v3' is set."""
|
||||||
|
verilog_s = open(verilog).read().replace("VPWR", "VDPWR")
|
||||||
|
lef_s = open(lef).read().replace("VPWR", "VDPWR")
|
||||||
|
|
||||||
|
# naive but good enough way to ignore comments
|
||||||
|
verilog_s = re.sub("//.*", "", verilog_s)
|
||||||
|
verilog_s = re.sub("/\\*.*\\*/", "", verilog_s, flags=(re.DOTALL | re.MULTILINE))
|
||||||
|
|
||||||
|
for ft, s in (("Verilog", verilog_s), ("LEF", lef_s)):
|
||||||
|
for pwr, ex in (("VGND", True), ("VDPWR", True), ("VAPWR", uses_3v3)):
|
||||||
|
if (pwr in s) and not ex:
|
||||||
|
raise PrecheckFailure(f"{ft} contains {pwr}")
|
||||||
|
if not (pwr in s) and ex:
|
||||||
|
raise PrecheckFailure(f"{ft} doesn't contain {pwr}")
|
||||||
|
|
||||||
|
|
||||||
|
def layer_check(gds: str, tech: str):
|
||||||
|
"""Check that there are no invalid layers in the GDS file."""
|
||||||
|
lib = gdstk.read_gds(gds)
|
||||||
|
layers = lib.layers_and_datatypes().union(lib.layers_and_texttypes())
|
||||||
|
excess = layers - valid_layers[tech]
|
||||||
|
if excess:
|
||||||
|
raise PrecheckFailure(f"Invalid layers in GDS: {excess}")
|
||||||
|
|
||||||
|
|
||||||
|
def cell_name_check(gds: str):
|
||||||
|
"""Check that there are no cell names with '#' or '/' in them."""
|
||||||
|
for cell_name in gdstk.read_rawcells(gds):
|
||||||
|
if "#" in cell_name:
|
||||||
|
raise PrecheckFailure(
|
||||||
|
f"Cell name {cell_name} contains invalid character '#'"
|
||||||
|
)
|
||||||
|
if "/" in cell_name:
|
||||||
|
raise PrecheckFailure(
|
||||||
|
f"Cell_name {cell_name} contains invalid character '/'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def urpm_nwell_check(gds: str, top_module: str):
|
||||||
|
"""Run a DRC check for urpm to nwell spacing."""
|
||||||
|
extra_vars = [f"thr={os.cpu_count()}", f"top_cell={top_module}"]
|
||||||
|
klayout_drc(
|
||||||
|
gds=gds, check="nwell_urpm", script="nwell_urpm.drc", extra_vars=extra_vars
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def analog_pin_check(
|
||||||
|
gds: str, tech: str, is_analog: bool, uses_3v3: bool, analog_pins: int, pinout: dict
|
||||||
|
):
|
||||||
|
"""Check that every analog pin connects to a piece of metal
|
||||||
|
if and only if the pin is used according to info.yaml."""
|
||||||
|
if is_analog:
|
||||||
|
lib = gdstk.read_gds(gds)
|
||||||
|
top = lib.top_level()[0]
|
||||||
|
met4 = top.copy("test_met4")
|
||||||
|
met4.flatten()
|
||||||
|
met4.filter([layer_map[tech]["met4"]], False)
|
||||||
|
via3 = top.copy("test_via3")
|
||||||
|
via3.flatten()
|
||||||
|
via3.filter([layer_map[tech]["via3"]], False)
|
||||||
|
|
||||||
|
for pin in range(8):
|
||||||
|
x = analog_pin_pos[tech](pin, uses_3v3)
|
||||||
|
x1, y1, x2, y2 = x, 0, x + 0.9, 1.0
|
||||||
|
pin_over = gdstk.rectangle((x1, y1), (x2, y2))
|
||||||
|
pin_above = gdstk.rectangle((x1, y2 + 0.1), (x2, y2 + 0.5))
|
||||||
|
pin_below = gdstk.rectangle((x1, y1 - 0.5), (x2, y1 - 0.1))
|
||||||
|
pin_left = gdstk.rectangle((x1 - 0.5, y1), (x1 - 0.1, y2))
|
||||||
|
pin_right = gdstk.rectangle((x2 + 0.1, y1), (x2 + 0.5, y2))
|
||||||
|
|
||||||
|
via3_over = gdstk.boolean(via3.polygons, pin_over, "and")
|
||||||
|
met4_above = gdstk.boolean(met4.polygons, pin_above, "and")
|
||||||
|
met4_below = gdstk.boolean(met4.polygons, pin_below, "and")
|
||||||
|
met4_left = gdstk.boolean(met4.polygons, pin_left, "and")
|
||||||
|
met4_right = gdstk.boolean(met4.polygons, pin_right, "and")
|
||||||
|
|
||||||
|
connected = (
|
||||||
|
bool(via3_over)
|
||||||
|
or bool(met4_above)
|
||||||
|
or bool(met4_below)
|
||||||
|
or bool(met4_left)
|
||||||
|
or bool(met4_right)
|
||||||
|
)
|
||||||
|
expected_pc = pin < analog_pins
|
||||||
|
expected_pd = bool(pinout.get(f"ua[{pin}]", ""))
|
||||||
|
|
||||||
|
if connected and not expected_pc:
|
||||||
|
raise PrecheckFailure(
|
||||||
|
f"Analog pin {pin} connected but `analog_pins` is {analog_pins}"
|
||||||
|
)
|
||||||
|
elif connected and not expected_pd:
|
||||||
|
raise PrecheckFailure(
|
||||||
|
f"Analog pin {pin} connected but `pinout.ua[{pin}]` is falsy"
|
||||||
|
)
|
||||||
|
elif not connected and expected_pc:
|
||||||
|
raise PrecheckFailure(
|
||||||
|
f"Analog pin {pin} not connected but `analog_pins` is {analog_pins}"
|
||||||
|
)
|
||||||
|
elif not connected and expected_pd:
|
||||||
|
raise PrecheckFailure(
|
||||||
|
f"Analog pin {pin} not connected but `pinout.ua[{pin}]` is truthy"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--gds", required=True)
|
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()
|
args = parser.parse_args()
|
||||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||||
logging.info(f"PDK_ROOT: {PDK_ROOT}")
|
logging.info(f"PDK_ROOT: {PDK_ROOT}")
|
||||||
@@ -154,42 +267,38 @@ def main():
|
|||||||
else:
|
else:
|
||||||
raise PrecheckFailure("Layout file extension is neither .gds nor .gds.br")
|
raise PrecheckFailure("Layout file extension is neither .gds nor .gds.br")
|
||||||
|
|
||||||
if args.top_module:
|
yaml_dir = os.path.dirname(args.gds)
|
||||||
top_module = args.top_module
|
while not os.path.exists(f"{yaml_dir}/info.yaml"):
|
||||||
else:
|
yaml_dir = os.path.dirname(yaml_dir)
|
||||||
top_module = os.path.basename(gds_stem)
|
if yaml_dir == "/":
|
||||||
|
raise PrecheckFailure("info.yaml not found")
|
||||||
|
yaml_file = f"{yaml_dir}/info.yaml"
|
||||||
|
yaml_data = yaml.safe_load(open(yaml_file))
|
||||||
|
logging.info("info.yaml data:" + str(yaml_data))
|
||||||
|
|
||||||
if args.lef:
|
wokwi_id = yaml_data["project"].get("wokwi_id", 0)
|
||||||
lef = args.lef
|
top_module = yaml_data["project"].get("top_module", f"tt_um_wokwi_{wokwi_id}")
|
||||||
else:
|
assert top_module == os.path.basename(gds_stem)
|
||||||
lef = gds_stem + ".lef"
|
|
||||||
|
|
||||||
if args.template_def:
|
tiles = yaml_data.get("project", {}).get("tiles", "1x1")
|
||||||
template_def = args.template_def
|
analog_pins = yaml_data.get("project", {}).get("analog_pins", 0)
|
||||||
else:
|
is_analog = analog_pins > 0
|
||||||
yaml_dir = os.path.dirname(args.gds)
|
uses_3v3 = bool(yaml_data.get("project", {}).get("uses_3v3", False))
|
||||||
while not os.path.exists(f"{yaml_dir}/info.yaml"):
|
pinout = yaml_data.get("pinout", {})
|
||||||
yaml_dir = os.path.dirname(yaml_dir)
|
if uses_3v3 and not is_analog:
|
||||||
if yaml_dir == "/":
|
raise PrecheckFailure("Projects with 3v3 power need at least one analog pin")
|
||||||
raise PrecheckFailure("info.yaml not found")
|
if is_analog:
|
||||||
yaml_file = f"{yaml_dir}/info.yaml"
|
if uses_3v3:
|
||||||
yaml_data = yaml.safe_load(open(yaml_file))
|
template_def = f"../def/analog/tt_analog_{tiles}_3v3.def"
|
||||||
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
|
|
||||||
uses_3v3 = bool(yaml_data.get("project", {}).get("uses_3v3", False))
|
|
||||||
if uses_3v3 and not is_analog:
|
|
||||||
raise PrecheckFailure(
|
|
||||||
"Projects with 3v3 power need at least one analog pin"
|
|
||||||
)
|
|
||||||
if is_analog:
|
|
||||||
if uses_3v3:
|
|
||||||
template_def = f"../def/analog/tt_analog_{tiles}_3v3.def"
|
|
||||||
else:
|
|
||||||
template_def = f"../def/analog/tt_analog_{tiles}.def"
|
|
||||||
else:
|
else:
|
||||||
template_def = f"../def/tt_block_{tiles}_pg.def"
|
template_def = f"../def/analog/tt_analog_{tiles}.def"
|
||||||
logging.info(f"using def template {template_def}")
|
else:
|
||||||
|
template_def = f"../def/tt_block_{tiles}_pg.def"
|
||||||
|
logging.info(f"using def template {template_def}")
|
||||||
|
|
||||||
|
lef_file = gds_stem + ".lef"
|
||||||
|
verilog_file = gds_stem + ".v"
|
||||||
|
tech = "sky130"
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
["Magic DRC", lambda: magic_drc(gds_file, top_module)],
|
["Magic DRC", lambda: magic_drc(gds_file, top_module)],
|
||||||
@@ -208,7 +317,18 @@ def main():
|
|||||||
["KLayout Checks", lambda: klayout_checks(gds_file, top_module)],
|
["KLayout Checks", lambda: klayout_checks(gds_file, top_module)],
|
||||||
[
|
[
|
||||||
"Pin check",
|
"Pin check",
|
||||||
lambda: pin_check(gds_file, lef, template_def, top_module, uses_3v3),
|
lambda: pin_check(gds_file, lef_file, template_def, top_module, uses_3v3),
|
||||||
|
],
|
||||||
|
["Boundary check", lambda: boundary_check(gds_file)],
|
||||||
|
["Power pin check", lambda: power_pin_check(verilog_file, lef_file, uses_3v3)],
|
||||||
|
["Layer check", lambda: layer_check(gds_file, tech)],
|
||||||
|
["Cell name check", lambda: cell_name_check(gds_file)],
|
||||||
|
["urpm/nwell check", lambda: urpm_nwell_check(gds_file, top_module)],
|
||||||
|
[
|
||||||
|
"Analog pin check",
|
||||||
|
lambda: analog_pin_check(
|
||||||
|
gds_file, tech, is_analog, uses_3v3, analog_pins, pinout
|
||||||
|
),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ drc_feol.xml
|
|||||||
drc_offgrid.xml
|
drc_offgrid.xml
|
||||||
drc_pin_label_purposes_overlapping_drawing.xml
|
drc_pin_label_purposes_overlapping_drawing.xml
|
||||||
drc_zero_area.xml
|
drc_zero_area.xml
|
||||||
|
drc_nwell_urpm.xml
|
||||||
magic_drc.mag
|
magic_drc.mag
|
||||||
magic_drc.txt
|
magic_drc.txt
|
||||||
results.md
|
results.md
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
sky130A_mr.drc from https://github.com/efabless/mpw_precheck revision 3467a4c8252aa884cddc7d3f73370df9744dd65e
|
sky130A_mr.drc from https://github.com/efabless/mpw_precheck revision 3467a4c8252aa884cddc7d3f73370df9744dd65e
|
||||||
pin_label_purposes_overlapping_drawing.rb.drc from https://github.com/efabless/mpw_precheck revision f6b9c3d3f00694f96dce8444149449b4719180f0
|
pin_label_purposes_overlapping_drawing.rb.drc from https://github.com/efabless/mpw_precheck revision f6b9c3d3f00694f96dce8444149449b4719180f0
|
||||||
zeroarea.rb.drc from https://github.com/efabless/mpw_precheck revision 4fd5283b124e931c9e71219f47270075176f84e2
|
zeroarea.rb.drc from https://github.com/efabless/mpw_precheck revision 4fd5283b124e931c9e71219f47270075176f84e2
|
||||||
|
nwell_urpm.drc from https://open-source-silicon.slack.com/archives/C05FW1VMY5A/p1729112262642699?thread_ts=1728921677.220509&cid=C05FW1VMY5A
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
source($input, $top_cell)
|
||||||
|
report("SKY130 DRC runset", $report)
|
||||||
|
deep
|
||||||
|
threads($thr)
|
||||||
|
verbose(true)
|
||||||
|
|
||||||
|
nwell = polygons(64, 20)
|
||||||
|
urpm = polygons(79, 20)
|
||||||
|
|
||||||
|
nwell.separation(urpm,0.7, euclidian).output(
|
||||||
|
"nwell space to urpm",
|
||||||
|
"nwell space to urpm: min. nwell spacing to urpm 0.7um"
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# fmt: off
|
||||||
|
valid_layers_sky130 = {
|
||||||
|
(10, 0), (11, 0), (11, 20), (11, 44), (17, 0), (18, 20), (20, 0), (21, 0),
|
||||||
|
(22, 0), (22, 20), (22, 21), (22, 22), (22, 23), (22, 24), (23, 0), (23, 28),
|
||||||
|
(25, 0), (25, 42), (25, 43), (25, 44), (26, 20), (26, 21), (26, 22), (27, 0),
|
||||||
|
(28, 0), (28, 28), (29, 20), (29, 21), (29, 22), (30, 0), (31, 20), (31, 21),
|
||||||
|
(31, 22), (32, 0), (33, 24), (33, 42), (33, 43), (33, 44), (34, 0), (34, 28),
|
||||||
|
(35, 0), (36, 0), (36, 28), (37, 0), (38, 20), (38, 21), (38, 22), (39, 0),
|
||||||
|
(40, 0), (41, 0), (41, 28), (43, 0), (44, 0), (44, 5), (44, 16), (44, 20),
|
||||||
|
(44, 42), (44, 43), (45, 20), (45, 21), (45, 22), (46, 0), (48, 0), (49, 0),
|
||||||
|
(50, 0), (51, 0), (51, 28), (53, 42), (53, 43), (53, 44), (56, 0), (56, 28),
|
||||||
|
(58, 0), (59, 0), (59, 28), (61, 20), (62, 20), (62, 21), (62, 22), (62, 24),
|
||||||
|
(64, 5), (64, 13), (64, 14), (64, 16), (64, 18), (64, 20), (64, 44), (64, 59),
|
||||||
|
(65, 4), (65, 5), (65, 6), (65, 8), (65, 13), (65, 14), (65, 16), (65, 20),
|
||||||
|
(65, 23), (65, 41), (65, 44), (65, 48), (65, 60), (66, 4), (66, 5), (66, 9),
|
||||||
|
(66, 13), (66, 14), (66, 15), (66, 16), (66, 20), (66, 23), (66, 25), (66, 41),
|
||||||
|
(66, 44), (66, 58), (66, 60), (66, 83), (67, 4), (67, 5), (67, 10), (67, 13),
|
||||||
|
(67, 14), (67, 15), (67, 16), (67, 20), (67, 23), (67, 25), (67, 41), (67, 44),
|
||||||
|
(67, 48), (67, 60), (68, 4), (68, 5), (68, 10), (68, 13), (68, 14), (68, 15),
|
||||||
|
(68, 16), (68, 20), (68, 23), (68, 25), (68, 32), (68, 33), (68, 34), (68, 35),
|
||||||
|
(68, 36), (68, 37), (68, 38), (68, 39), (68, 41), (68, 44), (68, 48), (68, 58),
|
||||||
|
(68, 60), (68, 88), (68, 89), (68, 90), (68, 91), (68, 92), (68, 93), (69, 4),
|
||||||
|
(69, 5), (69, 10), (69, 13), (69, 14), (69, 15), (69, 16), (69, 20), (69, 23),
|
||||||
|
(69, 25), (69, 32), (69, 33), (69, 34), (69, 35), (69, 36), (69, 37), (69, 38),
|
||||||
|
(69, 39), (69, 41), (69, 44), (69, 48), (69, 58), (69, 60), (69, 88), (69, 89),
|
||||||
|
(69, 90), (69, 91), (69, 92), (69, 93), (70, 4), (70, 5), (70, 10), (70, 13),
|
||||||
|
(70, 14), (70, 15), (70, 16), (70, 17), (70, 20), (70, 23), (70, 25), (70, 32),
|
||||||
|
(70, 33), (70, 34), (70, 35), (70, 36), (70, 37), (70, 38), (70, 39), (70, 41),
|
||||||
|
(70, 44), (70, 48), (70, 60), (70, 88), (70, 89), (70, 90), (70, 91), (70, 92),
|
||||||
|
(70, 93), (71, 4), (71, 5), (71, 10), (71, 13), (71, 14), (71, 15), (71, 16),
|
||||||
|
(71, 17), (71, 20), (71, 23), (71, 25), (71, 32), (71, 33), (71, 34), (71, 35),
|
||||||
|
(71, 36), (71, 37), (71, 38), (71, 39), (71, 41), (71, 44), (71, 48), (71, 60),
|
||||||
|
(71, 88), (71, 89), (71, 90), (71, 91), (71, 92), (71, 93), (72, 4), (72, 5),
|
||||||
|
(72, 10), (72, 13), (72, 14), (72, 15), (72, 16), (72, 17), (72, 20), (72, 23),
|
||||||
|
(72, 25), (72, 32), (72, 33), (72, 34), (72, 35), (72, 36), (72, 37), (72, 38),
|
||||||
|
(72, 39), (72, 88), (72, 89), (72, 90), (72, 91), (72, 92), (72, 93), (74, 5),
|
||||||
|
(74, 13), (74, 14), (74, 15), (74, 16), (74, 20), (74, 21), (74, 22), (74, 88),
|
||||||
|
(74, 89), (74, 90), (74, 91), (74, 92), (74, 93), (75, 20), (76, 5), (76, 16),
|
||||||
|
(76, 20), (76, 44), (77, 20), (78, 44), (79, 20), (80, 20), (81, 1), (81, 2),
|
||||||
|
(81, 3), (81, 4), (81, 6), (81, 7), (81, 8), (81, 10), (81, 11), (81, 12),
|
||||||
|
(81, 13), (81, 14), (81, 15), (81, 17), (81, 19), (81, 20), (81, 23), (81, 27),
|
||||||
|
(81, 50), (81, 51), (81, 52), (81, 53), (81, 54), (81, 57), (81, 60), (81, 63),
|
||||||
|
(81, 79), (81, 81), (81, 101), (81, 125), (82, 5), (82, 20), (82, 24),
|
||||||
|
(82, 25), (82, 26), (82, 27), (82, 28), (82, 44), (82, 59), (82, 64), (83, 44),
|
||||||
|
(84, 23), (84, 44), (85, 44), (86, 20), (87, 42), (87, 43), (87, 44), (88, 0),
|
||||||
|
(88, 44), (89, 32), (89, 33), (89, 34), (89, 35), (89, 36), (89, 37), (89, 38),
|
||||||
|
(89, 39), (89, 44), (90, 4), (90, 20), (91, 44), (92, 44), (93, 0), (93, 44),
|
||||||
|
(94, 0), (94, 20), (95, 20), (96, 0), (96, 20), (96, 21), (96, 22), (96, 44),
|
||||||
|
(97, 0), (97, 42), (97, 43), (97, 44), (98, 0), (98, 42), (98, 43), (98, 44),
|
||||||
|
(99, 0), (100, 0), (101, 0), (101, 42), (101, 43), (101, 44), (104, 42),
|
||||||
|
(104, 43), (104, 44), (105, 20), (105, 21), (105, 22), (105, 42), (105, 43),
|
||||||
|
(105, 44), (105, 52), (106, 42), (106, 43), (106, 44), (107, 20), (107, 21),
|
||||||
|
(107, 22), (107, 24), (108, 20), (108, 21), (108, 22), (109, 42), (109, 43),
|
||||||
|
(109, 44), (110, 20), (110, 21), (110, 22), (112, 4), (112, 20), (112, 21),
|
||||||
|
(112, 22), (112, 42), (112, 43), (115, 42), (115, 43), (115, 44), (117, 4),
|
||||||
|
(117, 20), (117, 21), (117, 22), (122, 5), (122, 16), (124, 40), (125, 20),
|
||||||
|
(125, 44), (127, 21), (127, 22), (201, 20), (235, 0), (235, 4), (235, 250),
|
||||||
|
(235, 252), (236, 0)
|
||||||
|
}
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
# layers used for the analog pin check
|
||||||
|
layer_map_sky130 = {
|
||||||
|
"met4": (71, 20),
|
||||||
|
"via3": (70, 44),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def analog_pin_pos_sky130(pin_number: int, uses_3v3: bool):
|
||||||
|
return 151.81 - 19.32 * pin_number - (15.64 if uses_3v3 else 0)
|
||||||
|
|
||||||
|
|
||||||
|
valid_layers = {"sky130": valid_layers_sky130}
|
||||||
|
layer_map = {"sky130": layer_map_sky130}
|
||||||
|
analog_pin_pos = {"sky130": analog_pin_pos_sky130}
|
||||||
+237
-10
@@ -107,6 +107,114 @@ def gds_invalid_macro_name(tmp_path_factory: pytest.TempPathFactory):
|
|||||||
return str(gds_file)
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_shapes_outside_area(tmp_path_factory: pytest.TempPathFactory):
|
||||||
|
"""Creates a GDS with shapes outside the project area."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "TEST_shapes_outside_area.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
met1_info = gds_layers["met1.drawing"]
|
||||||
|
met1 = layout.layer(met1_info.layer, met1_info.data_type)
|
||||||
|
top_cell = layout.create_cell("TEST_shapes_outside_area")
|
||||||
|
rect = pya.DBox(-1, 0, 0, 1)
|
||||||
|
top_cell.shapes(met1).insert(rect)
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def verilog_lef_wrong_power_pins(tmp_path_factory: pytest.TempPathFactory):
|
||||||
|
"""Creates a Verilog & LEF file with wrong power pins."""
|
||||||
|
verilog_file = tmp_path_factory.mktemp("verilog") / "TEST_wrong_power_pins.v"
|
||||||
|
verilog_data = """
|
||||||
|
`default_nettype none
|
||||||
|
module TEST_wrong_power_pins (
|
||||||
|
input wire VGND,
|
||||||
|
input wire VDPWR,
|
||||||
|
input wire VAPWR
|
||||||
|
);
|
||||||
|
endmodule
|
||||||
|
"""
|
||||||
|
open(verilog_file, "w").write(textwrap.dedent(verilog_data))
|
||||||
|
lef_file = tmp_path_factory.mktemp("lef") / "TEST_wrong_power_pins.lef"
|
||||||
|
lef_data = """
|
||||||
|
VERSION 5.7 ;
|
||||||
|
NOWIREEXTENSIONATPIN ON ;
|
||||||
|
DIVIDERCHAR "/" ;
|
||||||
|
BUSBITCHARS "[]" ;
|
||||||
|
MACRO TEST_wrong_power_pins
|
||||||
|
CLASS BLOCK ;
|
||||||
|
FOREIGN TEST_wrong_power_pins ;
|
||||||
|
ORIGIN 0.000 0.000 ;
|
||||||
|
SIZE 161.000 BY 111.520 ;
|
||||||
|
PIN VGND
|
||||||
|
DIRECTION INOUT ;
|
||||||
|
USE GROUND ;
|
||||||
|
PORT
|
||||||
|
LAYER met4 ;
|
||||||
|
RECT 21.580 2.480 23.180 109.040 ;
|
||||||
|
END
|
||||||
|
END VGND
|
||||||
|
PIN VDPWR
|
||||||
|
DIRECTION INOUT ;
|
||||||
|
USE POWER ;
|
||||||
|
PORT
|
||||||
|
LAYER met4 ;
|
||||||
|
RECT 18.280 2.480 19.880 109.040 ;
|
||||||
|
END
|
||||||
|
END VDPWR
|
||||||
|
"""
|
||||||
|
open(lef_file, "w").write(textwrap.dedent(lef_data))
|
||||||
|
return str(verilog_file), str(lef_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_invalid_layer(tmp_path_factory: pytest.TempPathFactory):
|
||||||
|
"""Creates a GDS with a layer/datatype not defined in sky130."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "TEST_invalid_layer.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
invalid_layer = layout.layer(255, 255)
|
||||||
|
top_cell = layout.create_cell("TEST_invalid_layer")
|
||||||
|
rect = pya.DBox(0, 0, 5, 5)
|
||||||
|
top_cell.shapes(invalid_layer).insert(rect)
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_invalid_cell_name(tmp_path_factory: pytest.TempPathFactory):
|
||||||
|
"""Creates a GDS with a subcell having a '#' in its name."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "TEST_invalid_cell_name.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
top_cell = layout.create_cell("TEST_invalid_cell_name")
|
||||||
|
subcell = layout.create_cell("subcell#")
|
||||||
|
subcell_instance = pya.CellInstArray(subcell, pya.Trans())
|
||||||
|
top_cell.insert(subcell_instance)
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_urpm_nwell_too_close(tmp_path_factory: pytest.TempPathFactory):
|
||||||
|
"""Creates a GDS with a subcell having a '#' in its name."""
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "TEST_urpm_nwell_too_close.gds"
|
||||||
|
layout = pya.Layout()
|
||||||
|
nwell_info = gds_layers["nwell.drawing"]
|
||||||
|
nwell = layout.layer(nwell_info.layer, nwell_info.data_type)
|
||||||
|
urpm = layout.layer(79, 20)
|
||||||
|
# Hardcoding "urpm" layer/datatype because the .lyp file on the PDK version
|
||||||
|
# we use on GitHub Actions doesn't have it yet. Once we upgrade it, we can
|
||||||
|
# replace the line above with:
|
||||||
|
# urpm_info = gds_layers["urpm"]
|
||||||
|
# urpm = layout.layer(urpm_info.layer, urpm_info.data_type)
|
||||||
|
top_cell = layout.create_cell("TEST_urpm_nwell_too_close")
|
||||||
|
nwell_rect = pya.DBox(0, 0, 5, 5)
|
||||||
|
top_cell.shapes(nwell).insert(nwell_rect)
|
||||||
|
urpm_rect = pya.DBox(5, 0, 10, 5)
|
||||||
|
top_cell.shapes(urpm).insert(urpm_rect)
|
||||||
|
layout.write(str(gds_file))
|
||||||
|
return str(gds_file)
|
||||||
|
|
||||||
|
|
||||||
class PortRect:
|
class PortRect:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, layer: str, bottom_left: tuple[int, int], top_right: tuple[int, int]
|
self, layer: str, bottom_left: tuple[int, int], top_right: tuple[int, int]
|
||||||
@@ -153,17 +261,23 @@ def generate_analog_example(
|
|||||||
f"""
|
f"""
|
||||||
box {rect.lx} {rect.by} {rect.rx} {rect.ty}
|
box {rect.lx} {rect.by} {rect.rx} {rect.ty}
|
||||||
paint {rect.layer}
|
paint {rect.layer}
|
||||||
label {port.name} FreeSans {rect.layer}
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
tcl_append(
|
if port.name:
|
||||||
f"""
|
tcl_append(
|
||||||
port {port.name} makeall n
|
f"""
|
||||||
port {port.name} use {port.port_use}
|
label {port.name} FreeSans {rect.layer}
|
||||||
port {port.name} class {port.port_class}
|
"""
|
||||||
port conn n s e w
|
)
|
||||||
"""
|
if port.name:
|
||||||
)
|
tcl_append(
|
||||||
|
f"""
|
||||||
|
port {port.name} makeall n
|
||||||
|
port {port.name} use {port.port_use}
|
||||||
|
port {port.name} class {port.port_class}
|
||||||
|
port conn n s e w
|
||||||
|
"""
|
||||||
|
)
|
||||||
tcl_append(
|
tcl_append(
|
||||||
f"""
|
f"""
|
||||||
# Export
|
# Export
|
||||||
@@ -294,7 +408,7 @@ def gds_lef_analog_compound_vgnd(tmp_path_factory: pytest.TempPathFactory):
|
|||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def gds_lef_analog_example_3v3(tmp_path_factory: pytest.TempPathFactory):
|
def gds_lef_analog_example_3v3(tmp_path_factory: pytest.TempPathFactory):
|
||||||
"""Creates a GDS and LEF using the 1x2 analog template."""
|
"""Creates a GDS and LEF using the 1x2 analog template using 3v3 power."""
|
||||||
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_example_3v3.tcl"
|
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_example_3v3.tcl"
|
||||||
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_example_3v3.gds"
|
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_example_3v3.gds"
|
||||||
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_example_3v3.lef"
|
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_example_3v3.lef"
|
||||||
@@ -313,6 +427,28 @@ def gds_lef_analog_example_3v3(tmp_path_factory: pytest.TempPathFactory):
|
|||||||
return str(gds_file), str(lef_file)
|
return str(gds_file), str(lef_file)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def gds_lef_analog_pin_example(tmp_path_factory: pytest.TempPathFactory):
|
||||||
|
"""Creates a GDS and LEF using the 1x2 analog template with 2 analog pins used."""
|
||||||
|
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_pin_example.tcl"
|
||||||
|
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_pin_example.gds"
|
||||||
|
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_pin_example.lef"
|
||||||
|
|
||||||
|
generate_analog_example(
|
||||||
|
str(tcl_file),
|
||||||
|
str(gds_file),
|
||||||
|
str(lef_file),
|
||||||
|
"TEST_analog_example",
|
||||||
|
[
|
||||||
|
SimplePort("VDPWR", "met4", (100, 500), (250, 22076)),
|
||||||
|
SimplePort("VGND", "met4", (4900, 500), (5050, 22076)),
|
||||||
|
SimplePort("", "via3", (15200, 30), (15250, 80)),
|
||||||
|
SimplePort("", "via3", (13270, 30), (13320, 80)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return str(gds_file), str(lef_file)
|
||||||
|
|
||||||
|
|
||||||
def test_magic_drc_pass(gds_valid: str):
|
def test_magic_drc_pass(gds_valid: str):
|
||||||
precheck.magic_drc(gds_valid, "TEST_valid")
|
precheck.magic_drc(gds_valid, "TEST_valid")
|
||||||
|
|
||||||
@@ -376,6 +512,44 @@ def test_klayout_zero_area_drc_fail(gds_zero_area: str):
|
|||||||
precheck.klayout_zero_area(gds_zero_area)
|
precheck.klayout_zero_area(gds_zero_area)
|
||||||
|
|
||||||
|
|
||||||
|
def test_shapes_outside_area(gds_shapes_outside_area: str):
|
||||||
|
with pytest.raises(precheck.PrecheckFailure, match="Shapes outside project area"):
|
||||||
|
precheck.boundary_check(gds_shapes_outside_area)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrong_power_pins_1(verilog_lef_wrong_power_pins: tuple[str, str]):
|
||||||
|
verilog_file, lef_file = verilog_lef_wrong_power_pins
|
||||||
|
with pytest.raises(precheck.PrecheckFailure, match="Verilog contains VAPWR"):
|
||||||
|
precheck.power_pin_check(verilog_file, lef_file, uses_3v3=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrong_power_pins_2(verilog_lef_wrong_power_pins: tuple[str, str]):
|
||||||
|
verilog_file, lef_file = verilog_lef_wrong_power_pins
|
||||||
|
with pytest.raises(precheck.PrecheckFailure, match="LEF doesn't contain VAPWR"):
|
||||||
|
precheck.power_pin_check(verilog_file, lef_file, uses_3v3=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_layer(gds_invalid_layer: str):
|
||||||
|
with pytest.raises(precheck.PrecheckFailure, match="Invalid layers in GDS"):
|
||||||
|
precheck.layer_check(gds_invalid_layer, "sky130")
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_cell_name(gds_invalid_cell_name: str):
|
||||||
|
with pytest.raises(
|
||||||
|
precheck.PrecheckFailure,
|
||||||
|
match="Cell name subcell# contains invalid character '#'",
|
||||||
|
):
|
||||||
|
precheck.cell_name_check(gds_invalid_cell_name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_urpm_nwell_too_close(gds_urpm_nwell_too_close: str):
|
||||||
|
with pytest.raises(
|
||||||
|
precheck.PrecheckFailure,
|
||||||
|
match="Klayout nwell_urpm failed with 1 DRC violations",
|
||||||
|
):
|
||||||
|
precheck.urpm_nwell_check(gds_urpm_nwell_too_close, "TEST_urpm_nwell_too_close")
|
||||||
|
|
||||||
|
|
||||||
def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]):
|
def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]):
|
||||||
gds_file, lef_file = gds_lef_analog_example
|
gds_file, lef_file = gds_lef_analog_example
|
||||||
precheck.pin_check(
|
precheck.pin_check(
|
||||||
@@ -478,3 +652,56 @@ def test_pin_analog_3v3_mismatch2(gds_lef_analog_example_3v3: tuple[str, str]):
|
|||||||
"TEST_analog_example_3v3",
|
"TEST_analog_example_3v3",
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_analog_exact_pins(gds_lef_analog_pin_example: tuple[str, str]):
|
||||||
|
gds_file, lef_file = gds_lef_analog_pin_example
|
||||||
|
precheck.analog_pin_check(
|
||||||
|
gds_file, "sky130", True, False, 2, {"ua[0]": "x", "ua[1]": "x"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_analog_less_pins(gds_lef_analog_pin_example: tuple[str, str]):
|
||||||
|
gds_file, lef_file = gds_lef_analog_pin_example
|
||||||
|
with pytest.raises(
|
||||||
|
precheck.PrecheckFailure, match="Analog pin 1 connected but `analog_pins` is 1"
|
||||||
|
):
|
||||||
|
precheck.analog_pin_check(
|
||||||
|
gds_file, "sky130", True, False, 1, {"ua[0]": "x", "ua[1]": "x"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_analog_more_pins(gds_lef_analog_pin_example: tuple[str, str]):
|
||||||
|
gds_file, lef_file = gds_lef_analog_pin_example
|
||||||
|
with pytest.raises(
|
||||||
|
precheck.PrecheckFailure,
|
||||||
|
match="Analog pin 2 not connected but `analog_pins` is 3",
|
||||||
|
):
|
||||||
|
precheck.analog_pin_check(
|
||||||
|
gds_file, "sky130", True, False, 3, {"ua[0]": "x", "ua[1]": "x"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_analog_less_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
|
||||||
|
gds_file, lef_file = gds_lef_analog_pin_example
|
||||||
|
with pytest.raises(
|
||||||
|
precheck.PrecheckFailure,
|
||||||
|
match="Analog pin 1 connected but `pinout\\.ua\\[1\\]` is falsy",
|
||||||
|
):
|
||||||
|
precheck.analog_pin_check(gds_file, "sky130", True, False, 2, {"ua[0]": "x"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_analog_more_ua_entries(gds_lef_analog_pin_example: tuple[str, str]):
|
||||||
|
gds_file, lef_file = gds_lef_analog_pin_example
|
||||||
|
with pytest.raises(
|
||||||
|
precheck.PrecheckFailure,
|
||||||
|
match="Analog pin 2 not connected but `pinout\\.ua\\[2\\]` is truthy",
|
||||||
|
):
|
||||||
|
precheck.analog_pin_check(
|
||||||
|
gds_file,
|
||||||
|
"sky130",
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
2,
|
||||||
|
{"ua[0]": "x", "ua[1]": "x", "ua[2]": "x"},
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user