| #!/usr/bin/env python3 |
| # Copyright 2020 Efabless Corporation |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """ |
| Reads in a structural verilog containing pads and a LEF file that |
| contains at least those pads and produces a DEF file with the padframe. |
| TODO: |
| core placement |
| config init, |
| external config |
| """ |
| import os |
| import sys |
| import argparse |
| from subprocess import Popen, PIPE |
| |
| import odb |
| |
| # TECH = "" |
| # PDK_ROOT = os.environ["PDK_ROOT"] |
| # os.environ["MAGTYPE"] = "maglef" |
| |
| parser = argparse.ArgumentParser(description="Generates a padframe DEF") |
| |
| # required if -cfg is not specified |
| parser.add_argument( |
| "--verilog-netlist", |
| "-verilog", |
| help="A structural verilog containing the pads (and other user macros)", |
| ) |
| |
| parser.add_argument( |
| "--def-netlist", |
| "-def", |
| help="A DEF file containing the unplaced pads (and other user macros)", |
| ) |
| |
| parser.add_argument("--design", "-d", help="Name of the top-level module") |
| |
| parser.add_argument("--width", help="Width of the die area") |
| |
| parser.add_argument("--height", help="Height of the die area") |
| |
| |
| parser.add_argument( |
| "--output-def", "-o", required=True, help="Name of the output file name" |
| ) |
| |
| parser.add_argument("--padframe-config", "-cfg", help="CFG file -- input to padring") |
| |
| parser.add_argument( |
| "--pad-name-prefixes", |
| "-prefixes", |
| default=["sky130_fd_io", "sky130_ef_io"], |
| help="e.g., sky130_fd_io", |
| ) |
| |
| parser.add_argument( |
| "--init-padframe-config", |
| "-init", |
| action="store_true", |
| default=False, |
| help="Only generate a CFG file to be user edited", |
| ) |
| |
| parser.add_argument( |
| "--working-dir", |
| "-dir", |
| default=".", |
| help="Working directory to create temporary files needed", |
| ) |
| |
| parser.add_argument( |
| "--special-nets", |
| "-special", |
| nargs="+", |
| type=str, |
| default=None, |
| help="Net names to mark as special", |
| ) |
| |
| parser.add_argument( |
| "--lefs", "-l", nargs="+", type=str, default=None, required=True, help="LEF input" |
| ) |
| |
| args = parser.parse_args() |
| |
| verilog_netlist = args.verilog_netlist |
| def_netlist = args.def_netlist |
| design = args.design |
| width = args.width |
| height = args.height |
| config_file_name = args.padframe_config |
| output_file_name = args.output_def |
| init_padframe_config_flag = args.init_padframe_config |
| working_dir = args.working_dir |
| lefs = args.lefs |
| special_nets = args.special_nets |
| pad_name_prefixes = args.pad_name_prefixes |
| |
| working_def = f"{working_dir}/{design}.pf.def" |
| working_cfg = f"{working_dir}/{design}.pf.cfg" |
| |
| for lef in lefs: |
| assert os.path.exists(lef), lef + " doesn't exist" |
| |
| |
| def invoke_padring(config_file_name, output_file_name): |
| print("Invoking padring to generate a padframe") |
| padring_command = [] |
| padring_command.append("padring") |
| for lef in lefs: |
| padring_command.extend(["-L", lef]) |
| padring_command.extend(["--def", output_file_name]) |
| padring_command.append(config_file_name) |
| |
| p = Popen(padring_command, stdout=PIPE, stdin=PIPE, stderr=PIPE, encoding="utf8") |
| |
| output = p.communicate() |
| print("STDERR:") |
| print("\n".join(output[1].splitlines()[-10:])) |
| print("STDOUT:") |
| print(output[0].strip()) |
| |
| print("Padring exit code:", p.returncode) |
| assert p.returncode == 0, p.returncode |
| assert os.path.exists(output_file_name) |
| |
| |
| # hard requirement of a user netlist either as a DEF or verilog |
| # this is to ensure that the padframe will contain all pads in the design |
| # whether the config is autogenerated or user-provided |
| assert ( |
| verilog_netlist is not None or def_netlist is not None |
| ), "One of --verilog_netlist or --def-netlist is required" |
| |
| # Step 1: create an openDB database from the verilog/def using OpenSTA's read_verilog |
| if verilog_netlist is not None: |
| assert ( |
| def_netlist is None |
| ), "Only one of --verilog_netlist or --def-netlist is required" |
| assert design is not None, "--design is required" |
| |
| openroad_script = [] |
| for lef in lefs: |
| openroad_script.append(f"read_lef {lef}") |
| openroad_script.append(f"read_verilog {verilog_netlist}") |
| openroad_script.append(f"link_design {design}") |
| openroad_script.append(f"write_def {working_def}") |
| # openroad_script.append(f"write_db {design}.pf.db") |
| openroad_script.append("exit") |
| |
| p = Popen(["openroad"], stdout=PIPE, stdin=PIPE, stderr=PIPE, encoding="utf8") |
| |
| openroad_script = "\n".join(openroad_script) |
| # print(openroad_script) |
| |
| output = p.communicate(openroad_script) |
| print("STDOUT:") |
| print(output[0].strip()) |
| print("STDERR:") |
| print(output[1].strip()) |
| print("openroad exit code:", p.returncode) |
| assert p.returncode == 0, p.returncode |
| # TODO: check for errors |
| else: |
| assert def_netlist is not None |
| working_def = def_netlist |
| |
| assert os.path.exists(working_def), "DEF file doesn't exist" |
| |
| db_top = odb.dbDatabase.create() |
| # odb.read_db(db_top, f"{design}.pf.db") |
| for lef in lefs: |
| odb.read_lef(db_top, lef) |
| odb.read_def(db_top, working_def) |
| |
| chip_top = db_top.getChip() |
| block_top = chip_top.getBlock() |
| top_design_name = block_top.getName() |
| |
| print("Top-level design name:", top_design_name) |
| |
| |
| ## Step 2: create a simple data structure with pads from the library |
| # types: corner, power, other |
| pads = {} |
| libs = db_top.getLibs() |
| for lib in libs: |
| masters = lib.getMasters() |
| for m in masters: |
| name = m.getName() |
| if m.isPad(): |
| assert any(name.startswith(p) for p in pad_name_prefixes), name |
| print("Found pad:", name) |
| pad_type = m.getType() |
| pads[name] = pad_type |
| if pad_type == "PAD_SPACER": |
| print("Found PAD_SPACER:", name) |
| elif pad_type == "PAD_AREAIO": |
| # using this for special bus fillers... |
| print("Found PAD_AREAIO", name) |
| if m.isEndCap(): |
| # FIXME: regular endcaps |
| assert any(name.startswith(p) for p in pad_name_prefixes), name |
| assert not m.isPad(), name + " is both pad and endcap?" |
| print("Found corner pad:", name) |
| pads[name] = "corner" |
| |
| print() |
| print("The I/O library contains", len(pads), "cells") |
| print() |
| |
| assert len(pads) != 0, "^" |
| |
| ## Step 3: Go over instances in the design and extract the used pads |
| def clean_name(name): |
| return name.replace("\\", "") |
| |
| |
| used_pads = [] |
| used_corner_pads = [] |
| other_instances = [] |
| for inst in block_top.getInsts(): |
| inst_name = inst.getName() |
| master_name = inst.getMaster().getName() |
| if inst.isPad(): |
| assert any(master_name.startswith(p) for p in pad_name_prefixes), master_name |
| print("Found pad instance", inst_name, "of type", master_name) |
| used_pads.append((inst_name, master_name)) |
| elif inst.isEndCap(): |
| # FIXME: regular endcaps |
| assert any(master_name.startswith(p) for p in pad_name_prefixes), master_name |
| print("Found pad instance", inst_name, "of type", master_name) |
| print("Found corner pad instance", inst_name, "of type", master_name) |
| used_corner_pads.append((inst_name, master_name)) |
| else: |
| assert not any( |
| master_name.startswith(p) for p in pad_name_prefixes |
| ), master_name |
| other_instances.append(inst_name) |
| |
| # FIXME: if used_corner_pads aren't supposed to be instantiated |
| assert len(used_corner_pads) == 4, used_corner_pads |
| |
| print() |
| print( |
| "The user design contains", |
| len(used_pads), |
| "pads, 4 corner pads, and", |
| len(other_instances), |
| "other instances", |
| ) |
| print() |
| assert len(used_pads) != 0, "^" |
| |
| ## Step 4: Generate a CFG or verify a user-provided config |
| |
| |
| def chunker(seq, size): |
| chunks = [seq[i::size] for i in range(size)] |
| # sort by type |
| chunks.sort(key=lambda pad_pair: pad_pair[1]) |
| return chunks |
| |
| |
| def diff_lists(l1, l2): |
| return list(list(set(l1) - set(l2)) + list(set(l2) - set(l1))) |
| |
| |
| def generate_cfg(north, east, south, west, corner_pads, width, height): |
| cfg = [] |
| cfg.append(f"AREA {width} {height} ;") |
| cfg.append("") |
| |
| assert len(corner_pads) == 4, corner_pads |
| cfg.append(f"CORNER {corner_pads[0][0]} SW {corner_pads[0][1]} ;") |
| cfg.append(f"CORNER {corner_pads[1][0]} NW {corner_pads[1][1]} ;") |
| cfg.append(f"CORNER {corner_pads[2][0]} NE {corner_pads[2][1]} ;") |
| cfg.append(f"CORNER {corner_pads[3][0]} SE {corner_pads[3][1]} ;") |
| |
| cfg.append("") |
| |
| for pad in north: |
| cfg.append(f"PAD {pad[0]} N {pad[1]} ;") |
| |
| cfg.append("") |
| |
| for pad in east: |
| cfg.append(f"PAD {pad[0]} E {pad[1]} ;") |
| |
| cfg.append("") |
| |
| for pad in south: |
| cfg.append(f"PAD {pad[0]} S {pad[1]} ;") |
| |
| cfg.append("") |
| |
| for pad in west: |
| cfg.append(f"PAD {pad[0]} W {pad[1]} ;") |
| |
| return "\n".join(cfg) |
| |
| |
| if config_file_name is not None: |
| assert os.path.exists(config_file_name), config_file_name + " doesn't exist" |
| with open(config_file_name, "r") as f: |
| lines = f.readlines() |
| user_config_pads = [] |
| for line in lines: |
| if line.startswith("CORNER") or line.startswith("PAD"): |
| tokens = line.split() |
| assert len(tokens) == 5, tokens |
| inst_name, master_name = tokens[1], tokens[3] |
| if ( |
| not pads[master_name] == "PAD_SPACER" |
| and not pads[master_name] == "PAD_AREAIO" |
| ): |
| user_config_pads.append((inst_name, master_name)) |
| elif line.startswith("AREA"): |
| tokens = line.split() |
| assert len(tokens) == 4, tokens |
| width = int(tokens[1]) |
| height = int(tokens[2]) |
| |
| assert sorted(user_config_pads) == sorted(used_pads + used_corner_pads), ( |
| "Mismatch between the provided config and the provided netlist. Diff:", |
| diff_lists(user_config_pads, used_pads + used_corner_pads), |
| ) |
| |
| print("User config verified") |
| working_cfg = config_file_name |
| else: |
| # TODO: get minimum width/height so that --width and --height aren't required |
| assert width is not None, "--width is required" |
| assert height is not None, "--height is required" |
| |
| # auto generate a configuration |
| |
| # TODO: after calssification, center power pads on each side |
| north, east, south, west = chunker(used_pads, 4) |
| |
| with open(working_cfg, "w") as f: |
| f.write(generate_cfg(north, east, south, west, used_corner_pads, width, height)) |
| |
| if not init_padframe_config_flag: |
| invoke_padring(working_cfg, working_def) |
| else: |
| print( |
| "Padframe config generated at", |
| working_cfg, |
| f"Modify it and re-run this program with the '-cfg {working_cfg}' option", |
| ) |
| sys.exit() |
| |
| print("Applying pad placements to the design DEF") |
| |
| db_padframe = odb.dbDatabase.create() |
| for lef in lefs: |
| odb.read_lef(db_padframe, lef) |
| odb.read_def(db_padframe, working_def) |
| |
| chip_padframe = db_padframe.getChip() |
| block_padframe = chip_padframe.getBlock() |
| padframe_design_name = block_padframe.getName() |
| |
| assert padframe_design_name == "PADRING", padframe_design_name |
| |
| print("Padframe design name:", padframe_design_name) |
| |
| # Mark special nets |
| if special_nets is not None: |
| for net in block_top.getNets(): |
| net_name = net.getName() |
| if net_name in special_nets: |
| print("Marking", net_name, "as a special net") |
| net.setSpecial() |
| for iterm in net.getITerms(): |
| iterm.setSpecial() |
| |
| # get minimum width/height (core-bounded) |
| |
| placed_cells_count = 0 |
| created_cells_count = 0 |
| for inst in block_padframe.getInsts(): |
| assert inst.isPad() or inst.isEndCap(), ( |
| inst.getName() + " is neither a pad nor corner pad" |
| ) |
| |
| inst_name = inst.getName() |
| master = inst.getMaster() |
| master_name = master.getName() |
| x, y = inst.getLocation() |
| orient = inst.getOrient() |
| |
| if (inst_name, master_name) in used_pads + used_corner_pads: |
| original_inst = block_top.findInst(inst_name) |
| assert original_inst is not None, "Failed to find " + inst_name |
| assert original_inst.getPlacementStatus() == "NONE", ( |
| inst_name + " is already placed" |
| ) |
| original_inst.setOrient(orient) |
| original_inst.setLocation(x, y) |
| original_inst.setPlacementStatus("FIRM") |
| placed_cells_count += 1 |
| else: |
| # must be a filler cell |
| new_inst = odb.dbInst_create( |
| block_top, db_top.findMaster(master_name), inst_name |
| ) |
| assert new_inst is not None, "Failed to create " + inst_name |
| new_inst.setOrient(orient) |
| new_inst.setLocation(x, y) |
| new_inst.setPlacementStatus("FIRM") |
| created_cells_count += 1 |
| |
| # TODO: place the core macros within the padframe (chip floorplan) |
| for inst in block_top.getInsts(): |
| if inst.isPlaced() or inst.isFixed(): |
| continue |
| print("Placing", inst.getName()) |
| master = inst.getMaster() |
| master_width = master.getWidth() |
| master_height = master.getHeight() |
| print(master_width, master_height) |
| print(width, height) |
| |
| inst.setLocation( |
| width * 1000 // 2 - master_width // 2, height * 1000 // 2 - master_height // 2 |
| ) |
| inst.setPlacementStatus("PLACED") |
| |
| odb.write_def(block_top, output_file_name) |
| print("Done") |