| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| # | |
| # Copyright (C) 2019-2022 F4PGA Authors | |
| # | |
| # 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. | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| """ Convert a PCF file into a VPR io.place file. """ | |
| from __future__ import print_function | |
| import argparse | |
| import sys | |
| import csv | |
| from os import environ | |
| import lxml.etree as ET | |
| import constraint | |
| import prjxray.db | |
| from collections import defaultdict, OrderedDict, namedtuple | |
| from pathlib import Path | |
| from subprocess import run as subprocess_run | |
| from f4pga.utils.eblif import parse_blif | |
| PlaceConstraint = namedtuple("PlaceConstraint", "name x y z comment") | |
| HEADER_TEMPLATE = """\ | |
| #{name:<{nl}} x y z pcf_line | |
| #{s:-^{nl}} -- -- - ----""" | |
| CONSTRAINT_TEMPLATE = "{name:<{nl}} {x: 3} {y: 3} {z: 2} # {comment}" | |
| def get_root_cluster(curr): | |
| while True: | |
| parent = curr.getparent() | |
| if parent is None: | |
| return None | |
| parent_parent = parent.getparent() | |
| if parent_parent is None: | |
| return curr | |
| curr = parent | |
| class PlaceConstraints(object): | |
| def __init__(self, net_file): | |
| self.constraints = OrderedDict() | |
| self.block_to_loc = dict() | |
| net_xml = ET.parse(net_file) | |
| self.net_root = net_xml.getroot() | |
| def load_loc_sites_from_net_file(self): | |
| """ | |
| .place files expect top-level block (cluster) names, not net names, so | |
| build a mapping from net names to block names from the .net file. | |
| """ | |
| self.net_to_block = {} | |
| self.block_to_root_block = {} | |
| for el in self.net_root.iter("block"): | |
| root_block = get_root_cluster(el) | |
| if root_block is not None: | |
| self.block_to_root_block[el.attrib["name"]] = root_block.attrib["name"] | |
| for attr in self.net_root.xpath("//attribute"): | |
| name = attr.attrib["name"] | |
| if name != "LOC": | |
| continue | |
| # Get block name | |
| top_block = attr.getparent() | |
| assert top_block is not None | |
| while top_block.getparent() is not self.net_root: | |
| assert top_block is not None | |
| top_block = top_block.getparent() | |
| self.block_to_loc[top_block.get("name")] = attr.text | |
| def constrain_block(self, block_name, loc, comment=""): | |
| assert len(loc) == 3 | |
| assert block_name not in self.constraints, block_name | |
| place_constraint = PlaceConstraint( | |
| name=block_name, | |
| x=loc[0], | |
| y=loc[1], | |
| z=loc[2], | |
| comment=comment, | |
| ) | |
| root_block = self.block_to_root_block[block_name] | |
| self.constraints[root_block] = place_constraint | |
| def output_place_constraints(self, f): | |
| if not self.constraints: | |
| return | |
| max_name_length = max(len(c.name) for c in self.constraints.values()) | |
| constrained_blocks = {} | |
| for vpr_net, constraint in self.constraints.items(): | |
| name = constraint.name | |
| # This block is already constrained, check if there is no | |
| # conflict there. | |
| if name in constrained_blocks: | |
| existing = constrained_blocks[name] | |
| if existing.x != constraint.x or existing.y != constraint.y or existing.z != constraint.z: | |
| print("Error: block '{}' has multiple conflicting constraints!".format(name)) | |
| print("", constrained_blocks[name]) | |
| print("", constraint) | |
| exit(-1) | |
| # Don't write the second constraing | |
| continue | |
| # omit if no corresponding block name for the net | |
| if name is not None: | |
| print( | |
| CONSTRAINT_TEMPLATE.format( | |
| name=name, | |
| nl=max_name_length, | |
| x=constraint.x, | |
| y=constraint.y, | |
| z=constraint.z, | |
| comment=constraint.comment, | |
| ), | |
| file=f, | |
| ) | |
| # Add to constrained block list | |
| constrained_blocks[name] = constraint | |
| def get_loc_sites(self): | |
| """Yields user-constraints (block, location) pairs""" | |
| if self.block_to_loc is None: | |
| return | |
| for loc in self.block_to_loc: | |
| yield (loc, self.block_to_loc[loc]) | |
| def get_used_instances(self, instance): | |
| """ | |
| Returns a list containing the root clusters of the specified instances in the packed netlist | |
| that are marked as used. | |
| An instance is marked as used when the XML element relative to the instance name has | |
| children tags. | |
| E.g.: | |
| <block name="idelayctrl_block" instance="IDELAYCTRL[0]"> | |
| <inputs> | |
| <port name="REFCLK">refclk</port> | |
| </inputs> | |
| ... | |
| </block> | |
| ... | |
| <block name="open" instance="IDELAYCTRL[0]" /> | |
| """ | |
| instances = list() | |
| for el in self.net_root.iter("block"): | |
| inst = el.attrib["instance"] | |
| if instance in inst: | |
| if len(el.getchildren()) != 0: | |
| instances.append(get_root_cluster(el).attrib["name"]) | |
| return instances | |
| CLOCKS = { | |
| "PLLE2_ADV_VPR": { | |
| "sinks": frozenset(("CLKFBIN", "CLKIN1", "CLKIN2", "DCLK")), | |
| "sources": frozenset(("CLKFBOUT", "CLKOUT0", "CLKOUT1", "CLKOUT2", "CLKOUT3", "CLKOUT4", "CLKOUT5")), | |
| "type": "PLLE2_ADV", | |
| }, | |
| "MMCME2_ADV_VPR": { | |
| "sinks": frozenset(("CLKFBIN", "CLKIN1", "CLKIN2", "DCLK", "PSCLK")), | |
| "sources": frozenset( | |
| ( | |
| "CLKFBOUT", | |
| "CLKFBOUTB", | |
| "CLKOUT0", | |
| "CLKOUT0B", | |
| "CLKOUT1", | |
| "CLKOUT1B", | |
| "CLKOUT2", | |
| "CLKOUT2B", | |
| "CLKOUT3", | |
| "CLKOUT3B", | |
| "CLKOUT4", | |
| "CLKOUT5", | |
| "CLKOUT6", | |
| ) | |
| ), | |
| "type": "MMCME2_ADV", | |
| }, | |
| "BUFGCTRL_VPR": { | |
| "sinks": frozenset(("I0", "I1")), | |
| "sources": frozenset(("O",)), | |
| "type": "BUFGCTRL", | |
| }, | |
| "PS7_VPR": { | |
| "sinks": frozenset( | |
| ( | |
| "DMA0ACLK", | |
| "DMA1ACLK", | |
| "DMA2ACLK", | |
| "DMA3ACLK", | |
| "EMIOENET0GMIIRXCLK", | |
| "EMIOENET0GMIITXCLK", | |
| "EMIOENET1GMIIRXCLK", | |
| "EMIOENET1GMIITXCLK", | |
| "EMIOSDIO0CLKFB", | |
| "EMIOSDIO1CLKFB", | |
| "EMIOSPI0SCLKI", | |
| "EMIOSPI1SCLKI", | |
| "EMIOTRACECLK", | |
| "EMIOTTC0CLKI[0]", | |
| "EMIOTTC0CLKI[1]", | |
| "EMIOTTC0CLKI[2]", | |
| "EMIOTTC1CLKI[0]", | |
| "EMIOTTC1CLKI[1]", | |
| "EMIOTTC1CLKI[2]", | |
| "EMIOWDTCLKI", | |
| "FCLKCLKTRIGN[0]", | |
| "FCLKCLKTRIGN[1]", | |
| "FCLKCLKTRIGN[2]", | |
| "FCLKCLKTRIGN[3]", | |
| "MAXIGP0ACLK", | |
| "MAXIGP1ACLK", | |
| "SAXIACPACLK", | |
| "SAXIGP0ACLK", | |
| "SAXIGP1ACLK", | |
| "SAXIHP0ACLK", | |
| "SAXIHP1ACLK", | |
| "SAXIHP2ACLK", | |
| "SAXIHP3ACLK", | |
| ) | |
| ), | |
| "sources": frozenset( | |
| ( | |
| "FCLKCLK[0]", | |
| "FCLKCLK[1]", | |
| "FCLKCLK[2]", | |
| "FCLKCLK[3]", | |
| "EMIOSDIO0CLK", | |
| "EMIOSDIO1CLK", | |
| # There are also EMIOSPI[01]CLKO and EMIOSPI[01]CLKTN but seem | |
| # to be more of a GPIO outputs than clock sources for the FPGA | |
| # fabric. | |
| ) | |
| ), | |
| "type": "PS7", | |
| }, | |
| "IBUF_VPR": { | |
| "sources": frozenset(("O",)), | |
| "sinks": frozenset(("I",)), | |
| "type": "IBUF", | |
| }, | |
| "IBUFDS_GTE2_VPR": { | |
| "sources": frozenset(("O",)), | |
| "sinks": frozenset(), | |
| "type": "IBUFDS_GTE2", | |
| }, | |
| "GTPE2_COMMON_VPR": { | |
| "sources": frozenset(), | |
| "sinks": frozenset(("GTREFCLK0", "GTREFCLK1")), | |
| "type": "GTPE2_COMMON", | |
| }, | |
| "GTPE2_CHANNEL_VPR": { | |
| "sources": frozenset(("TXOUTCLK", "RXOUTCLK")), | |
| "sinks": frozenset(), | |
| "type": "GTPE2_CHANNEL", | |
| }, | |
| } | |
| GTP_PRIMITIVES = ["IBUFDS_GTE2", "GTPE2_COMMON", "GTPE2_CHANNEL"] | |
| def eprint(*args, **kwargs): | |
| print(*args, file=sys.stderr, **kwargs) | |
| def get_cmt(cmt_dict, loc): | |
| """Returns the clock region of an input location.""" | |
| for k, v in cmt_dict.items(): | |
| for (x, y) in v["vpr_loc"]: | |
| if x == loc[0] and y == loc[1]: | |
| return v["clock_region"] | |
| return None | |
| class VprGrid(object): | |
| """This class contains a set of dictionaries helpful | |
| to have a fast lookup at the various coordinates mapping.""" | |
| def __init__(self, vpr_grid_map, graph_limit): | |
| self.site_dict = dict() | |
| self.site_type_dict = dict() | |
| self.cmt_dict = dict() | |
| self.tile_dict = dict() | |
| self.vpr_loc_cmt = dict() | |
| self.canon_loc = dict() | |
| if graph_limit is not None: | |
| limits = graph_limit.split(",") | |
| xmin, ymin, xmax, ymax = [int(x) for x in limits] | |
| with open(vpr_grid_map, "r") as csv_vpr_grid: | |
| csv_reader = csv.DictReader(csv_vpr_grid) | |
| for row in csv_reader: | |
| site_name = row["site_name"] | |
| site_type = row["site_type"] | |
| phy_tile = row["physical_tile"] | |
| vpr_x = row["vpr_x"] | |
| vpr_y = row["vpr_y"] | |
| can_x = row["canon_x"] | |
| can_y = row["canon_y"] | |
| clk_region = row["clock_region"] | |
| connected_to_site = row["connected_to_site"] | |
| if graph_limit is not None: | |
| if int(can_x) < xmin or int(can_x) > xmax: | |
| continue | |
| if int(can_y) < ymin or int(can_y) > ymax: | |
| continue | |
| clk_region = None if not clk_region else int(clk_region) | |
| # Generating the site dictionary | |
| self.site_dict[site_name] = { | |
| "type": site_type, | |
| "tile": phy_tile, | |
| "vpr_loc": (int(vpr_x), int(vpr_y)), | |
| "canon_loc": (int(can_x), int(can_y)), | |
| "clock_region": clk_region, | |
| "connected_to_site": connected_to_site, | |
| } | |
| # Generating site types dictionary. | |
| if site_type not in self.site_type_dict: | |
| self.site_type_dict[site_type] = [] | |
| self.site_type_dict[site_type].append((site_name, phy_tile, clk_region)) | |
| # Generating the cmt dictionary. | |
| # Each entry has: | |
| # - canonical location | |
| # - a list of vpr coordinates | |
| # - clock region | |
| if phy_tile not in self.cmt_dict: | |
| self.cmt_dict[phy_tile] = { | |
| "canon_loc": (int(can_x), int(can_y)), | |
| "vpr_loc": [(int(vpr_x), int(vpr_y))], | |
| "clock_region": clk_region, | |
| } | |
| else: | |
| self.cmt_dict[phy_tile]["vpr_loc"].append((int(vpr_x), int(vpr_y))) | |
| # Generating the tile dictionary. | |
| # Each tile has a list of (site, site_type) pairs | |
| if phy_tile not in self.tile_dict: | |
| self.tile_dict[phy_tile] = [] | |
| self.tile_dict[phy_tile].append((site_name, site_type)) | |
| self.vpr_loc_cmt[(int(vpr_x), int(vpr_y))] = clk_region | |
| self.canon_loc[(int(vpr_x), int(vpr_y))] = (int(can_x), int(can_y)) | |
| def get_site_dict(self): | |
| return self.site_dict | |
| def get_site_type_dict(self): | |
| return self.site_type_dict | |
| def get_cmt_dict(self): | |
| return self.cmt_dict | |
| def get_tile_dict(self): | |
| return self.tile_dict | |
| def get_vpr_loc_cmt(self): | |
| """ | |
| Returns a dictionary containing the mapping between | |
| VPR physical locations to the belonging clock region. | |
| Dictionary content: | |
| - key : (x, y) coordinate on the VPR grid | |
| - value : clock region corresponding to the (x, y) coordinates | |
| """ | |
| return self.vpr_loc_cmt | |
| def get_canon_loc(self): | |
| return self.canon_loc | |
| class ClockPlacer(object): | |
| def __init__(self, vpr_grid, io_locs, blif_data, roi, graph_limit, allow_bufg_logic_sources=False): | |
| self.roi = roi | |
| self.cmt_to_bufg_tile = {} | |
| self.bufg_from_cmt = { | |
| "TOP": [], | |
| "BOT": [], | |
| } | |
| self.pll_cmts = set() | |
| self.mmcm_cmts = set() | |
| self.gtp_cmts = set() | |
| cmt_dict = vpr_grid.get_cmt_dict() | |
| site_type_dict = vpr_grid.get_site_type_dict() | |
| try: | |
| top_cmt_tile = next(k for k, v in cmt_dict.items() if k.startswith("CLK_BUFG_TOP")) | |
| _, thresh_top_y = cmt_dict[top_cmt_tile]["canon_loc"] | |
| except StopIteration: | |
| thresh_top_y = None | |
| try: | |
| bot_cmt_tile = next(k for k, v in cmt_dict.items() if k.startswith("CLK_BUFG_BOT")) | |
| _, thresh_bot_y = cmt_dict[bot_cmt_tile]["canon_loc"] | |
| except StopIteration: | |
| thresh_bot_y = None | |
| if graph_limit is None: | |
| assert thresh_top_y is not None, "BUFG sites in the top half of the device not found" | |
| assert thresh_bot_y is not None, "BUFG sites in the bottom half of the device not found" | |
| else: | |
| assert ( | |
| thresh_top_y is not None or thresh_bot_y is not None | |
| ), "The device grid does not contain any BUFG sites" | |
| for k, v in cmt_dict.items(): | |
| clock_region = v["clock_region"] | |
| x, y = v["canon_loc"] | |
| if clock_region is None: | |
| continue | |
| elif clock_region in self.cmt_to_bufg_tile: | |
| continue | |
| elif thresh_top_y is not None and y <= thresh_top_y: | |
| self.cmt_to_bufg_tile[clock_region] = "TOP" | |
| elif thresh_bot_y is not None and y >= thresh_bot_y: | |
| self.cmt_to_bufg_tile[clock_region] = "BOT" | |
| for _, _, clk_region in site_type_dict["PLLE2_ADV"]: | |
| self.pll_cmts.add(clk_region) | |
| if any(site in GTP_PRIMITIVES for site in site_type_dict): | |
| assert "IBUFDS_GTE2" in site_type_dict | |
| for _, _, clk_region in site_type_dict["IBUFDS_GTE2"]: | |
| self.gtp_cmts.add(clk_region) | |
| for _, _, clk_region in site_type_dict["MMCME2_ADV"]: | |
| self.mmcm_cmts.add(clk_region) | |
| self.input_pins = {} | |
| if not self.roi: | |
| for input_pin in blif_data["inputs"]["args"]: | |
| if input_pin not in io_locs.keys(): | |
| continue | |
| loc = io_locs[input_pin] | |
| cmt = get_cmt(cmt_dict, loc) | |
| assert cmt is not None, loc | |
| self.input_pins[input_pin] = cmt | |
| self.clock_blocks = {} | |
| self.clock_sources = {} | |
| self.clock_sources_cname = {} | |
| self.clock_cmts = {} | |
| if "subckt" not in blif_data.keys(): | |
| return | |
| for subckt in blif_data["subckt"]: | |
| if "cname" not in subckt: | |
| continue | |
| bel = subckt["args"][0] | |
| assert "cname" in subckt and len(subckt["cname"]) == 1, subckt | |
| if bel not in CLOCKS: | |
| continue | |
| cname = subckt["cname"][0] | |
| clock = { | |
| "name": cname, | |
| "subckt": bel, | |
| "sink_nets": [], | |
| "source_nets": [], | |
| } | |
| sources = CLOCKS[bel]["sources"] | |
| ports = dict(arg.split("=", maxsplit=1) for arg in subckt["args"][1:]) | |
| for source in sources: | |
| source_net = ports[source] | |
| if source_net == "$true" or source_net == "$false": | |
| continue | |
| self.clock_sources[source_net] = [] | |
| self.clock_sources_cname[source_net] = cname | |
| clock["source_nets"].append(source_net) | |
| self.clock_blocks[cname] = clock | |
| # Both PS7 and BUFGCTRL has specialized constraints, | |
| # do not bind based on input pins. | |
| if bel not in ["PS7_VPR", "BUFGCTRL_VPR"]: | |
| for port in ports.values(): | |
| if port not in io_locs: | |
| continue | |
| if cname in self.clock_cmts: | |
| assert_out = (cname, port, self.clock_cmts[cname], self.input_pins[port]) | |
| assert self.clock_cmts[cname] == self.input_pins[port], assert_out | |
| else: | |
| self.clock_cmts[cname] = self.input_pins[port] | |
| for subckt in blif_data["subckt"]: | |
| if "cname" not in subckt: | |
| continue | |
| bel = subckt["args"][0] | |
| if bel not in CLOCKS: | |
| continue | |
| sinks = CLOCKS[bel]["sinks"] | |
| ports = dict(arg.split("=", maxsplit=1) for arg in subckt["args"][1:]) | |
| assert "cname" in subckt and len(subckt["cname"]) == 1, subckt | |
| cname = subckt["cname"][0] | |
| clock = self.clock_blocks[cname] | |
| for sink in sinks: | |
| if sink not in ports: | |
| continue | |
| sink_net = ports[sink] | |
| if sink_net == "$true" or sink_net == "$false": | |
| continue | |
| clock["sink_nets"].append(sink_net) | |
| if sink_net not in self.input_pins and sink_net not in self.clock_sources: | |
| # Allow IBUFs. At this point we cannot distinguish between | |
| # IBUFs for clock and logic. | |
| if bel == "IBUF_VPR": | |
| continue | |
| # Allow BUFGs to be driven by generic sources but only | |
| # when enabled. | |
| if bel == "BUFGCTRL_VPR" and allow_bufg_logic_sources: | |
| continue | |
| # The clock source comes from logic, disallow that | |
| eprint( | |
| "The clock net '{}' driving '{}' sources at logic which is not allowed!".format(sink_net, bel) | |
| ) | |
| exit(-1) | |
| if sink_net in self.input_pins: | |
| if sink_net not in self.clock_sources: | |
| self.clock_sources[sink_net] = [] | |
| self.clock_sources[sink_net].append(cname) | |
| def assign_cmts(self, vpr_grid, blocks, block_locs): | |
| """Assign CMTs to subckt's that require it (e.g. BURF/PLL/MMCM).""" | |
| problem = constraint.Problem() | |
| site_dict = vpr_grid.get_site_dict() | |
| vpr_loc_cmt = vpr_grid.get_vpr_loc_cmt() | |
| # Any clocks that have LOC's already defined should be respected. | |
| # Store the parent CMT in clock_cmts. | |
| for block, loc in block_locs.items(): | |
| if block in self.clock_blocks: | |
| clock = self.clock_blocks[block] | |
| if CLOCKS[clock["subckt"]]["type"] == "BUFGCTRL": | |
| pass | |
| else: | |
| site = site_dict[loc.replace('"', "")] | |
| assert site is not None, (block, loc) | |
| clock_region_pkey = site["clock_region"] | |
| if block in self.clock_cmts: | |
| assert clock_region_pkey == self.clock_cmts[block], (block, clock_region_pkey) | |
| else: | |
| self.clock_cmts[block] = clock_region_pkey | |
| # Any clocks that were previously constrained must be preserved | |
| for block, (loc_x, loc_y, _) in blocks.items(): | |
| if block in self.clock_blocks: | |
| clock = self.clock_blocks[block] | |
| if CLOCKS[clock["subckt"]]["type"] == "BUFGCTRL": | |
| pass | |
| else: | |
| cmt = vpr_loc_cmt[(loc_x, loc_y)] | |
| if block in self.clock_cmts: | |
| assert cmt == self.clock_cmts[block], (block, cmt) | |
| else: | |
| self.clock_cmts[block] = cmt | |
| # Non-clock IBUF's increase the solution space if they are not | |
| # constrainted by a LOC. Given that non-clock IBUF's don't need to | |
| # solved for, remove them. | |
| unused_blocks = set() | |
| any_variable = False | |
| for clock_name, clock in self.clock_blocks.items(): | |
| used = False | |
| if CLOCKS[clock["subckt"]]["type"] != "IBUF": | |
| used = True | |
| for source_net in clock["source_nets"]: | |
| if len(self.clock_sources[source_net]) > 0: | |
| used = True | |
| break | |
| if not used: | |
| unused_blocks.add(clock_name) | |
| continue | |
| if CLOCKS[clock["subckt"]]["type"] == "BUFGCTRL": | |
| problem.addVariable(clock_name, list(self.bufg_from_cmt.keys())) | |
| any_variable = True | |
| else: | |
| # Constrained objects have a domain of 1 | |
| if clock_name in self.clock_cmts: | |
| problem.addVariable(clock_name, (self.clock_cmts[clock_name],)) | |
| any_variable = True | |
| else: | |
| problem.addVariable(clock_name, list(self.cmt_to_bufg_tile.keys())) | |
| any_variable = True | |
| # Remove unused blocks from solutions. | |
| for clock_name in unused_blocks: | |
| del self.clock_blocks[clock_name] | |
| exclusive_clocks = defaultdict(set) | |
| ibuf_cmt_sinks = defaultdict(set) | |
| for net in self.clock_sources: | |
| is_net_ibuf = False | |
| if net not in self.input_pins: | |
| source_clock_name = self.clock_sources_cname[net] | |
| if source_clock_name not in unused_blocks: | |
| source_block = self.clock_blocks[source_clock_name] | |
| if CLOCKS[source_block["subckt"]]["type"] == "IBUF": | |
| is_net_ibuf = True | |
| for clock_name in self.clock_sources[net]: | |
| if clock_name in unused_blocks: | |
| continue | |
| clock = self.clock_blocks[clock_name] | |
| if CLOCKS[clock["subckt"]]["type"] == "PLLE2_ADV": | |
| problem.addConstraint(lambda cmt: cmt in self.pll_cmts, (clock_name,)) | |
| exclusive_clocks["PLLE2_ADV"].add(clock_name) | |
| if is_net_ibuf: | |
| ibuf_cmt_sinks[source_clock_name].add(clock_name) | |
| if CLOCKS[clock["subckt"]]["type"] == "MMCME2_ADV": | |
| problem.addConstraint(lambda cmt: cmt in self.mmcm_cmts, (clock_name,)) | |
| exclusive_clocks["MMCME2_ADV"].add(clock_name) | |
| if is_net_ibuf: | |
| ibuf_cmt_sinks[source_clock_name].add(clock_name) | |
| if net in self.input_pins: | |
| if CLOCKS[clock["subckt"]]["type"] == "BUFGCTRL": | |
| # BUFGCTRL do not get a CMT, instead they get either top or | |
| # bottom. | |
| problem.addConstraint( | |
| lambda clock: clock == self.cmt_to_bufg_tile[self.input_pins[net]], (clock_name,) | |
| ) | |
| elif CLOCKS[clock["subckt"]]["type"] != "IBUF": | |
| problem.addConstraint(lambda clock: clock == self.input_pins[net], (clock_name,)) | |
| else: | |
| source_clock_name = self.clock_sources_cname[net] | |
| source_block = self.clock_blocks[source_clock_name] | |
| is_net_bufg = CLOCKS[source_block["subckt"]]["type"] == "BUFGCTRL" | |
| if is_net_bufg: | |
| continue | |
| if CLOCKS[clock["subckt"]]["type"] == "BUFGCTRL": | |
| # BUFG's need to be in the right half. | |
| problem.addConstraint( | |
| lambda source, sink_bufg: self.cmt_to_bufg_tile[source] == sink_bufg, | |
| (source_clock_name, clock_name), | |
| ) | |
| elif not is_net_ibuf: | |
| problem.addConstraint(lambda source, sink: source == sink, (source_clock_name, clock_name)) | |
| # Ensure that in case of an IBUF driving multiple CMTs at least one of | |
| # them is in the same clock region as the IBUF. | |
| for source, sinks in ibuf_cmt_sinks.items(): | |
| problem.addConstraint( | |
| lambda s, *k: s in k, | |
| ( | |
| source, | |
| *sinks, | |
| ), | |
| ) | |
| # Prevent constraining exclusive clock source sets to the same resource | |
| for typ, clocks in exclusive_clocks.items(): | |
| problem.addConstraint(constraint.AllDifferentConstraint(), list(clocks)) | |
| if any_variable: | |
| solutions = problem.getSolutions() | |
| assert len(solutions) > 0, "No solution found!" | |
| self.clock_cmts.update(solutions[0]) | |
| def place_clocks(self, canon_grid, vpr_grid, loc_in_use, block_locs, blocks, grid_capacities): | |
| self.assign_cmts(vpr_grid, blocks, block_locs) | |
| site_type_dict = vpr_grid.get_site_type_dict() | |
| # Key is (type, clock_region_pkey) | |
| available_placements = {} | |
| available_locs = {} | |
| vpr_locs = {} | |
| for clock_type in CLOCKS.values(): | |
| if clock_type == "IBUF" or clock_type["type"] not in site_type_dict: | |
| continue | |
| for loc, tile_name, clock_region_pkey in site_type_dict[clock_type["type"]]: | |
| if clock_type["type"] == "BUFGCTRL": | |
| if "_TOP_" in tile_name: | |
| key = (clock_type["type"], "TOP") | |
| elif "_BOT_" in tile_name: | |
| key = (clock_type["type"], "BOT") | |
| else: | |
| key = (clock_type["type"], clock_region_pkey) | |
| if key not in available_placements: | |
| available_placements[key] = [] | |
| available_locs[key] = [] | |
| available_placements[key].append(loc) | |
| vpr_loc = get_vpr_coords_from_site_name(canon_grid, vpr_grid, loc, grid_capacities) | |
| if vpr_loc is None: | |
| continue | |
| available_locs[key].append(vpr_loc) | |
| vpr_locs[loc] = vpr_loc | |
| for clock_name, clock in self.clock_blocks.items(): | |
| # All clocks should have an assigned CMTs at this point. | |
| assert clock_name in self.clock_cmts, clock_name | |
| bel_type = CLOCKS[clock["subckt"]]["type"] | |
| # Skip LOCing the PS7. There is only one | |
| if bel_type == "PS7": | |
| continue | |
| if bel_type == "IBUF": | |
| continue | |
| key = (bel_type, self.clock_cmts[clock_name]) | |
| if clock_name in blocks: | |
| # This block has a LOC constraint from the user, verify that | |
| # this LOC constraint makes sense. | |
| assert blocks[clock_name] in available_locs[key], (clock_name, blocks[clock_name], available_locs[key]) | |
| continue | |
| loc = None | |
| for potential_loc in sorted(available_placements[key]): | |
| # This block has no existing placement, make one | |
| vpr_loc = vpr_locs[potential_loc] | |
| if vpr_loc in loc_in_use: | |
| continue | |
| loc_in_use.add(vpr_loc) | |
| yield clock_name, potential_loc | |
| loc = potential_loc | |
| break | |
| # No free LOC!!! | |
| assert loc is not None, (clock_name, available_placements[key]) | |
| def has_clock_nets(self): | |
| return self.clock_sources | |
| def get_tile_capacities(arch_xml_filename): | |
| arch = ET.parse(arch_xml_filename, ET.XMLParser(remove_blank_text=True)) | |
| root = arch.getroot() | |
| tile_capacities = {} | |
| for el in root.iter("tile"): | |
| tile_name = el.attrib["name"] | |
| tile_capacities[tile_name] = 0 | |
| for sub_tile in el.iter("sub_tile"): | |
| capacity = 1 | |
| if "capacity" in sub_tile.attrib: | |
| capacity = int(sub_tile.attrib["capacity"]) | |
| tile_capacities[tile_name] += capacity | |
| grid = {} | |
| for el in root.iter("single"): | |
| x = int(el.attrib["x"]) | |
| y = int(el.attrib["y"]) | |
| grid[(x, y)] = tile_capacities[el.attrib["type"]] | |
| return grid | |
| def get_vpr_coords_from_site_name(canon_grid, vpr_grid, site_name, grid_capacities): | |
| site_name = site_name.replace('"', "") | |
| site_dict = vpr_grid.get_site_dict() | |
| canon_loc = vpr_grid.get_canon_loc() | |
| tile = site_dict[site_name]["tile"] | |
| capacity = 0 | |
| x, y = site_dict[site_name]["vpr_loc"] | |
| canon_x, canon_y = canon_loc[(x, y)] | |
| if (x, y) in grid_capacities.keys(): | |
| capacity = grid_capacities[(x, y)] | |
| if not capacity: | |
| # If capacity is zero it means that the site is out of | |
| # the ROI, hence the site needs to be skipped. | |
| return None | |
| elif capacity == 1: | |
| return (x, y, 0) | |
| else: | |
| sites = list(canon_grid.gridinfo_at_loc((canon_x, canon_y)).sites.keys()) | |
| assert capacity == len(sites), (tile, capacity, (x, y)) | |
| instance_idx = sites.index(site_name) | |
| return (x, y, instance_idx) | |
| def constrain_special_ios(canon_grid, vpr_grid, io_blocks, blif_data, blocks, place_constraints): | |
| """ | |
| There are special IOs which need extra handling when dealing with placement constraints. | |
| For instance, the IBUFDS_GTE2 primitive must be placed in correspondance with the location | |
| of its input PADs, as no other route exists other than that. | |
| This function reads the connectivity of the top level nets, which have been previously | |
| constrained, and correctly constrains those blocks which require special handling. | |
| """ | |
| if "subckt" not in blif_data: | |
| return | |
| BEL_TYPES = ["IBUFDS_GTE2_VPR", "GTPE2_CHANNEL_VPR"] | |
| SPECIAL_IPADS = ["IPAD_GTP_VPR"] | |
| special_io_map = dict() | |
| for subckt in blif_data["subckt"]: | |
| if "cname" not in subckt: | |
| continue | |
| bel = subckt["args"][0] | |
| if bel not in SPECIAL_IPADS: | |
| continue | |
| top_net = None | |
| renamed_net = None | |
| for port in subckt["args"][1:]: | |
| port_name, net = port.split("=") | |
| if port_name == "I": | |
| assert net in io_blocks, (net, io_blocks) | |
| top_net = net | |
| elif port_name == "O": | |
| renamed_net = net | |
| else: | |
| assert False, "ERROR: Special IPAD ports not recognized!" | |
| special_io_map[renamed_net] = top_net | |
| blocks_to_constrain = set() | |
| for subckt in blif_data["subckt"]: | |
| if "cname" not in subckt: | |
| continue | |
| bel = subckt["args"][0] | |
| if bel not in BEL_TYPES: | |
| continue | |
| assert "cname" in subckt and len(subckt["cname"]) == 1, subckt | |
| cname = subckt["cname"][0] | |
| for port in subckt["args"][1:]: | |
| _, net = port.split("=") | |
| if net not in special_io_map: | |
| continue | |
| net = special_io_map[net] | |
| if net in io_blocks: | |
| x, y, z = io_blocks[net] | |
| canon_loc = vpr_grid.get_canon_loc()[(x, y)] | |
| gridinfo = canon_grid.gridinfo_at_loc(canon_loc) | |
| sites = sorted(gridinfo.sites.keys()) | |
| site_name = sites[z] | |
| connected_io_site = vpr_grid.get_site_dict()[site_name]["connected_to_site"] | |
| assert connected_io_site, (site_name, bel, cname) | |
| new_z = sites.index(connected_io_site) | |
| loc = (x, y, new_z) | |
| blocks_to_constrain.add((cname, loc)) | |
| for block, vpr_loc in blocks_to_constrain: | |
| place_constraints.constrain_block(block, vpr_loc, "Constraining block {}".format(block)) | |
| blocks[block] = vpr_loc | |
| def p_main( | |
| net, | |
| vpr_grid_map, | |
| arch, | |
| part, | |
| blif, | |
| db_root, | |
| input=sys.stdin, | |
| output=sys.stdout, | |
| roi=False, | |
| allow_bufg_logic_sources=False, | |
| graph_limit=None, | |
| ): | |
| device_families = { | |
| "xc7a": "artix7", | |
| "xc7k": "kintex7", | |
| "xc7z": "zynq7", | |
| "xc7s": "spartan7", | |
| } | |
| device_family = None | |
| for device in device_families: | |
| if part.startswith(device): | |
| device_family = device_families[device] | |
| break | |
| assert device_family | |
| db_root = str(Path(db_root) / device_family) | |
| db = prjxray.db.Database(db_root, part) | |
| canon_grid = db.grid() | |
| io_blocks = {} | |
| loc_in_use = set() | |
| for line in input: | |
| output.write(line) | |
| if line[0] == "#": | |
| continue | |
| block, x, y, z = line.strip().split()[0:4] | |
| io_blocks[block] = (int(x), int(y), int(z)) | |
| loc_in_use.add(io_blocks[block]) | |
| place_constraints = PlaceConstraints(net) | |
| place_constraints.load_loc_sites_from_net_file() | |
| grid_capacities = get_tile_capacities(arch) | |
| eblif_data = parse_blif(blif) | |
| vpr_grid = VprGrid(vpr_grid_map, graph_limit) | |
| # Constrain IO blocks and LOCed resources | |
| blocks = {} | |
| block_locs = {} | |
| for block, loc in place_constraints.get_loc_sites(): | |
| vpr_loc = get_vpr_coords_from_site_name(canon_grid, vpr_grid, loc, grid_capacities) | |
| loc_in_use.add(vpr_loc) | |
| if block in io_blocks: | |
| assert io_blocks[block] == vpr_loc, (block, vpr_loc, io_blocks[block]) | |
| blocks[block] = vpr_loc | |
| block_locs[block] = loc | |
| place_constraints.constrain_block(block, vpr_loc, "Constraining block {}".format(block)) | |
| # Constrain blocks directly connected to IO in the same x, y location | |
| constrain_special_ios(canon_grid, vpr_grid, io_blocks, eblif_data, blocks, place_constraints) | |
| # Constrain clock resources | |
| clock_placer = ClockPlacer(vpr_grid, io_blocks, eblif_data, roi, graph_limit, allow_bufg_logic_sources) | |
| if clock_placer.has_clock_nets(): | |
| for block, loc in clock_placer.place_clocks( | |
| canon_grid, vpr_grid, loc_in_use, block_locs, blocks, grid_capacities | |
| ): | |
| vpr_loc = get_vpr_coords_from_site_name(canon_grid, vpr_grid, loc, grid_capacities) | |
| place_constraints.constrain_block(block, vpr_loc, "Constraining clock block {}".format(block)) | |
| """ Constrain IDELAYCTRL sites | |
| Prior to the invocation of this script, the IDELAYCTRL sites must have been | |
| replicated accordingly to the IDELAY specifications. | |
| There can be three different usage combinations of IDELAYCTRL and IDELAYs in a design: | |
| 1. IODELAYs and IDELAYCTRLs can be constrained to banks as needed, | |
| through an in-design LOC constraint. | |
| Manual replication of the constrained IDELAYCTRLs is necessary to provide a | |
| controller for each bank. | |
| 2. IODELAYs and a single IDELAYCTRL can be left entirely unconstrained, | |
| becoming a default group. The IDELAYCTRLis replicated depending on bank usage. | |
| Replication must have happened prior to this step | |
| 3. One or more IODELAY_GROUPs can be defined that contain IODELAYs and a single | |
| IDELAYCTRL each. These components can be otherwise unconstrained and the IDELAYCTRL | |
| for each group has to be replicated as needed (depending on bank usage). | |
| NOTE: IODELAY_GROUPS are not enabled at the moment. | |
| """ | |
| idelayctrl_cmts = set() | |
| idelay_instances = place_constraints.get_used_instances("IDELAYE2") | |
| for inst in idelay_instances: | |
| x, y, z = io_blocks[inst] | |
| idelayctrl_cmt = vpr_grid.get_vpr_loc_cmt()[(x, y)] | |
| idelayctrl_cmts.add(idelayctrl_cmt) | |
| idelayctrl_instances = place_constraints.get_used_instances("IDELAYCTRL") | |
| assert len(idelayctrl_cmts) <= len( | |
| idelayctrl_instances | |
| ), "The number of IDELAYCTRL blocks and IO banks with IDELAYs used do not match." | |
| idelayctrl_sites = dict() | |
| for site_name, _, clk_region in vpr_grid.get_site_type_dict()["IDELAYCTRL"]: | |
| if clk_region in idelayctrl_cmts: | |
| idelayctrl_sites[clk_region] = site_name | |
| # Check and remove user constrained IDELAYCTRLs | |
| for idelayctrl_block in idelayctrl_instances: | |
| if idelayctrl_block in blocks.keys(): | |
| x, y, _ = blocks[idelayctrl_block] | |
| idelayctrl_cmt = vpr_grid.get_vpr_loc_cmt()[(x, y)] | |
| assert idelayctrl_cmt in idelayctrl_cmts | |
| idelayctrl_cmts.remove(idelayctrl_cmt) | |
| idelayctrl_instances.remove(idelayctrl_block) | |
| # TODO: Add possibility to bind IDELAY banks to IDELAYCTRL sites using | |
| # the IDELAY_GROUP attribute. | |
| for cmt, idelayctrl_block in zip(idelayctrl_cmts, idelayctrl_instances): | |
| x, y = vpr_grid.get_site_dict()[idelayctrl_sites[cmt]]["vpr_loc"] | |
| vpr_loc = (x, y, 0) | |
| place_constraints.constrain_block( | |
| idelayctrl_block, vpr_loc, "Constraining idelayctrl block {}".format(idelayctrl_block) | |
| ) | |
| if len(idelayctrl_instances) > 0: | |
| print("Warning: IDELAY_GROUPS parameters are currently being ignored!", file=sys.stderr) | |
| place_constraints.output_place_constraints(output) | |
| def main( | |
| net, | |
| vpr_grid_map, | |
| arch, | |
| part, | |
| blif, | |
| db_root=None, | |
| input=None, | |
| output=None, | |
| roi=False, | |
| allow_bufg_logic_sources=False, | |
| graph_limit=None, | |
| ): | |
| p_main( | |
| net=Path(net).open("r"), | |
| vpr_grid_map=vpr_grid_map, | |
| arch=arch, | |
| db_root=environ.get( | |
| "DATABASE_DIR", subprocess_run("prjxray-config", capture_output=True).stdout.decode("utf-8").strip() | |
| ) | |
| if db_root is None | |
| else db_root, | |
| part=part, | |
| blif=Path(blif).open("r"), | |
| input=sys.stdin if input is None else Path(input).open("r"), | |
| output=sys.stdout if output is None else Path(output).open("w"), | |
| roi=roi, | |
| allow_bufg_logic_sources=allow_bufg_logic_sources, | |
| graph_limit=graph_limit, | |
| ) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Convert a PCF file into a VPR io.place file.") | |
| parser.add_argument( | |
| "--input", "-i", "-I", type=argparse.FileType("r"), default=sys.stdout, help="The input constraints place file" | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| "-o", | |
| "-O", | |
| type=argparse.FileType("w"), | |
| default=sys.stdout, | |
| help="The output constraints place file", | |
| ) | |
| parser.add_argument("--net", "-n", type=argparse.FileType("r"), required=True, help="top.net file") | |
| parser.add_argument("--vpr_grid_map", help="Map of canonical to VPR grid locations", required=True) | |
| parser.add_argument("--arch", help="Arch XML", required=True) | |
| parser.add_argument("--db_root", required=True) | |
| parser.add_argument("--part", required=True) | |
| parser.add_argument("--blif", "-b", type=argparse.FileType("r"), required=True, help="BLIF / eBLIF file") | |
| parser.add_argument("--roi", action="store_true", help="Using ROI") | |
| parser.add_argument( | |
| "--allow-bufg-logic-sources", action="store_true", help="When set allows BUFGs to be driven by logic" | |
| ) | |
| parser.add_argument("--graph_limit", help="Graph limit parameters") | |
| args = parser.parse_args() | |
| p_main( | |
| net=args.net, | |
| vpr_grid_map=args.vpr_grid_map, | |
| arch=args.arch, | |
| db_root=args.db_root, | |
| part=args.part, | |
| blif=args.blif, | |
| input=args.input, | |
| output=args.output, | |
| roi=args.roi, | |
| allow_bufg_logic_sources=args.allow_bufg_logic_sources, | |
| graph_limit=args.graph_limit, | |
| ) |