feat(precheck): add support for 3v3 templates

This commit is contained in:
htfab
2024-06-24 07:13:00 +03:00
committed by Uri Shaked
parent 674fdc7ef7
commit 8e41bfb1d9
5 changed files with 244 additions and 89 deletions
+28 -28
View File
@@ -98,8 +98,13 @@ def parsefp3(value: str):
return int(mul) return int(mul)
def pin_check(gds: str, lef: str, template_def: str, toplevel: str): def pin_check(gds: str, lef: str, template_def: str, toplevel: str, uses_3v3: bool):
logging.info("Running pin check...") logging.info("Running pin check...")
logging.info(f"* gds: {gds}")
logging.info(f"* lef: {lef}")
logging.info(f"* template_def: {template_def}")
logging.info(f"* toplevel: {toplevel}")
logging.info(f"* uses_3v3: {uses_3v3}")
# parse pins from template def # parse pins from template def
# def syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/DEFSyntax.html # def syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/DEFSyntax.html
@@ -174,8 +179,13 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str):
layer_re = re.compile(r"LAYER (\S+) ;") layer_re = re.compile(r"LAYER (\S+) ;")
rect_re = re.compile(r"RECT (\S+) (\S+) (\S+) (\S+) ;") rect_re = re.compile(r"RECT (\S+) (\S+) (\S+) (\S+) ;")
power_pins = ["VGND", "VDPWR"]
if uses_3v3:
power_pins.append("VAPWR")
compat_pins = {"VPWR": "VDPWR"}
macro_active = False macro_active = False
pins_expected = set(def_pins).union({"VPWR", "VGND"}) pins_expected = set(def_pins).union(power_pins).union(compat_pins)
pins_seen = set() pins_seen = set()
current_pin = None current_pin = None
pin_rects = 0 pin_rects = 0
@@ -259,6 +269,16 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str):
ports.append((layer, lx, by, rx, ty)) ports.append((layer, lx, by, rx, ty))
lef_ports[current_pin] = ports lef_ports[current_pin] = ports
for old_pin, new_pin in compat_pins.items():
if old_pin in lef_ports:
if new_pin in lef_ports:
logging.error(f"Both {old_pin} and {new_pin} ports appear in {lef}")
lef_errors += 1
else:
lef_ports[new_pin] = []
lef_ports[new_pin].extend(lef_ports[old_pin])
del lef_ports[old_pin]
for current_pin in def_pins: for current_pin in def_pins:
if current_pin not in lef_ports: if current_pin not in lef_ports:
logging.error(f"Pin {current_pin} not found in {lef}") logging.error(f"Pin {current_pin} not found in {lef}")
@@ -280,21 +300,13 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str):
) )
lef_errors += 1 lef_errors += 1
pin_widths = {} for current_pin in power_pins:
for current_pin in ("VPWR", "VGND"):
if current_pin not in lef_ports: if current_pin not in lef_ports:
logging.error(f"Pin {current_pin} not found in {lef}") logging.error(f"Pin {current_pin} not found in {lef}")
lef_errors += 1 lef_errors += 1
else: else:
for layer, lx, by, rx, ty in lef_ports[current_pin]: for layer, lx, by, rx, ty in lef_ports[current_pin]:
width, height = rx - lx, ty - by width = rx - lx
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": if layer != "met4":
logging.error( logging.error(
f"Port {current_pin} has wrong layer in {lef}: {layer} != met4" f"Port {current_pin} has wrong layer in {lef}: {layer} != met4"
@@ -305,14 +317,14 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str):
f"Port {current_pin} has too small width in {lef}: {width/1000} < 1.2 um" f"Port {current_pin} has too small width in {lef}: {width/1000} < 1.2 um"
) )
lef_errors += 1 lef_errors += 1
if width > 2000: if by > 10000:
logging.error( logging.error(
f"Port {current_pin} has too large width in {lef}: {width/1000} > 2.0 um" f"Port {current_pin} is too far from bottom edge of module: {by/1000} > 10 um"
) )
lef_errors += 1 lef_errors += 1
if height < die_height * 0.95: if die_height - ty > 10000:
logging.error( logging.error(
f"Port {current_pin} has too small height in {lef}: {height/1000} < {die_height*0.95/1000} um" f"Port {current_pin} is too far from top edge of module: {(die_height-ty)/1000} > 10 um"
) )
lef_errors += 1 lef_errors += 1
if lx < 0 or rx > die_width or by < 0 or ty > die_height: if lx < 0 or rx > die_width or by < 0 or ty > die_height:
@@ -321,18 +333,6 @@ def pin_check(gds: str, lef: str, template_def: str, toplevel: str):
) )
lef_errors += 1 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 for overlapping pins # check for overlapping pins
for (pin1, rects1), (pin2, rects2) in combinations(sorted(lef_ports.items()), 2): for (pin1, rects1), (pin2, rects2) in combinations(sorted(lef_ports.items()), 2):
+13 -2
View File
@@ -160,8 +160,16 @@ def main():
logging.info("info.yaml data:" + str(yaml_data)) logging.info("info.yaml data:" + str(yaml_data))
tiles = yaml_data.get("project", {}).get("tiles", "1x1") tiles = yaml_data.get("project", {}).get("tiles", "1x1")
is_analog = yaml_data.get("project", {}).get("analog_pins", 0) > 0 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 is_analog:
template_def = f"../def/analog/tt_analog_{tiles}.def" 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/tt_block_{tiles}_pg.def"
logging.info(f"using def template {template_def}") logging.info(f"using def template {template_def}")
@@ -181,7 +189,10 @@ def main():
], ],
["KLayout zero area", lambda: klayout_zero_area(args.gds)], ["KLayout zero area", lambda: klayout_zero_area(args.gds)],
["KLayout Checks", lambda: klayout_checks(args.gds)], ["KLayout Checks", lambda: klayout_checks(args.gds)],
["Pin check", lambda: pin_check(args.gds, lef, template_def, top_module)], [
"Pin check",
lambda: pin_check(args.gds, lef, template_def, top_module, uses_3v3),
],
] ]
testsuite = ET.Element("testsuite", name="Tiny Tapeout Prechecks") testsuite = ET.Element("testsuite", name="Tiny Tapeout Prechecks")
+174 -52
View File
@@ -107,50 +107,69 @@ def gds_invalid_macro_name(tmp_path_factory: pytest.TempPathFactory):
return str(gds_file) return str(gds_file)
class PortRect:
def __init__(
self, layer: str, bottom_left: tuple[int, int], top_right: tuple[int, int]
):
self.layer = layer
self.lx, self.by = bottom_left
self.rx, self.ty = top_right
class CompoundPort:
def __init__(self, name: str, rects: list[PortRect]):
self.name = name
self.port_use = "ground" if name == "VGND" else "power"
self.port_class = "bidirectional"
self.rects = rects
class SimplePort(CompoundPort):
def __init__(self, name: str, *rect_args: list):
super().__init__(name, [PortRect(*rect_args)])
def generate_analog_example( def generate_analog_example(
tcl_file: str, tcl_file: str,
gds_file: str, gds_file: str,
lef_file: str, lef_file: str,
toplevel: str, toplevel: str,
vpwr_layer: str, extra_ports: list[CompoundPort],
vpwr_boxes: str,
vgnd_layer: str,
vgnd_boxes: str,
): ):
with open(tcl_file, "w") as f: with open(tcl_file, "w") as f:
f.write(
textwrap.dedent( def tcl_append(s):
f.write(textwrap.dedent(s))
tcl_append(
f"""
def read ../def/analog/tt_analog_1x2.def
cellname rename tt_um_template {toplevel}
"""
)
for port in extra_ports:
for rect in port.rects:
tcl_append(
f"""
box {rect.lx} {rect.by} {rect.rx} {rect.ty}
paint {rect.layer}
label {port.name} FreeSans {rect.layer}
"""
)
tcl_append(
f""" f"""
def read ../def/analog/tt_analog_1x2.def port {port.name} makeall n
cellname rename tt_um_template {toplevel} port {port.name} use {port.port_use}
port {port.name} class {port.port_class}
# VPWR
foreach vpwr_box {{ {vpwr_boxes} }} {{
box {{*}}$vpwr_box
paint {vpwr_layer}
label VPWR FreeSans {vpwr_layer}
}}
port VPWR makeall n
port VPWR use power
port VPWR class bidirectional
port conn n s e w port conn n s e w
# VGND
foreach vgnd_box {{ {vgnd_boxes} }} {{
box {{*}}$vgnd_box
paint {vgnd_layer}
label VGND FreeSans {vgnd_layer}
}}
port VGND makeall n
port VGND use ground
port VGND class bidirectional
port conn n s e w
# Export
gds write {gds_file}
lef write {lef_file}
""" """
) )
tcl_append(
f"""
# Export
gds write {gds_file}
lef write {lef_file}
"""
) )
magic = subprocess.run( magic = subprocess.run(
@@ -179,10 +198,30 @@ def gds_lef_analog_example(tmp_path_factory: pytest.TempPathFactory):
str(gds_file), str(gds_file),
str(lef_file), str(lef_file),
"TEST_analog_example", "TEST_analog_example",
"met4", [
"{100 500 250 22076}", SimplePort("VDPWR", "met4", (100, 500), (250, 22076)),
"met4", SimplePort("VGND", "met4", (4900, 500), (5050, 22076)),
"{4900 500 5050 22076}", ],
)
return str(gds_file), str(lef_file)
@pytest.fixture(scope="session")
def gds_lef_analog_power_compat(tmp_path_factory: pytest.TempPathFactory):
"""Creates a GDS and LEF using the 1x2 analog template, with VPWR instead of VDPWR."""
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_power_compat.tcl"
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_power_compat.gds"
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_power_compat.lef"
generate_analog_example(
str(tcl_file),
str(gds_file),
str(lef_file),
"TEST_analog_power_compat",
[
SimplePort("VPWR", "met4", (100, 500), (250, 22076)),
SimplePort("VGND", "met4", (4900, 500), (5050, 22076)),
],
) )
return str(gds_file), str(lef_file) return str(gds_file), str(lef_file)
@@ -199,10 +238,10 @@ def gds_lef_analog_wrong_vgnd(tmp_path_factory: pytest.TempPathFactory):
str(gds_file), str(gds_file),
str(lef_file), str(lef_file),
"TEST_analog_wrong_vgnd", "TEST_analog_wrong_vgnd",
"met4", [
"{100 500 250 22076}", SimplePort("VDPWR", "met4", (100, 500), (250, 22076)),
"met3", SimplePort("VGND", "met3", (4900, 500), (5250, 12076)),
"{4900 500 5250 12076}", ],
) )
return str(gds_file), str(lef_file) return str(gds_file), str(lef_file)
@@ -219,10 +258,10 @@ def gds_lef_analog_overlapping_vgnd(tmp_path_factory: pytest.TempPathFactory):
str(gds_file), str(gds_file),
str(lef_file), str(lef_file),
"TEST_analog_overlapping_vgnd", "TEST_analog_overlapping_vgnd",
"met4", [
"{100 500 250 22076}", SimplePort("VDPWR", "met4", (100, 500), (250, 22076)),
"met4", SimplePort("VGND", "met4", (3000, 20), (3200, 22504)),
"{349 20 549 22504}", ],
) )
return str(gds_file), str(lef_file) return str(gds_file), str(lef_file)
@@ -230,19 +269,46 @@ def gds_lef_analog_overlapping_vgnd(tmp_path_factory: pytest.TempPathFactory):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def gds_lef_analog_compound_vgnd(tmp_path_factory: pytest.TempPathFactory): def gds_lef_analog_compound_vgnd(tmp_path_factory: pytest.TempPathFactory):
"""Creates a GDS and LEF using the 1x2 analog template, with VGND consisting of two rectangles.""" """Creates a GDS and LEF using the 1x2 analog template, with VGND consisting of two rectangles."""
tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_example.tcl" tcl_file = tmp_path_factory.mktemp("tcl") / "TEST_analog_compound_vgnd.tcl"
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_example.gds" gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_compound_vgnd.gds"
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_example.lef" lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_compound_vgnd.lef"
generate_analog_example( generate_analog_example(
str(tcl_file), str(tcl_file),
str(gds_file), str(gds_file),
str(lef_file), str(lef_file),
"TEST_analog_compound_vgnd", "TEST_analog_compound_vgnd",
"met4", [
"{100 500 250 22076}", SimplePort("VDPWR", "met4", (100, 500), (250, 22076)),
"met4", CompoundPort(
"{4900 500 5050 12076} {4900 12000 5050 22076}", "VGND",
[
PortRect("met4", (4900, 500), (5050, 12076)),
PortRect("met4", (4900, 12000), (5050, 22076)),
],
),
],
)
return str(gds_file), str(lef_file)
@pytest.fixture(scope="session")
def gds_lef_analog_example_3v3(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_3v3.tcl"
gds_file = tmp_path_factory.mktemp("gds") / "TEST_analog_example_3v3.gds"
lef_file = tmp_path_factory.mktemp("lef") / "TEST_analog_example_3v3.lef"
generate_analog_example(
str(tcl_file),
str(gds_file),
str(lef_file),
"TEST_analog_example_3v3",
[
SimplePort("VDPWR", "met4", (100, 500), (250, 22076)),
SimplePort("VAPWR", "met4", (2500, 500), (2650, 22076)),
SimplePort("VGND", "met4", (4900, 500), (5050, 22076)),
],
) )
return str(gds_file), str(lef_file) return str(gds_file), str(lef_file)
@@ -317,6 +383,18 @@ def test_pin_analog_example(gds_lef_analog_example: tuple[str, str]):
lef_file, lef_file,
"../def/analog/tt_analog_1x2.def", "../def/analog/tt_analog_1x2.def",
"TEST_analog_example", "TEST_analog_example",
False,
)
def test_pin_analog_power_compat(gds_lef_analog_power_compat: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_power_compat
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"TEST_analog_power_compat",
False,
) )
@@ -331,6 +409,7 @@ def test_pin_analog_wrong_vgnd(gds_lef_analog_wrong_vgnd: tuple[str, str]):
lef_file, lef_file,
"../def/analog/tt_analog_1x2.def", "../def/analog/tt_analog_1x2.def",
"TEST_analog_wrong_vgnd", "TEST_analog_wrong_vgnd",
False,
) )
@@ -345,6 +424,7 @@ def test_pin_analog_overlapping_vgnd(gds_lef_analog_overlapping_vgnd: tuple[str,
lef_file, lef_file,
"../def/analog/tt_analog_1x2.def", "../def/analog/tt_analog_1x2.def",
"TEST_analog_overlapping_vgnd", "TEST_analog_overlapping_vgnd",
False,
) )
@@ -355,4 +435,46 @@ def test_pin_analog_compound_vgnd(gds_lef_analog_compound_vgnd: tuple[str, str])
lef_file, lef_file,
"../def/analog/tt_analog_1x2.def", "../def/analog/tt_analog_1x2.def",
"TEST_analog_compound_vgnd", "TEST_analog_compound_vgnd",
False,
) )
def test_pin_analog_example_3v3(gds_lef_analog_example_3v3: tuple[str, str]):
gds_file, lef_file = gds_lef_analog_example_3v3
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"TEST_analog_example_3v3",
True,
)
def test_pin_analog_3v3_mismatch1(gds_lef_analog_example: tuple[str, str]):
with pytest.raises(
precheck.PrecheckFailure,
match="Some ports are missing or have wrong dimensions",
):
gds_file, lef_file = gds_lef_analog_example
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"TEST_analog_example",
True,
)
def test_pin_analog_3v3_mismatch2(gds_lef_analog_example_3v3: tuple[str, str]):
with pytest.raises(
precheck.PrecheckFailure,
match="Some ports are missing or have wrong dimensions",
):
gds_file, lef_file = gds_lef_analog_example_3v3
precheck.pin_check(
gds_file,
lef_file,
"../def/analog/tt_analog_1x2.def",
"TEST_analog_example_3v3",
False,
)
+22 -7
View File
@@ -187,6 +187,19 @@ class Project:
os.unlink(json_file) os.unlink(json_file)
module_ports = ports["modules"][top]["ports"] module_ports = ports["modules"][top]["ports"]
if "VPWR" in module_ports:
if "VDPWR" in module_ports:
logging.error(
f"{self} top module '{top}' uses both VPWR and VDPWR ports"
)
exit(1)
else:
logging.info(
f"{self} top module '{top}' uses VPWR port, substituting VDPWR"
)
module_ports["VDPWR"] = module_ports["VPWR"]
del module_ports["VPWR"]
required_ports = [ required_ports = [
["input", "clk", 1], ["input", "clk", 1],
["input", "ena", 1], ["input", "ena", 1],
@@ -197,15 +210,19 @@ class Project:
["output", "uio_out", 8], ["output", "uio_out", 8],
["output", "uo_out", 8], ["output", "uo_out", 8],
] ]
if self.is_analog_design(): if self.info.is_analog:
required_ports += [ required_ports += [
["inout", "ua", 8], ["inout", "ua", 8],
] ]
if include_power_ports: if include_power_ports:
required_ports += [ required_ports += [
["input", "VGND", 1], ["input", "VGND", 1],
["input", "VPWR", 1], ["input", "VDPWR", 1],
] ]
if self.info.uses_3v3:
required_ports += [
["input", "VAPWR", 1],
]
for direction, port, bits in required_ports: for direction, port, bits in required_ports:
if port not in module_ports: if port not in module_ports:
logging.error(f"{self} port '{port}' missing from top module ('{top}')") logging.error(f"{self} port '{port}' missing from top module ('{top}')")
@@ -248,7 +265,7 @@ class Project:
def get_project_type_string(self): def get_project_type_string(self):
if self.is_wokwi(): if self.is_wokwi():
return f"[Wokwi]({self.get_wokwi_url()})" return f"[Wokwi]({self.get_wokwi_url()})"
elif self.is_analog_design(): elif self.info.is_analog:
return "Analog" return "Analog"
else: else:
return "HDL" return "HDL"
@@ -306,9 +323,6 @@ class Project:
commit_info = self.read_commit_info_json() commit_info = self.read_commit_info_json()
return commit_info["workflow_url"] return commit_info["workflow_url"]
def is_analog_design(self) -> bool:
return len(self.info.pinout.ua) > 0
def get_workflow_url(self): def get_workflow_url(self):
GITHUB_SERVER_URL = os.getenv("GITHUB_SERVER_URL") GITHUB_SERVER_URL = os.getenv("GITHUB_SERVER_URL")
GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY") GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY")
@@ -612,7 +626,8 @@ class Project:
} }
for i, desc in enumerate(self.info.pinout.ua) for i, desc in enumerate(self.info.pinout.ua)
], ],
"is_analog": bool(self.info.pinout.ua), "is_analog": self.info.is_analog,
"uses_3v3": self.info.uses_3v3,
} }
) )
+7
View File
@@ -101,6 +101,13 @@ class ProjectInfo:
"Invalid value for 'analog_pins' in 'project' section, must be between 0 and 6" "Invalid value for 'analog_pins' in 'project' section, must be between 0 and 6"
) )
self.analog_pins: int = analog_pins self.analog_pins: int = analog_pins
self.is_analog: bool = self.analog_pins > 0
self.uses_3v3: bool = project_section.get("uses_3v3", False)
if self.uses_3v3 and not self.is_analog:
raise ProjectYamlError(
"Projects with 3v3 power need at least one analog pin"
)
language = project_section.get("language") language = project_section.get("language")
if language is None: if language is None: