Skip to content

Commit

Permalink
feat(precheck): add pin checks
Browse files Browse the repository at this point in the history
Checks for missing or invalid pins, wrong pin dimensions or layers.
  • Loading branch information
htfab committed Apr 30, 2024
1 parent 51e5747 commit 0e2f6b8
Show file tree
Hide file tree
Showing 3 changed files with 340 additions and 0 deletions.
231 changes: 231 additions & 0 deletions precheck/pin_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import gdstk
import logging
import re


class PrecheckFailure(Exception):
pass


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_seen = False
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_seen = True
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}, {ry}) != ({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
lef_ports[current_pin] = (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
elif pin_rects > 1:
logging.error(f'Too many rectangles 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:
lef_layer, *lef_rect = lef_ports[current_pin]
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:
layer, lx, by, rx, ty = lef_ports[current_pin]
width, height = rx-lx, ty-by
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, (layer, lx, by, rx, ty) in sorted(lef_ports.items()):
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')

25 changes: 25 additions & 0 deletions precheck/precheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import time
import traceback
import xml.etree.ElementTree as ET
import yaml

import gdstk
import klayout.db as pya
import klayout.rdb as rdb
from klayout_tools import parse_lyp_layers

from pin_check import pin_check

PDK_ROOT = os.getenv("PDK_ROOT")
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
LYP_FILE = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/{PDK_NAME}.lyp"
Expand Down Expand Up @@ -128,9 +131,13 @@ 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")
Expand All @@ -141,6 +148,23 @@ 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_file = f"{os.path.dirname(args.gds)}/info.yaml"
yaml_data = yaml.safe_load(open(yaml_file))
tiles = yaml_data.get('project', {}).get('tiles', '1x1')
is_analog = yaml_data.get('pinout', {}).get('analog_pins', 0) > 0
if is_analog:
template_def = "../def/analog/tt_block_{tiles}_pg_ana.def"
else:
template_def = "../def/tt_block_{tiles}_pg.def"

checks = [
["Magic DRC", lambda: magic_drc(args.gds, top_module)],
["KLayout FEOL", lambda: klayout_drc(args.gds, "feol")],
Expand All @@ -156,6 +180,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")
Expand Down
84 changes: 84 additions & 0 deletions precheck/test_precheck.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import textwrap

import klayout.db as pya
import klayout_tools
Expand Down Expand Up @@ -105,6 +106,79 @@ 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")

Expand Down Expand Up @@ -166,3 +240,13 @@ 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]):
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')

0 comments on commit 0e2f6b8

Please sign in to comment.