diff --git a/Makefile b/Makefile index dee0106..a13a220 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,10 @@ MAG_MACROS_SRC := $(foreach macro,$(TARGET_MACRO),$(filter %$(macro).mag,$(MAG_S GDS_MACROS_SRC := $(foreach macro,$(TARGET_MACRO),$(filter %$(macro).gds,$(GDS_SOURCES))) LEF_MACROS_SRC := $(foreach macro,$(TARGET_MACRO),$(filter %$(macro).lef,$(LEF_SOURCES))) +# Name of top module +TOP_NAME ?= tt_um_macro_test_wrapper +PURE_ART ?= 1 + ifeq ($(words $(MAKECMDGOALS)),2) # check if we have a mag or GDS for the macro ifeq ($(or $(MAG_MACROS_SRC),$(GDS_MACROS_SRC)),) @@ -91,6 +95,10 @@ tt_harden_top: preproc mkdir -p runs/$(TAGET_MACRO) ln -sf $(TARGET_MACRO)_config.json src/config.json ./tt/tt_tool.py --harden --openlane2 + cp runs/wokwi/final/gds/* gds/final/ + cp runs/wokwi/final/mag/* mag/final/ + cp runs/wokwi/final/lef/* lef/final/ + PURE_ART=$(PURE_ART) TOP_NAME=$(TOP_NAME) MACRO_NAME=$(TARGET_MACRO) magic -noconsole -dnull ./tcl/place_power_pins.tcl tt_render_final: mkdir -p render diff --git a/README.md b/README.md index 4ed0e84..f6504d1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ _Note: Work In Progress_ - GDSPY: `apt install python3-gdspy` - required to add boundary layers to GDS files and to remove output driver cells: - `rsvg-convert`: For SVG based input images - +- `magic` if your design is a pure art design, to manually add the VPWR, VGND nets and remove the output drivers. ## Getting Started @@ -26,23 +26,10 @@ _Note: Work In Progress_ 4. Make a copy of `src/ttlogo_config.json` as `src/_config.json` - Edit the `VERILOG_DEFINES` and `MACROS` inside the config options to specify which macro you would like to place. - If you are using this to add a logo to an existing project, you need to delete the flow in the config, and use the normal classic flow. - -5. Run `make tt_harden_top ` - -DO NOT run this step if your design has existing digital logic, this is for pure art macro designs! - -6. The output of your design should now be in `runs/wokwi/final`, now one last thing needs to be done -before submitting via adding your top-level macro to the `gds/final` folder. The output drivers need to -be removed from the final GDS. This can be done using the following command: - - `./script/gds_rm_outputs.py runs/wokwi/final/gds/tt_um_macro_test_wrapper.gds` - - _Note:_ Replace `tt_um_macro_test_wrapper` with the name of your top-level module. - -7. Now the final design files can be copied to `gds/final` and `lef/final`: - -```bash -cp runs/wokwi/final/gds/tt_um_macro_test_wrapper.gds gds/final -cp runs/wokwi/final/lef/tt_um_macro_test_wrapper.lef lef/final -``` +5. Run `make tt_harden_top `. + - If your design in purely art, and has no logic, please do: `PURE_ART=1 make tt_harden_top ` + - If you need to override the top module name set `TOP_NAME=` as well. +6. The final designs are now made available in {gds,mag,lef}/final. --- diff --git a/gds/final/tt_um_macro_test_wrapper.gds b/gds/final/tt_um_macro_test_wrapper.gds index c44311d..a0e09f7 100644 Binary files a/gds/final/tt_um_macro_test_wrapper.gds and b/gds/final/tt_um_macro_test_wrapper.gds differ diff --git a/lef/final/tt_um_macro_test_wrapper.lef b/lef/final/tt_um_macro_test_wrapper.lef index 9c02d2c..26b6c9c 100644 --- a/lef/final/tt_um_macro_test_wrapper.lef +++ b/lef/final/tt_um_macro_test_wrapper.lef @@ -2,9 +2,9 @@ VERSION 5.7 ; NOWIREEXTENSIONATPIN ON ; DIVIDERCHAR "/" ; BUSBITCHARS "[]" ; -MACRO tt_um_macro_test_wrapper +MACRO mag/final/tt_um_macro_test_wrapper CLASS BLOCK ; - FOREIGN tt_um_macro_test_wrapper ; + FOREIGN mag/final/tt_um_macro_test_wrapper ; ORIGIN 0.000 0.000 ; SIZE 334.880 BY 225.760 ; PIN clk @@ -351,22 +351,26 @@ MACRO tt_um_macro_test_wrapper RECT 74.830 224.760 75.130 225.760 ; END END uo_out[7] + PIN VPWR + DIRECTION INOUT ; + USE POWER ; + PORT + LAYER met4 ; + RECT 1.000 5.000 3.000 220.760 ; + END + END VPWR + PIN VGND + DIRECTION INOUT ; + USE GROUND ; + PORT + LAYER met4 ; + RECT 4.000 5.000 6.000 220.760 ; + END + END VGND OBS - LAYER nwell ; - RECT -0.190 1.305 2.030 2.910 ; - RECT 0.000 0.105 2.030 1.305 ; - RECT 0.000 0.000 0.145 0.105 ; - LAYER pwell ; - RECT 0.145 -0.085 0.315 0.105 ; - LAYER nwell ; - RECT 0.315 0.000 2.030 0.105 ; - LAYER li1 ; - RECT 0.000 -0.085 1.840 2.805 ; - LAYER met1 ; - RECT 0.000 -0.240 1.840 2.960 ; LAYER met2 ; RECT 50.000 50.000 59.800 55.600 ; END -END tt_um_macro_test_wrapper +END mag/final/tt_um_macro_test_wrapper END LIBRARY diff --git a/mag/final/tt_um_macro_test_wrapper.mag b/mag/final/tt_um_macro_test_wrapper.mag new file mode 100644 index 0000000..32bb74b --- /dev/null +++ b/mag/final/tt_um_macro_test_wrapper.mag @@ -0,0 +1,153 @@ +magic +tech sky130A +timestamp 1725862423 +<< checkpaint >> +rect -530 926 1230 22706 +rect 4370 4370 6610 6190 +rect -649 -130 1230 926 +rect -649 -654 833 -130 +<< metal4 >> +rect 3067 22476 3097 22576 +rect 3343 22476 3373 22576 +rect 3619 22476 3649 22576 +rect 3895 22476 3925 22576 +rect 4171 22476 4201 22576 +rect 4447 22476 4477 22576 +rect 4723 22476 4753 22576 +rect 4999 22476 5029 22576 +rect 5275 22476 5305 22576 +rect 5551 22476 5581 22576 +rect 5827 22476 5857 22576 +rect 6103 22476 6133 22576 +rect 6379 22476 6409 22576 +rect 6655 22476 6685 22576 +rect 6931 22476 6961 22576 +rect 7207 22476 7237 22576 +rect 7483 22476 7513 22576 +rect 7759 22476 7789 22576 +rect 8035 22476 8065 22576 +rect 8311 22476 8341 22576 +rect 8587 22476 8617 22576 +rect 8863 22476 8893 22576 +rect 9139 22476 9169 22576 +rect 9415 22476 9445 22576 +rect 9691 22476 9721 22576 +rect 9967 22476 9997 22576 +rect 10243 22476 10273 22576 +rect 10519 22476 10549 22576 +rect 10795 22476 10825 22576 +rect 11071 22476 11101 22576 +rect 11347 22476 11377 22576 +rect 11623 22476 11653 22576 +rect 11899 22476 11929 22576 +rect 12175 22476 12205 22576 +rect 12451 22476 12481 22576 +rect 12727 22476 12757 22576 +rect 13003 22476 13033 22576 +rect 13279 22476 13309 22576 +rect 13555 22476 13585 22576 +rect 13831 22476 13861 22576 +rect 14107 22476 14137 22576 +rect 14383 22476 14413 22576 +rect 14659 22476 14689 22576 +rect 100 500 300 22076 +rect 400 500 600 22076 +use ttlogo um_ttlogo +timestamp 1554431724 +transform 1 0 5000 0 1 5000 +box 0 0 980 560 +<< labels >> +flabel metal4 s 14383 22476 14413 22576 0 FreeSans 240 90 0 0 clk +port 0 nsew signal input +flabel metal4 s 14659 22476 14689 22576 0 FreeSans 240 90 0 0 ena +port 1 nsew signal input +flabel metal4 s 14107 22476 14137 22576 0 FreeSans 240 90 0 0 rst_n +port 2 nsew signal input +flabel metal4 s 13831 22476 13861 22576 0 FreeSans 240 90 0 0 ui_in[0] +port 3 nsew signal input +flabel metal4 s 13555 22476 13585 22576 0 FreeSans 240 90 0 0 ui_in[1] +port 4 nsew signal input +flabel metal4 s 13279 22476 13309 22576 0 FreeSans 240 90 0 0 ui_in[2] +port 5 nsew signal input +flabel metal4 s 13003 22476 13033 22576 0 FreeSans 240 90 0 0 ui_in[3] +port 6 nsew signal input +flabel metal4 s 12727 22476 12757 22576 0 FreeSans 240 90 0 0 ui_in[4] +port 7 nsew signal input +flabel metal4 s 12451 22476 12481 22576 0 FreeSans 240 90 0 0 ui_in[5] +port 8 nsew signal input +flabel metal4 s 12175 22476 12205 22576 0 FreeSans 240 90 0 0 ui_in[6] +port 9 nsew signal input +flabel metal4 s 11899 22476 11929 22576 0 FreeSans 240 90 0 0 ui_in[7] +port 10 nsew signal input +flabel metal4 s 11623 22476 11653 22576 0 FreeSans 240 90 0 0 uio_in[0] +port 11 nsew signal input +flabel metal4 s 11347 22476 11377 22576 0 FreeSans 240 90 0 0 uio_in[1] +port 12 nsew signal input +flabel metal4 s 11071 22476 11101 22576 0 FreeSans 240 90 0 0 uio_in[2] +port 13 nsew signal input +flabel metal4 s 10795 22476 10825 22576 0 FreeSans 240 90 0 0 uio_in[3] +port 14 nsew signal input +flabel metal4 s 10519 22476 10549 22576 0 FreeSans 240 90 0 0 uio_in[4] +port 15 nsew signal input +flabel metal4 s 10243 22476 10273 22576 0 FreeSans 240 90 0 0 uio_in[5] +port 16 nsew signal input +flabel metal4 s 9967 22476 9997 22576 0 FreeSans 240 90 0 0 uio_in[6] +port 17 nsew signal input +flabel metal4 s 9691 22476 9721 22576 0 FreeSans 240 90 0 0 uio_in[7] +port 18 nsew signal input +flabel metal4 s 4999 22476 5029 22576 0 FreeSans 240 90 0 0 uio_oe[0] +port 19 nsew signal output +flabel metal4 s 4723 22476 4753 22576 0 FreeSans 240 90 0 0 uio_oe[1] +port 20 nsew signal output +flabel metal4 s 4447 22476 4477 22576 0 FreeSans 240 90 0 0 uio_oe[2] +port 21 nsew signal output +flabel metal4 s 4171 22476 4201 22576 0 FreeSans 240 90 0 0 uio_oe[3] +port 22 nsew signal output +flabel metal4 s 3895 22476 3925 22576 0 FreeSans 240 90 0 0 uio_oe[4] +port 23 nsew signal output +flabel metal4 s 3619 22476 3649 22576 0 FreeSans 240 90 0 0 uio_oe[5] +port 24 nsew signal output +flabel metal4 s 3343 22476 3373 22576 0 FreeSans 240 90 0 0 uio_oe[6] +port 25 nsew signal output +flabel metal4 s 3067 22476 3097 22576 0 FreeSans 240 90 0 0 uio_oe[7] +port 26 nsew signal output +flabel metal4 s 7207 22476 7237 22576 0 FreeSans 240 90 0 0 uio_out[0] +port 27 nsew signal output +flabel metal4 s 6931 22476 6961 22576 0 FreeSans 240 90 0 0 uio_out[1] +port 28 nsew signal output +flabel metal4 s 6655 22476 6685 22576 0 FreeSans 240 90 0 0 uio_out[2] +port 29 nsew signal output +flabel metal4 s 6379 22476 6409 22576 0 FreeSans 240 90 0 0 uio_out[3] +port 30 nsew signal output +flabel metal4 s 6103 22476 6133 22576 0 FreeSans 240 90 0 0 uio_out[4] +port 31 nsew signal output +flabel metal4 s 5827 22476 5857 22576 0 FreeSans 240 90 0 0 uio_out[5] +port 32 nsew signal output +flabel metal4 s 5551 22476 5581 22576 0 FreeSans 240 90 0 0 uio_out[6] +port 33 nsew signal output +flabel metal4 s 5275 22476 5305 22576 0 FreeSans 240 90 0 0 uio_out[7] +port 34 nsew signal output +flabel metal4 s 9415 22476 9445 22576 0 FreeSans 240 90 0 0 uo_out[0] +port 35 nsew signal output +flabel metal4 s 9139 22476 9169 22576 0 FreeSans 240 90 0 0 uo_out[1] +port 36 nsew signal output +flabel metal4 s 8863 22476 8893 22576 0 FreeSans 240 90 0 0 uo_out[2] +port 37 nsew signal output +flabel metal4 s 8587 22476 8617 22576 0 FreeSans 240 90 0 0 uo_out[3] +port 38 nsew signal output +flabel metal4 s 8311 22476 8341 22576 0 FreeSans 240 90 0 0 uo_out[4] +port 39 nsew signal output +flabel metal4 s 8035 22476 8065 22576 0 FreeSans 240 90 0 0 uo_out[5] +port 40 nsew signal output +flabel metal4 s 7759 22476 7789 22576 0 FreeSans 240 90 0 0 uo_out[6] +port 41 nsew signal output +flabel metal4 s 7483 22476 7513 22576 0 FreeSans 240 90 0 0 uo_out[7] +port 42 nsew signal output +flabel metal4 100 500 300 22076 1 FreeSans 200 0 0 0 VPWR +port 43 nsew power bidirectional +flabel metal4 400 500 600 22076 1 FreeSans 200 0 0 0 VGND +port 44 nsew ground bidirectional +<< properties >> +string FIXED_BBOX 0 0 33488 22576 +string LEFclass BLOCK +<< end >> diff --git a/script/gds_add_pnb.py b/script/gds_add_pnb.py index 1cda49d..bf14826 100755 --- a/script/gds_add_pnb.py +++ b/script/gds_add_pnb.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2024 Dayton Pidhirney import sys, os import gdspy diff --git a/script/gds_rm_outputs.py b/script/gds_rm_outputs.py deleted file mode 100755 index 42a442b..0000000 --- a/script/gds_rm_outputs.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -import sys, os -import gdspy - -BOUNDARY_LAYER = 235 # prBndry -BOUNDARY_DATATYPE = 4 # boundary - -if __name__ == '__main__': - - # Check args - if len(sys.argv) < 2: - print(f"{sys.argv[0]} ") - print("warning: this script modifies the file in-place") - exit(1) - - # Check filepath - filepath: str = sys.argv.pop() - if not os.path.isfile(filepath): - raise FileNotFoundError("unable to locate GDS file specified") - - target_cellname: str = os.path.basename(filepath.split(".")[0]) - - # Load GDS - gdsii = gdspy.GdsLibrary(infile=filepath) - if not target_cellname in gdsii.cells: - raise RuntimeError("cannot locate a cell in the GDS file matching the filename") - - # Delete output driver cells - target_cell: gdspy.Cell = gdsii.cells[target_cellname] - - if "sky130_fd_sc_hd__buf_2" in gdsii.cells: - gdsii.remove(gdsii.cells["sky130_fd_sc_hd__buf_2"]) - print("removing: sky130_fd_sc_hd__buf_2") - - if "sky130_fd_sc_hd__conb_1" in gdsii.cells: - gdsii.remove(gdsii.cells["sky130_fd_sc_hd__conb_1"]) - print("removing: sky130_fd_sc_hd__conb_1") - - print(gdsii.cells) - gdsii.write_gds(filepath) - print("Success") - \ No newline at end of file diff --git a/script/pin_check.py b/script/pin_check.py new file mode 100755 index 0000000..e7148cd --- /dev/null +++ b/script/pin_check.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2024 TinyTapeout & Contributors + +import sys +import logging +import re +from collections.abc import Iterable +from itertools import combinations +from numbers import Real + +import gdstk + +class PrecheckFailure(Exception): + pass + +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 + print(new_multiplicity) + 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 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, uses_3v3: bool): + 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 + # def syntax: https://coriolis.lip6.fr/doc/lefdef/lefdefref/DEFSyntax.html + + 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, 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(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( + f"Unexpected token in template DEF: PINS {pin_count} section does not end after {pin_count} pins" + ) + + # 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_3v3: + power_pins.append("VAPWR") + compat_pins = {"VPWR": "VDPWR"} + + macro_active = False + pins_expected = set(def_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(parsefp3, 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(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: + 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(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: 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()): + print(layer, rects) + 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 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 + + 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 + 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 by > 10000: + logging.error( + f"Port {current_pin} is too far from bottom edge of module: {by/1000} > 10 um" + ) + lef_errors += 1 + if die_height - ty > 10000: + logging.error( + f"Port {current_pin} is too far from top edge of module: {(die_height-ty)/1000} > 10 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 + + # 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_layers = { + "met1.pin": (68, 16), + "met2.pin": (69, 16), + "met3.pin": (70, 16), + "met4.pin": (71, 16), + } + + 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") + + gds_errors = 0 + 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" + ) + +# tt-gds-macro-testing BEGIN +# Added for direct execution +if __name__ == '__main__': + # gds, lef, def, toplevel, uses_3v3 + pin_check(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], False) +# tt-gds-macro-testing END \ No newline at end of file diff --git a/tcl/place_power_pins.tcl b/tcl/place_power_pins.tcl new file mode 100644 index 0000000..f8b2003 --- /dev/null +++ b/tcl/place_power_pins.tcl @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2024 Tiny Tapeout LTD +# Authors: +# - Uri Shaked +# - Dayton Pidhirney + +set POWER_STRIPE_WIDTH 2um; + +# Power stripes: NET name, x position. You can add additional power stripes for each net, as needed. +set POWER_STRIPES { + VPWR 1um + VGND 4um +} + +set TOP_NAME [file tail $::env(TOP_NAME)] +set MACRO_NAME [file tail $::env(MACRO_NAME)] + +# Load top-level mag +load mag/final/${TOP_NAME}.mag +lef read lef/final/${TOP_NAME}.lef + +# Load macro GDS +gds read gds/${MACRO_NAME}.gds + +# If we are pure art, we need to remove the output drivers from the template +if { [info exists ::env(PURE_ART)] && $::env(PURE_ART) != "" } { + # cellname doesn't return bool, so we check the output list form for the + # match as a boolean instead + set cellname_list [cellname list exists sky130_fd_sc_hd__buf_2] + if {[lsearch -exact $cellname_list sky130_fd_sc_hd__buf_2] != -1} { + for {set i 0} {$i < 24} {incr i} { + set formatted_i [format "%02d" $i] + select cell "_$formatted_i\_" + delete + } + select cell TIE_ZERO_zero_ + delete + } +} + +# Draw the power stripes +# -------------------------------- +proc draw_power_stripe {name x} { + global POWER_STRIPE_WIDTH + box $x 5um $x 220.76um + box width $POWER_STRIPE_WIDTH + paint met4 + label $name FreeSans 0.25u -met4 + port make + port use [expr {$name eq "VGND" ? "ground" : "power"}] + port class bidirectional + port connections n s e w +} + +# You can extra power stripes, as you need. +foreach {name x} $POWER_STRIPES { + puts "Drawing power stripe $name at $x" + draw_power_stripe $name $x +} + +# Save the layout and export MAG/GDS/LEF +# ---------------------------------- +save mag/final/${TOP_NAME} +gds write gds/final/${TOP_NAME}.gds +lef write lef/final/${TOP_NAME}.lef -hide -pinonly \ No newline at end of file