mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
The previous version of the boundary check was a quick heuristic where we compared the bounding box of the design to that of the prBoundary layer. Over time we came across some designs where this is insufficient. The new check compares the bounding box to the correct project size based on the .def template and verifies that there is a rectangle of this exact size in the prBoundary layer at the top level. The patch also abstracts parsing the .def template into a separate function since we now use it in two different checks.
459 lines
18 KiB
Python
459 lines
18 KiB
Python
import logging
|
|
import re
|
|
from collections.abc import Iterable
|
|
from dataclasses import dataclass
|
|
from functools import partial
|
|
from itertools import combinations
|
|
from numbers import Real
|
|
|
|
import gdstk
|
|
from precheck_failure import PrecheckFailure
|
|
from tech_data import power_pins_layer, power_pins_min_width, valid_lef_port_layers
|
|
|
|
|
|
def canonicalize_rectangles(
|
|
rects: Iterable[Iterable[Real]],
|
|
) -> Iterable[Iterable[Real]]:
|
|
# lists all maximal rectangles covered by the union of input rectangles
|
|
|
|
sweep_events = {}
|
|
for lx, by, rx, ty in rects:
|
|
sweep_events[by] = sweep_events.get(by, {})
|
|
sweep_events[by][lx] = sweep_events[by].get(lx, 0) + 1
|
|
sweep_events[by][rx] = sweep_events[by].get(rx, 0) - 1
|
|
sweep_events[ty] = sweep_events.get(ty, {})
|
|
sweep_events[ty][lx] = sweep_events[ty].get(lx, 0) - 1
|
|
sweep_events[ty][rx] = sweep_events[ty].get(rx, 0) + 1
|
|
|
|
closed_rects = []
|
|
open_rects = {}
|
|
cross_section_events = {}
|
|
for y, sweep_line_events in sorted(sweep_events.items()):
|
|
old_multiplicity = 0
|
|
new_multiplicity = 0
|
|
is_covered, covered_intervals, covered_start = False, [], None
|
|
is_removed, removed_intervals, removed_start = False, [], None
|
|
for x in sorted(set(cross_section_events).union(sweep_line_events)):
|
|
old_delta = cross_section_events.get(x, 0)
|
|
new_delta = sweep_line_events.get(x, 0) + old_delta
|
|
old_multiplicity += old_delta
|
|
new_multiplicity += new_delta
|
|
assert old_multiplicity >= 0
|
|
assert new_multiplicity >= 0
|
|
was_covered, was_removed = is_covered, is_removed
|
|
is_covered = new_multiplicity > 0
|
|
is_removed = old_multiplicity > 0 and new_multiplicity == 0
|
|
if was_covered and not is_covered:
|
|
assert covered_start is not None
|
|
covered_intervals.append((covered_start, x))
|
|
covered_start = None
|
|
if was_removed and not is_removed:
|
|
assert removed_start is not None
|
|
removed_intervals.append((removed_start, x))
|
|
removed_start = None
|
|
if is_covered and not was_covered:
|
|
assert covered_start is None
|
|
covered_start = x
|
|
if is_removed and not was_removed:
|
|
assert removed_start is None
|
|
removed_start = x
|
|
|
|
for x, m in sorted(sweep_line_events.items()):
|
|
cross_section_events[x] = cross_section_events.get(x, 0) + m
|
|
if cross_section_events[x] == 0:
|
|
del cross_section_events[x]
|
|
|
|
for (lx, rx), by in open_rects.items():
|
|
closed = False
|
|
for ix, jx in removed_intervals:
|
|
if ix < rx and lx < jx:
|
|
closed = True
|
|
if closed:
|
|
closed_rects.append((lx, by, rx, y))
|
|
|
|
kept_intervals = [
|
|
(b, c)
|
|
for ((a, b), (c, d)) in zip(
|
|
[(None, None)] + removed_intervals, removed_intervals + [(None, None)]
|
|
)
|
|
]
|
|
open_rects_new = {}
|
|
for ix, jx in covered_intervals:
|
|
open_rects_new[(ix, jx)] = y
|
|
for (lx, rx), by in open_rects.items():
|
|
for ix, jx in kept_intervals:
|
|
if (ix is None or ix < rx) and (jx is None or lx < jx):
|
|
clx = lx if ix is None else max(lx, ix)
|
|
crx = rx if jx is None else min(rx, jx)
|
|
open_rects_new[(clx, crx)] = min(
|
|
open_rects_new.get((clx, crx), by), by
|
|
)
|
|
open_rects = open_rects_new
|
|
|
|
return sorted(set(closed_rects))
|
|
|
|
|
|
def parse_fp3(value: str):
|
|
# parse fixed-point numbers with 3 digits after the decimal point
|
|
# e.g. '20.470' to 20470
|
|
if "." in value:
|
|
ip, fp = value.split(".")
|
|
mul = ip + fp[:3].ljust(3, "0")
|
|
else:
|
|
mul = value + "000"
|
|
return int(mul)
|
|
|
|
|
|
def parse_dbu_to_nm(value: str, dbu_per_micron: int):
|
|
# parse a value in database units into nanometers
|
|
assert dbu_per_micron != 0
|
|
val_dbu = int(value)
|
|
assert (val_dbu * 1000) % dbu_per_micron == 0
|
|
return (val_dbu * 1000) // dbu_per_micron
|
|
|
|
|
|
@dataclass
|
|
class DefTemplate:
|
|
die_width: int
|
|
die_height: int
|
|
dbu_per_micron: int
|
|
pins: dict
|
|
|
|
|
|
def parse_def(template_def: str) -> DefTemplate:
|
|
|
|
# parse die size & pins from template def
|
|
# def syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/DEFSyntax.html
|
|
|
|
units_re = re.compile(r"UNITS DISTANCE MICRONS (\S+) ;")
|
|
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+) ;")
|
|
|
|
die_width = 0
|
|
die_height = 0
|
|
dbu_per_micron = 0
|
|
pins = {}
|
|
|
|
with open(template_def) as f:
|
|
for line in f:
|
|
if line.startswith("UNITS "):
|
|
match = units_re.match(line)
|
|
(dbu_per_micron,) = map(int, match.groups())
|
|
parse_nm = partial(parse_dbu_to_nm, dbu_per_micron=dbu_per_micron)
|
|
elif line.startswith("DIEAREA "):
|
|
match = diearea_re.match(line)
|
|
lx, by, rx, ty = map(parse_nm, match.groups())
|
|
if (lx, by) != (0, 0):
|
|
raise PrecheckFailure(
|
|
"Wrong die origin in template DEF, expecting (0, 0)"
|
|
)
|
|
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(
|
|
f"Inconsistent pin name and net name in template DEF: {pin_name} vs {net_name}"
|
|
)
|
|
|
|
line = next(f)
|
|
if not line.strip().startswith("+ PORT"):
|
|
raise PrecheckFailure(
|
|
"Unexpected token in template DEF: PINS not followed by PORT"
|
|
)
|
|
|
|
line = next(f)
|
|
match = layer_re.match(line)
|
|
layer, lx, by, rx, ty = match.groups()
|
|
lx, by, rx, ty = map(parse_nm, (lx, by, rx, ty))
|
|
|
|
line = next(f)
|
|
match = placed_re.match(line)
|
|
ox, oy, direction = match.groups()
|
|
ox, oy = map(parse_nm, (ox, oy))
|
|
|
|
if pin_name in pins:
|
|
raise PrecheckFailure("Duplicate pin in template DEF")
|
|
|
|
pins[pin_name] = (layer, ox + lx, oy + by, ox + rx, oy + ty)
|
|
|
|
line = next(f)
|
|
if not line.startswith("END PINS"):
|
|
raise PrecheckFailure(
|
|
f"Unexpected token in template DEF: PINS {pin_count} section does not end after {pin_count} pins"
|
|
)
|
|
|
|
return DefTemplate(
|
|
die_width=die_width,
|
|
die_height=die_height,
|
|
dbu_per_micron=dbu_per_micron,
|
|
pins=pins,
|
|
)
|
|
|
|
|
|
def pin_check(
|
|
gds: str, lef: str, template_def: str, toplevel: str, uses_vapwr: bool, tech: str
|
|
):
|
|
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_vapwr: {uses_vapwr}")
|
|
|
|
# parse template def
|
|
template = parse_def(template_def)
|
|
|
|
# parse pins from user lef
|
|
# lef syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/LEFSyntax.html
|
|
|
|
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+) ;")
|
|
|
|
power_pins = ["VGND", "VDPWR"]
|
|
if uses_vapwr:
|
|
power_pins.append("VAPWR")
|
|
compat_pins = {"VPWR": "VDPWR"}
|
|
|
|
macro_active = False
|
|
pins_expected = set(template.pins).union(power_pins).union(compat_pins)
|
|
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(parse_fp3, match.groups())
|
|
if lx != 0 or by != 0:
|
|
raise PrecheckFailure(
|
|
"Wrong die origin in LEF, expecting (0, 0)"
|
|
)
|
|
elif line.startswith("SIZE "):
|
|
match = size_re.match(line)
|
|
rx, ty = map(parse_fp3, match.groups())
|
|
if (rx, ty) != (template.die_width, template.die_height):
|
|
raise PrecheckFailure(
|
|
f"Inconsistent die area between LEF and template DEF: ({rx}, {ty}) != ({template.die_width}, {template.die_height})"
|
|
)
|
|
elif line.startswith("PIN "):
|
|
if current_pin is not None:
|
|
new_pin = line.removeprefix("PIN ")
|
|
raise PrecheckFailure(
|
|
f"Unexpected token in LEF: pin {new_pin} starts without ending previous pin {current_pin}"
|
|
)
|
|
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: PORT outside of PIN"
|
|
)
|
|
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(parse_fp3, 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: LAYER within PORT should be followed by RECT or LAYER lines until END of port"
|
|
)
|
|
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
|
|
|
|
lef_ports_orig = lef_ports
|
|
lef_ports = {}
|
|
for current_pin, ports_orig in lef_ports_orig.items():
|
|
ports_by_layers = {}
|
|
for layer, lx, by, rx, ty in ports_orig:
|
|
if layer not in ports_by_layers:
|
|
ports_by_layers[layer] = []
|
|
ports_by_layers[layer].append((lx, by, rx, ty))
|
|
ports = []
|
|
for layer, rects in sorted(ports_by_layers.items()):
|
|
for lx, by, rx, ty in canonicalize_rectangles(rects):
|
|
ports.append((layer, lx, by, rx, ty))
|
|
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 template.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 = template.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
|
|
|
|
for current_pin in power_pins:
|
|
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 = rx - lx
|
|
expected_layer = power_pins_layer[tech]
|
|
if layer != expected_layer:
|
|
logging.error(
|
|
f"Port {current_pin} has wrong layer in {lef}: {layer} != {expected_layer}"
|
|
)
|
|
lef_errors += 1
|
|
expected_width = power_pins_min_width[tech]
|
|
if width < expected_width:
|
|
logging.error(
|
|
f"Port {current_pin} has too small width in {lef}: {width/1000} < {expected_width/1000} um"
|
|
)
|
|
lef_errors += 1
|
|
if by > 10000:
|
|
logging.error(
|
|
f"Port {current_pin} is too far from bottom edge of module: {by/1000} > 10 um"
|
|
)
|
|
lef_errors += 1
|
|
if template.die_height - ty > 10000:
|
|
logging.error(
|
|
f"Port {current_pin} is too far from top edge of module: {(template.die_height-ty)/1000} > 10 um"
|
|
)
|
|
lef_errors += 1
|
|
if (
|
|
lx < 0
|
|
or rx > template.die_width
|
|
or by < 0
|
|
or ty > template.die_height
|
|
):
|
|
logging.error(
|
|
f"Port {current_pin} not entirely within project area in {lef}"
|
|
)
|
|
lef_errors += 1
|
|
|
|
# check for overlapping pins
|
|
|
|
for (pin1, rects1), (pin2, rects2) in combinations(sorted(lef_ports.items()), 2):
|
|
for layer1, lx1, by1, rx1, ty1 in rects1:
|
|
for layer2, lx2, by2, rx2, ty2 in rects2:
|
|
if layer1 != layer2:
|
|
continue
|
|
if rx1 < lx2 or rx2 < lx1:
|
|
continue
|
|
if ty1 < by2 or ty2 < by1:
|
|
continue
|
|
logging.error(
|
|
f"Overlapping pins in {lef}: {pin1} and {pin2}."
|
|
"All exported pins have to be separate, and must not overlap or abut."
|
|
)
|
|
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_errors = 0
|
|
|
|
if tech != "gf180mcuD":
|
|
gds_layers = valid_lef_port_layers[tech]
|
|
gds_layer_lookup = {j: i for i, j in gds_layers.items()}
|
|
polygon_list = {layer: [] for layer in gds_layers}
|
|
for poly in top.polygons:
|
|
poly_layer = (poly.layer, poly.datatype)
|
|
layer_name = gds_layer_lookup.get(poly_layer, None)
|
|
if layer_name is not None:
|
|
polygon_list[layer_name].append(poly)
|
|
merged_layers = {}
|
|
for layer in gds_layers:
|
|
merged_layers[layer] = gdstk.boolean(polygon_list[layer], [], "or")
|
|
|
|
for current_pin, lef_rects in sorted(lef_ports_orig.items()):
|
|
for layer, lx, by, rx, ty in lef_rects:
|
|
if layer + ".pin" not in gds_layers:
|
|
raise PrecheckFailure(
|
|
f"Unexpected port layer in LEF: {current_pin} is on layer {layer}"
|
|
)
|
|
|
|
pin_ok = False
|
|
for poly in merged_layers[layer + ".pin"]:
|
|
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),
|
|
):
|
|
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"
|
|
)
|