| #!/usr/bin/env python3 | |
| """ | |
| Functions related to parsing and processing of data stored in a QuickLogic | |
| TechFile. | |
| """ | |
| from copy import deepcopy | |
| import itertools | |
| import argparse | |
| from collections import defaultdict | |
| import pickle | |
| import re | |
| import csv | |
| import lxml.etree as ET | |
| from data_structs import Pin, PinDirection, Quadrant, ClockCell, Cell, CellType, \ | |
| Tile, TileType, Loc, SwitchboxPinLoc, SwitchboxPinType, Switchbox, SwitchboxPin, \ | |
| SwitchConnection, SwitchPin, PackagePin, OPPOSITE_DIRECTION | |
| from utils import yield_muxes, get_loc_of_cell, find_cell_in_tile, natural_keys | |
| from connections import build_connections, check_connections | |
| from connections import hop_to_str, get_name_and_hop, is_regular_hop_wire | |
| # ============================================================================= | |
| # A List of cells and their pins which are clocks. These are relevant only for | |
| # cells modeled as pb_types in VPR and are used to determine whether an input | |
| # pin should be of type "input"/"output" or "clock". | |
| CLOCK_PINS = { | |
| "LOGIC": ("QCK", ), | |
| "CLOCK": ("IC", ), | |
| "GMUX": ( | |
| "IP", | |
| "IC", | |
| "IZ", | |
| ), | |
| "RAM": ( | |
| "CLK1_0", | |
| "CLK1_1", | |
| "CLK2_0", | |
| "CLK2_1", | |
| ), | |
| } | |
| # A list of const pins | |
| CONST_PINS = ( | |
| "GND", | |
| "VCC", | |
| ) | |
| # ============================================================================= | |
| def parse_library(xml_library): | |
| """ | |
| Loads cell definitions from the XML | |
| """ | |
| KNOWN_PORT_ATTRIB = [ | |
| "hardWired", | |
| "isInvertible", | |
| "isAsynchronous", | |
| "realPortName", | |
| "isblank", | |
| "io", | |
| "unet", | |
| ] | |
| cells = [] | |
| for xml_node in xml_library: | |
| # Skip those | |
| if xml_node.tag in ["PortProperties"]: | |
| continue | |
| cell_type = xml_node.tag | |
| cell_pins = [] | |
| # Load pins | |
| for xml_pins in itertools.chain(xml_node.findall("INPUT"), | |
| xml_node.findall("OUTPUT")): | |
| # Pin direction | |
| if xml_pins.tag == "INPUT": | |
| direction = PinDirection.INPUT | |
| elif xml_pins.tag == "OUTPUT": | |
| direction = PinDirection.OUTPUT | |
| else: | |
| assert False, xml_pins.tag | |
| # "mport" | |
| for xml_mport in xml_pins: | |
| xml_bus = xml_mport.find("bus") | |
| # Check if the port is routable. Skip it if it is not. | |
| is_routable = xml_mport.get("routable", "true") == "true" | |
| if not is_routable: | |
| continue | |
| # Gather attributes | |
| port_attrib = {} | |
| for key, val in xml_mport.attrib.items(): | |
| if key in KNOWN_PORT_ATTRIB: | |
| port_attrib[key] = str(val) | |
| # A bus | |
| if xml_bus is not None: | |
| lsb = int(xml_bus.attrib["lsb"]) | |
| msb = int(xml_bus.attrib["msb"]) | |
| stp = int(xml_bus.attrib["step"]) | |
| for i in range(lsb, msb + 1, stp): | |
| cell_pins.append( | |
| Pin( | |
| name="{}[{}]".format( | |
| xml_bus.attrib["name"], i | |
| ), | |
| direction=direction, | |
| attrib=port_attrib, | |
| ) | |
| ) | |
| # A single pin | |
| else: | |
| name = xml_mport.attrib["name"] | |
| # HACK: Do not import the CLOCK.OP pin | |
| if cell_type == "CLOCK" and name == "OP": | |
| continue | |
| if cell_type in CLOCK_PINS and \ | |
| name in CLOCK_PINS[cell_type]: | |
| port_attrib["clock"] = "true" | |
| cell_pins.append( | |
| Pin( | |
| name=name, | |
| direction=direction, | |
| attrib=port_attrib, | |
| ) | |
| ) | |
| # Add the cell | |
| cells.append(CellType(type=cell_type, pins=cell_pins)) | |
| return cells | |
| # ============================================================================= | |
| def load_logic_cells(xml_placement, cellgrid, cells_library): | |
| # Load "LOGIC" tiles | |
| xml_logic = xml_placement.find("LOGIC") | |
| assert xml_logic is not None | |
| exceptions = set() | |
| xml_exceptions = xml_logic.find("EXCEPTIONS") | |
| if xml_exceptions is not None: | |
| for xml in xml_exceptions: | |
| tag = xml.tag.upper() | |
| # FIXME: Is this connect decoding of those werid loc specs? | |
| x = 1 + ord(tag[0]) - ord("A") | |
| y = 1 + int(tag[1:]) | |
| exceptions.add(Loc(x=x, y=y, z=0)) | |
| xml_logicmatrix = xml_logic.find("LOGICMATRIX") | |
| assert xml_logicmatrix is not None | |
| x0 = int(xml_logicmatrix.get("START_COLUMN")) | |
| nx = int(xml_logicmatrix.get("COLUMNS")) | |
| y0 = int(xml_logicmatrix.get("START_ROW")) | |
| ny = int(xml_logicmatrix.get("ROWS")) | |
| for j in range(ny): | |
| for i in range(nx): | |
| loc = Loc(x0 + i, y0 + j, 0) | |
| if loc in exceptions: | |
| continue | |
| cell_type = "LOGIC" | |
| assert cell_type in cells_library, cell_type | |
| cellgrid[loc].append( | |
| Cell(type=cell_type, index=None, name=cell_type, alias=None) | |
| ) | |
| def load_other_cells(xml_placement, cellgrid, cells_library): | |
| # Loop over XML entries | |
| for xml in xml_placement: | |
| # Got a "Cell" tag | |
| if xml.tag == "Cell": | |
| cell_name = xml.get("name") | |
| cell_type = xml.get("type") | |
| assert cell_type in cells_library, ( | |
| cell_type, | |
| cell_name, | |
| ) | |
| # Cell matrix | |
| xml_matrices = [x for x in xml if x.tag.startswith("Matrix")] | |
| for xml_matrix in xml_matrices: | |
| x0 = int(xml_matrix.get("START_COLUMN")) | |
| nx = int(xml_matrix.get("COLUMNS")) | |
| y0 = int(xml_matrix.get("START_ROW")) | |
| ny = int(xml_matrix.get("ROWS")) | |
| for j in range(ny): | |
| for i in range(nx): | |
| loc = Loc(x0 + i, y0 + j, 0) | |
| cellgrid[loc].append( | |
| Cell( | |
| type=cell_type, | |
| index=None, | |
| name=cell_name, | |
| alias=None, | |
| ) | |
| ) | |
| # A single cell | |
| if len(xml_matrices) == 0: | |
| x = int(xml.get("column")) | |
| y = int(xml.get("row")) | |
| loc = Loc(x, y, 0) | |
| alias = xml.get("Alias", None) | |
| cellgrid[loc].append( | |
| Cell( | |
| type=cell_type, | |
| index=None, | |
| name=cell_name, | |
| alias=alias, | |
| ) | |
| ) | |
| # Got something else, parse recursively | |
| else: | |
| load_other_cells(xml, cellgrid, cells_library) | |
| def make_tile_type_name(cells): | |
| """ | |
| Generate the tile type name from cell types | |
| """ | |
| cell_types = sorted([c.type for c in cells]) | |
| cell_counts = {t: 0 for t in cell_types} | |
| for cell in cells: | |
| cell_counts[cell.type] += 1 | |
| parts = [] | |
| for t, c in cell_counts.items(): | |
| if c == 1: | |
| parts.append(t) | |
| else: | |
| parts.append("{}x{}".format(c, t)) | |
| return "_".join(parts).upper() | |
| def parse_placement(xml_placement, cells_library): | |
| # Load tilegrid quadrants | |
| quadrants = {} | |
| xml_quadrants = xml_placement.find("Quadrants") | |
| assert xml_quadrants is not None | |
| xmin = None | |
| xmax = None | |
| ymin = None | |
| ymax = None | |
| for xml_quadrant in xml_quadrants: | |
| name = xml_quadrant.get("name") | |
| x0 = int(xml_quadrant.get("ColStartNum")) | |
| x1 = int(xml_quadrant.get("ColEndNum")) | |
| y0 = int(xml_quadrant.get("RowStartNum")) | |
| y1 = int(xml_quadrant.get("RowEndNum")) | |
| quadrants[name] = Quadrant( | |
| name=name, | |
| x0=x0, | |
| x1=x1, | |
| y0=y0, | |
| y1=y1, | |
| ) | |
| xmin = min(xmin, x0) if xmin is not None else x0 | |
| xmax = max(xmax, x1) if xmax is not None else x1 | |
| ymin = min(ymin, y0) if ymin is not None else y0 | |
| ymax = max(ymax, y1) if ymax is not None else y1 | |
| # Define the initial tile grid. Group cells with the same location | |
| # together. | |
| cellgrid = defaultdict(lambda: []) | |
| # Load LOGIC cells into it | |
| load_logic_cells(xml_placement, cellgrid, cells_library) | |
| # Load other cells | |
| load_other_cells(xml_placement, cellgrid, cells_library) | |
| # Assign each location with a tile type name generated basing on cells | |
| # present there. | |
| tile_types = {} | |
| tile_types_at_loc = {} | |
| for loc, cells in cellgrid.items(): | |
| # Generate type and assign | |
| type = make_tile_type_name(cells) | |
| tile_types_at_loc[loc] = type | |
| # A new type? complete its definition | |
| if type not in tile_types: | |
| cell_types = [c.type for c in cells] | |
| cell_count = { | |
| t: len([c for c in cells if c.type == t]) | |
| for t in cell_types | |
| } | |
| tile_type = TileType(type, cell_count) | |
| tile_type.make_pins(cells_library) | |
| tile_types[type] = tile_type | |
| # Make the final tilegrid | |
| tilegrid = {} | |
| for loc, type in tile_types_at_loc.items(): | |
| # Group cells by type | |
| tile_cells_by_type = defaultdict(lambda: []) | |
| for cell in cellgrid[loc]: | |
| tile_cells_by_type[cell.type].append(cell) | |
| # Create a list of cell instances within the tile | |
| tile_cells = [] | |
| for cell_type, cell_list in tile_cells_by_type.items(): | |
| cell_list.sort(key=lambda c: natural_keys(c.name)) | |
| for i, cell in enumerate(cell_list): | |
| tile_cells.append( | |
| Cell( | |
| type=cell.type, | |
| index=i, | |
| name=cell.name, | |
| alias=cell.alias | |
| ) | |
| ) | |
| tilegrid[loc] = Tile( | |
| type=type, | |
| name="TILE_X{}Y{}".format(loc.x, loc.y), | |
| cells=tile_cells | |
| ) | |
| return quadrants, tile_types, tilegrid | |
| def populate_switchboxes(xml_sbox, switchbox_grid): | |
| """ | |
| Assings each tile in the grid its switchbox type. | |
| """ | |
| xmin = int(xml_sbox.attrib["ColStartNum"]) | |
| xmax = int(xml_sbox.attrib["ColEndNum"]) | |
| ymin = int(xml_sbox.attrib["RowStartNum"]) | |
| ymax = int(xml_sbox.attrib["RowEndNum"]) | |
| for y, x in itertools.product(range(ymin, ymax + 1), range(xmin, | |
| xmax + 1)): | |
| loc = Loc(x, y, 0) | |
| assert loc not in switchbox_grid, loc | |
| switchbox_grid[loc] = xml_sbox.tag | |
| # ============================================================================= | |
| def update_switchbox_pins(switchbox): | |
| """ | |
| Identifies top-level inputs and outputs of the switchbox and updates lists | |
| of them. | |
| """ | |
| switchbox.inputs = {} | |
| switchbox.outputs = {} | |
| # Top-level inputs and their locations. Indexed by pin names. | |
| input_locs = defaultdict(lambda: []) | |
| for stage_id, stage in switchbox.stages.items(): | |
| for switch_id, switch in stage.switches.items(): | |
| for mux_id, mux in switch.muxes.items(): | |
| # Add the mux output pin as top level output if necessary | |
| if mux.output.name is not None: | |
| loc = SwitchboxPinLoc( | |
| stage_id=stage.id, | |
| switch_id=switch.id, | |
| mux_id=mux.id, | |
| pin_id=0, | |
| pin_direction=PinDirection.OUTPUT | |
| ) | |
| if stage.type == "STREET": | |
| pin_type = SwitchboxPinType.LOCAL | |
| else: | |
| pin_type = SwitchboxPinType.HOP | |
| pin = SwitchboxPin( | |
| id=len(switchbox.outputs), | |
| name=mux.output.name, | |
| direction=PinDirection.OUTPUT, | |
| locs=[loc], | |
| type=pin_type | |
| ) | |
| assert pin.name not in switchbox.outputs, pin | |
| switchbox.outputs[pin.name] = pin | |
| # Add the mux input pins as top level inputs if necessary | |
| for pin in mux.inputs.values(): | |
| if pin.name is not None: | |
| loc = SwitchboxPinLoc( | |
| stage_id=stage.id, | |
| switch_id=switch.id, | |
| mux_id=mux.id, | |
| pin_id=pin.id, | |
| pin_direction=PinDirection.INPUT | |
| ) | |
| input_locs[pin.name].append(loc) | |
| # Add top-level input pins to the switchbox. | |
| keys = sorted(input_locs.keys(), key=lambda k: k[0]) | |
| for name, locs in {k: input_locs[k] for k in keys}.items(): | |
| # Determine the pin type | |
| is_hop = is_regular_hop_wire(name) | |
| _, hop = get_name_and_hop(name) | |
| if name in ["VCC", "GND"]: | |
| pin_type = SwitchboxPinType.CONST | |
| elif name.startswith("CAND"): | |
| pin_type = SwitchboxPinType.GCLK | |
| elif is_hop: | |
| pin_type = SwitchboxPinType.HOP | |
| elif hop is not None: | |
| pin_type = SwitchboxPinType.FOREIGN | |
| else: | |
| pin_type = SwitchboxPinType.LOCAL | |
| pin = SwitchboxPin( | |
| id=len(switchbox.inputs), | |
| name=name, | |
| direction=PinDirection.INPUT, | |
| locs=locs, | |
| type=pin_type | |
| ) | |
| assert pin.name not in switchbox.inputs, pin | |
| switchbox.inputs[pin.name] = pin | |
| return switchbox | |
| def parse_switchbox(xml_sbox, xml_common=None): | |
| """ | |
| Parses the switchbox definition from XML. Returns a Switchbox object | |
| """ | |
| switchbox = Switchbox(type=xml_sbox.tag) | |
| # Identify stages. Append stages from the "COMMON_STAGES" section if | |
| # given. | |
| stages = [n for n in xml_sbox if n.tag.startswith("STAGE")] | |
| if xml_common is not None: | |
| common_stages = [n for n in xml_common if n.tag.startswith("STAGE")] | |
| stages.extend(common_stages) | |
| # Load stages | |
| for xml_stage in stages: | |
| # Get stage id | |
| stage_id = int(xml_stage.attrib["StageNumber"]) | |
| assert stage_id not in switchbox.stages, ( | |
| stage_id, switchbox.stages.keys() | |
| ) | |
| stage_type = xml_stage.attrib["StageType"] | |
| # Add the new stage | |
| stage = Switchbox.Stage(id=stage_id, type=stage_type) | |
| switchbox.stages[stage_id] = stage | |
| # Process outputs | |
| switches = {} | |
| for xml_output in xml_stage.findall("Output"): | |
| out_switch_id = int(xml_output.attrib["SwitchNum"]) | |
| out_pin_id = int(xml_output.attrib["SwitchOutputNum"]) | |
| out_pin_name = xml_output.get("JointOutputName", None) | |
| # These indicate unconnected top-level output. | |
| if out_pin_name in ["-1"]: | |
| out_pin_name = None | |
| # Add a new switch if needed | |
| if out_switch_id not in switches: | |
| switches[out_switch_id] = Switchbox.Switch( | |
| out_switch_id, stage_id | |
| ) | |
| switch = switches[out_switch_id] | |
| # Add a mux for the output | |
| mux = Switchbox.Mux(out_pin_id, switch.id) | |
| assert mux.id not in switch.muxes, mux | |
| switch.muxes[mux.id] = mux | |
| # Add output pin to the mux | |
| mux.output = SwitchPin( | |
| id=0, name=out_pin_name, direction=PinDirection.OUTPUT | |
| ) | |
| # Process inputs | |
| for xml_input in xml_output: | |
| inp_pin_id = int(xml_input.tag.replace("Input", "")) | |
| inp_pin_name = xml_input.get("WireName", None) | |
| inp_hop_dir = xml_input.get("Direction", None) | |
| inp_hop_len = int(xml_input.get("Length", "-1")) | |
| # These indicate unconnected top-level input. | |
| if inp_pin_name in ["-1"]: | |
| inp_pin_name = None | |
| # Append the actual wire length and hop diretion to names of | |
| # pins that connect to HOP wires. | |
| is_hop = (inp_hop_dir in ["Left", "Right", "Top", "Bottom"]) | |
| if is_hop: | |
| inp_pin_name = "{}_{}{}".format( | |
| inp_pin_name, inp_hop_dir[0], inp_hop_len | |
| ) | |
| # Add the input to the mux | |
| pin = SwitchPin( | |
| id=inp_pin_id, | |
| name=inp_pin_name, | |
| direction=PinDirection.INPUT | |
| ) | |
| assert pin.id not in mux.inputs, pin | |
| mux.inputs[pin.id] = pin | |
| # Add internal connection | |
| if stage_type == "STREET" and stage_id > 0: | |
| conn_stage_id = int(xml_input.attrib["Stage"]) | |
| conn_switch_id = int(xml_input.attrib["SwitchNum"]) | |
| conn_pin_id = int(xml_input.attrib["SwitchOutputNum"]) | |
| conn = SwitchConnection( | |
| src=SwitchboxPinLoc( | |
| stage_id=conn_stage_id, | |
| switch_id=conn_switch_id, | |
| mux_id=conn_pin_id, | |
| pin_id=0, | |
| pin_direction=PinDirection.OUTPUT | |
| ), | |
| dst=SwitchboxPinLoc( | |
| stage_id=stage.id, | |
| switch_id=switch.id, | |
| mux_id=mux.id, | |
| pin_id=inp_pin_id, | |
| pin_direction=PinDirection.INPUT | |
| ), | |
| ) | |
| assert conn not in switchbox.connections, conn | |
| switchbox.connections.add(conn) | |
| # Add switches to the stage | |
| stage.switches = switches | |
| # Update top-level pins | |
| update_switchbox_pins(switchbox) | |
| return switchbox | |
| # ============================================================================= | |
| def parse_wire_mapping_table(xml_root, switchbox_grid, switchbox_types): | |
| """ | |
| Parses the "DeviceWireMappingTable" section. Returns a dict indexed by | |
| locations. | |
| """ | |
| def yield_locs_and_maps(): | |
| """ | |
| Yields locations and wire mappings associated with it. | |
| """ | |
| RE_LOC = re.compile(r"^(Row|Col)_([0-9]+)_([0-9]+)$") | |
| # Rows | |
| xml_rows = [e for e in xml_root if e.tag.startswith("Row_")] | |
| for xml_row in xml_rows: | |
| # Decode row range | |
| match = RE_LOC.match(xml_row.tag) | |
| assert match is not None, xml_row.tag | |
| row_beg = int(xml_row.attrib["RowStartNum"]) | |
| row_end = int(xml_row.attrib["RowEndNum"]) | |
| assert row_beg == int(match.group(2)), \ | |
| (xml_row.tag, row_beg, row_end) | |
| assert row_end == int(match.group(3)), \ | |
| (xml_row.tag, row_beg, row_end) | |
| # Columns | |
| xml_cols = [e for e in xml_row if e.tag.startswith("Col_")] | |
| for xml_col in xml_cols: | |
| # Decode column range | |
| match = RE_LOC.match(xml_col.tag) | |
| assert match is not None, xml_col.tag | |
| col_beg = int(xml_col.attrib["ColStartNum"]) | |
| col_end = int(xml_col.attrib["ColEndNum"]) | |
| assert col_beg == int(match.group(2)), \ | |
| (xml_col.tag, col_beg, col_end) | |
| assert col_end == int(match.group(3)), \ | |
| (xml_col.tag, col_beg, col_end) | |
| # Wire maps | |
| xml_maps = [e for e in xml_col if e.tag.startswith("Stage_")] | |
| # Yield wire maps for each location | |
| for y in range(row_beg, row_end + 1): | |
| for x in range(col_beg, col_end + 1): | |
| yield (Loc(x=x, y=y, z=0), xml_maps) | |
| # Process wire maps | |
| wire_maps = defaultdict(lambda: {}) | |
| RE_STAGE = re.compile(r"^Stage_([0-9])$") | |
| RE_JOINT = re.compile(r"^Join\.([0-9]+)\.([0-9]+)\.([0-9]+)$") | |
| RE_WIREMAP = re.compile( | |
| r"^WireMap\.(Top|Bottom|Left|Right)\.Length_([0-9])\.(.*)$" | |
| ) | |
| for loc, xml_maps in yield_locs_and_maps(): | |
| for xml_map in xml_maps: | |
| # Decode stage id | |
| match = RE_STAGE.match(xml_map.tag) | |
| assert match is not None, xml_map.tag | |
| stage_id = int(xml_map.attrib["StageNumber"]) | |
| assert stage_id == int(match.group(1)), \ | |
| (xml_map.tag, stage_id) | |
| # Decode wire joints | |
| joints = { | |
| k: v | |
| for k, v in xml_map.attrib.items() | |
| if k.startswith("Join.") | |
| } | |
| for joint_key, joint_map in joints.items(): | |
| # Decode the joint key | |
| match = RE_JOINT.match(joint_key) | |
| assert match is not None, joint_key | |
| pin_loc = SwitchboxPinLoc( | |
| stage_id=stage_id, | |
| switch_id=int(match.group(1)), | |
| mux_id=int(match.group(2)), | |
| pin_id=int(match.group(3)), | |
| pin_direction=PinDirection. | |
| INPUT # FIXME: Are those always inputs ? | |
| ) | |
| # Decode the wire name | |
| match = RE_WIREMAP.match(joint_map) | |
| assert match is not None, joint_map | |
| wire_hop_dir = match.group(1) | |
| wire_hop_len = int(match.group(2)) | |
| wire_name = match.group(3) | |
| # Compute location of the tile that the wire is connected to | |
| if wire_hop_dir == "Top": | |
| tile_loc = Loc(x=loc.x, y=loc.y - wire_hop_len, z=0) | |
| elif wire_hop_dir == "Bottom": | |
| tile_loc = Loc(x=loc.x, y=loc.y + wire_hop_len, z=0) | |
| elif wire_hop_dir == "Left": | |
| tile_loc = Loc(x=loc.x - wire_hop_len, y=loc.y, z=0) | |
| elif wire_hop_dir == "Right": | |
| tile_loc = Loc(x=loc.x + wire_hop_len, y=loc.y, z=0) | |
| else: | |
| assert False, wire_hop_dir | |
| # Append to the map | |
| wire_maps[loc][pin_loc] = (wire_name, tile_loc) | |
| return wire_maps | |
| def parse_port_mapping_table(xml_root, switchbox_grid): | |
| """ | |
| Parses switchbox port mapping tables. Returns a dict indexed by locations | |
| containing a dict with switchbox port to tile port name correspondence. | |
| """ | |
| port_maps = defaultdict(lambda: {}) | |
| # Sections are named "*_Table" | |
| xml_tables = [e for e in xml_root if e.tag.endswith("_Table")] | |
| for xml_table in xml_tables: | |
| # Get the origin | |
| origin = xml_table.tag.split("_")[0] | |
| assert origin in ["Left", "Right", "Top", "Bottom"], origin | |
| # Get switchbox types affected by the mapping | |
| sbox_types_xml = xml_table.find("SBoxTypes") | |
| assert sbox_types_xml is not None | |
| switchbox_types = set( | |
| [ | |
| v for k, v in sbox_types_xml.attrib.items() | |
| if k.startswith("type") | |
| ] | |
| ) | |
| # Get their locations | |
| locs = [ | |
| loc for loc, type in switchbox_grid.items() | |
| if type in switchbox_types | |
| ] | |
| # Get the first occurrence of a switchbox with one of considered types | |
| # that is closes to the (0, 0) according to manhattan distance. | |
| base_loc = None | |
| for loc in locs: | |
| if not base_loc: | |
| base_loc = loc | |
| elif (loc.x + loc.y) < (base_loc.x + base_loc.y): | |
| base_loc = loc | |
| # Parse the port mapping table(s) | |
| for port_mapping_xml in xml_table.findall("PortMappingTable"): | |
| # Get the direction of the switchbox offset | |
| orientation = port_mapping_xml.attrib["Orientation"] | |
| if orientation == "Horizontal": | |
| assert origin in ["Top", "Bottom"], (origin, orientation) | |
| dx, dy = (+1, 0) | |
| elif orientation == "Vertical": | |
| assert origin in ["Left", "Right"], (origin, orientation) | |
| dx, dy = (0, +1) | |
| # Process the mapping of switchbox output ports | |
| for index_xml in port_mapping_xml.findall("Index"): | |
| pin_name = index_xml.attrib["Mapped_Interface_Name"] | |
| output_num = index_xml.attrib["SwitchOutputNum"] | |
| # Determine the mapped port direction | |
| if output_num == "-1": | |
| pin_direction = PinDirection.INPUT | |
| else: | |
| pin_direction = PinDirection.OUTPUT | |
| sbox_xmls = [e for e in index_xml if e.tag.startswith("SBox")] | |
| for sbox_xml in sbox_xmls: | |
| offset = int(sbox_xml.attrib["Offset"]) | |
| mapped_name = sbox_xml.get("MTB_PortName", None) | |
| # "-1" means unconnected | |
| if mapped_name == "-1": | |
| mapped_name = None | |
| # Get the location for the map | |
| loc = Loc( | |
| x=base_loc.x + dx * offset, | |
| y=base_loc.y + dy * offset, | |
| z=0 | |
| ) | |
| # Append mapping | |
| key = (pin_name, pin_direction) | |
| assert key not in port_maps[loc], (loc, key) | |
| port_maps[loc][key] = mapped_name | |
| # Make a normal dict | |
| port_maps = dict(port_maps) | |
| return port_maps | |
| # ============================================================================= | |
| def parse_clock_network(xml_clock_network): | |
| """ | |
| Parses the "CLOCK_NETWORK" section of the techfile | |
| """ | |
| def parse_cell(xml_cell, quadrant=None): | |
| """ | |
| Parses a "Cell" tag inside "CLOCK_NETWORK" | |
| """ | |
| NON_PIN_TAGS = ("name", "type", "row", "column") | |
| cell_loc = Loc( | |
| x=int(xml_cell.attrib["column"]), | |
| y=int(xml_cell.attrib["row"]), | |
| z=0 | |
| ) | |
| # Get the cell's pinmap | |
| pin_map = { | |
| k: v | |
| for k, v in xml_cell.attrib.items() | |
| if k not in NON_PIN_TAGS | |
| } | |
| # Return the cell | |
| return ClockCell( | |
| type=xml_cell.attrib["type"], | |
| name=xml_cell.attrib["name"], | |
| loc=cell_loc, | |
| quadrant=quadrant, | |
| pin_map=pin_map | |
| ) | |
| clock_cells = {} | |
| # Parse GMUX cells | |
| xml_gmux = xml_clock_network.find("GMUX") | |
| assert xml_gmux is not None | |
| for xml_cell in xml_gmux.findall("Cell"): | |
| clock_cell = parse_cell(xml_cell) | |
| clock_cells[clock_cell.name] = clock_cell | |
| # Parse QMUX cells | |
| xml_qmux = xml_clock_network.find("QMUX") | |
| assert xml_qmux is not None | |
| for xml_quad in xml_qmux: | |
| for xml_cell in xml_quad.findall("Cell"): | |
| clock_cell = parse_cell(xml_cell, xml_quad.tag) | |
| clock_cells[clock_cell.name] = clock_cell | |
| # Parse CAND cells | |
| xml_cand = xml_clock_network.find("COL_CLKEN") | |
| assert xml_cand is not None | |
| for xml_quad in xml_cand: | |
| for xml_cell in xml_quad.findall("Cell"): | |
| clock_cell = parse_cell(xml_cell, xml_quad.tag) | |
| clock_cells[clock_cell.name] = clock_cell | |
| # Since we are not going to use dynamic enables on CAND we remove the EN | |
| # pin connection from the pinmap. This way the connections between | |
| # switchboxes which drive them can be used for generic routing. | |
| for cell_name in clock_cells.keys(): | |
| cell = clock_cells[cell_name] | |
| pin_map = cell.pin_map | |
| if cell.type == "CAND" and "EN" in pin_map: | |
| del pin_map["EN"] | |
| clock_cells[cell_name] = ClockCell( | |
| name=cell.name, | |
| type=cell.type, | |
| loc=cell.loc, | |
| quadrant=cell.quadrant, | |
| pin_map=pin_map | |
| ) | |
| return clock_cells | |
| def populate_clk_mux_port_maps( | |
| port_maps, clock_cells, tile_grid, cells_library | |
| ): | |
| """ | |
| Converts global clock network cells port mappings and appends them to | |
| the port_maps used later for switchbox specialization. | |
| """ | |
| for clock_cell in clock_cells.values(): | |
| cell_type = clock_cell.type | |
| loc = clock_cell.loc | |
| # Find the cell in the cells_library to get its pin definitions | |
| assert cell_type in cells_library, cell_type | |
| cell_pins = cells_library[cell_type].pins | |
| # Find the cell in a tile | |
| tile = tile_grid[loc] | |
| cell = find_cell_in_tile(clock_cell.name, tile) | |
| # Add the mux location to the port map | |
| if loc not in port_maps: | |
| port_maps[loc] = {} | |
| # Add map entries | |
| for mux_pin_name, sbox_pin_name in clock_cell.pin_map.items(): | |
| # Get the pin definition to get its driection. | |
| cell_pin = [p for p in cell_pins if p.name == mux_pin_name] | |
| assert len(cell_pin) == 1, (clock_cell, mux_pin_name) | |
| cell_pin = cell_pin[0] | |
| # Skip hard-wired pins | |
| if cell_pin.attrib.get("hardWired", None) == "true": | |
| continue | |
| # Add entry to the map | |
| key = (sbox_pin_name, OPPOSITE_DIRECTION[cell_pin.direction]) | |
| assert key not in port_maps[loc], (port_maps[loc], key) | |
| port_maps[loc][key] = "{}{}_{}".format( | |
| cell.type, cell.index, mux_pin_name | |
| ) | |
| # ============================================================================= | |
| def specialize_switchboxes_with_port_maps( | |
| switchbox_types, switchbox_grid, port_maps | |
| ): | |
| """ | |
| Specializes switchboxes by applying port mapping. | |
| """ | |
| for loc, port_map in port_maps.items(): | |
| # No switchbox at that location | |
| if loc not in switchbox_grid: | |
| continue | |
| # Get the switchbox type | |
| switchbox_type = switchbox_grid[loc] | |
| switchbox = switchbox_types[switchbox_type] | |
| # Make a copy of the switchbox | |
| suffix = "X{}Y{}".format(loc.x, loc.y) | |
| if not switchbox.type.endswith(suffix): | |
| new_type = "{}_{}".format(switchbox.type, suffix) | |
| else: | |
| new_type = switchbox_type | |
| new_switchbox = Switchbox(new_type) | |
| new_switchbox.stages = deepcopy(switchbox.stages) | |
| new_switchbox.connections = deepcopy(switchbox.connections) | |
| # Remap pin names | |
| did_remap = False | |
| for stage, switch, mux in yield_muxes(new_switchbox): | |
| # Remap output | |
| alt_name = "{}.{}.{}".format(stage.id, switch.id, mux.id) | |
| pin = mux.output | |
| keys = ((pin.name, pin.direction), (alt_name, pin.direction)) | |
| for key in keys: | |
| if key in port_map: | |
| did_remap = True | |
| mux.output = SwitchPin( | |
| id=pin.id, | |
| name=port_map[key], | |
| direction=pin.direction, | |
| ) | |
| break | |
| # Remap inputs | |
| for pin in mux.inputs.values(): | |
| key = (pin.name, pin.direction) | |
| if key in port_map: | |
| did_remap = True | |
| mux.inputs[pin.id] = SwitchPin( | |
| id=pin.id, | |
| name=port_map[key], | |
| direction=pin.direction, | |
| ) | |
| # Nothing remapped, discard the new switchbox | |
| if not did_remap: | |
| continue | |
| # Update top-level pins | |
| update_switchbox_pins(new_switchbox) | |
| # Add to the switchbox types and the grid | |
| switchbox_types[new_switchbox.type] = new_switchbox | |
| switchbox_grid[loc] = new_switchbox.type | |
| def specialize_switchboxes_with_wire_maps( | |
| switchbox_types, switchbox_grid, port_maps, wire_maps | |
| ): | |
| """ | |
| Specializes switchboxes by applying wire mapping. | |
| """ | |
| for loc, wire_map in wire_maps.items(): | |
| # No switchbox at that location | |
| if loc not in switchbox_grid: | |
| continue | |
| # Get the switchbox type | |
| switchbox_type = switchbox_grid[loc] | |
| switchbox = switchbox_types[switchbox_type] | |
| # Make a copy of the switchbox | |
| suffix = "X{}Y{}".format(loc.x, loc.y) | |
| if not switchbox.type.endswith(suffix): | |
| new_type = "{}_{}".format(switchbox.type, suffix) | |
| else: | |
| new_type = switchbox_type | |
| new_switchbox = Switchbox(new_type) | |
| new_switchbox.stages = deepcopy(switchbox.stages) | |
| new_switchbox.connections = deepcopy(switchbox.connections) | |
| # Remap pin names | |
| did_remap = False | |
| for pin_loc, (wire_name, map_loc) in wire_map.items(): | |
| # Get port map at the destination location of the wire that is | |
| # being remapped. | |
| assert map_loc in port_maps, (map_loc, wire_name) | |
| port_map = port_maps[map_loc] | |
| # Get the actual tile pin name | |
| key = (wire_name, PinDirection.INPUT) | |
| assert key in port_map, (map_loc, key) | |
| pin_name = port_map[key] | |
| # Append the hop to the wire name. Only if the map indicates that | |
| # the pin is connected. | |
| if pin_name is not None: | |
| hop = ( | |
| map_loc.x - loc.x, | |
| map_loc.y - loc.y, | |
| ) | |
| pin_name += "_{}".format(hop_to_str(hop)) | |
| # Rename pin | |
| stage = new_switchbox.stages[pin_loc.stage_id] | |
| switch = stage.switches[pin_loc.switch_id] | |
| mux = switch.muxes[pin_loc.mux_id] | |
| pin = mux.inputs[pin_loc.pin_id] | |
| new_pin = SwitchPin( | |
| id=pin.id, direction=pin.direction, name=pin_name | |
| ) | |
| mux.inputs[new_pin.id] = new_pin | |
| did_remap = True | |
| # Nothing remapped, discard the new switchbox | |
| if not did_remap: | |
| continue | |
| # Update top-level pins | |
| update_switchbox_pins(new_switchbox) | |
| # Add to the switchbox types and the grid | |
| switchbox_types[new_switchbox.type] = new_switchbox | |
| switchbox_grid[loc] = new_switchbox.type | |
| # ============================================================================= | |
| def find_special_cells(tile_grid): | |
| """ | |
| Finds cells that occupy more than one tilegrid location. | |
| """ | |
| cells = {} | |
| # Assign each cell name its locations. | |
| for loc, tile in tile_grid.items(): | |
| for cell_type, cell_names in tile.cell_names.items(): | |
| for cell_name, in cell_names: | |
| # Skip LOGIC as it is always contained in a single tile | |
| if cell_name == "LOGIC": | |
| continue | |
| if cell_name not in cells: | |
| cells[cell_name] = {"type": cell_type, "locs": [loc]} | |
| else: | |
| cells[cell_name]["locs"].append(loc) | |
| # Leave only those that have more than one location | |
| cells = {k: v for k, v in cells.items() if len(v["locs"]) > 1} | |
| def parse_pinmap(xml_root, tile_grid): | |
| """ | |
| Parses the "Package" section that holds IO pin to BIDIR/CLOCK cell map. | |
| Returns a dict indexed by package name which holds dicts indexed by logical | |
| pin names (eg. "FBIO_1"). Then, for each logical pin name there is a list | |
| of PackagePin objects. | |
| """ | |
| pin_map = {} | |
| # Parse "PACKAGE" sections. | |
| for xml_package in xml_root.findall("PACKAGE"): | |
| # Initialize map | |
| pkg_name = xml_package.attrib["name"] | |
| pkg_pin_map = defaultdict(lambda: set()) | |
| xml_pins = xml_package.find("Pins") | |
| assert xml_pins is not None | |
| # Parse pins | |
| for xml_pin in xml_pins.findall("Pin"): | |
| pin_name = xml_pin.attrib["name"] | |
| pin_alias = xml_pin.get("alias", None) | |
| cell_names = [] | |
| cell_locs = [] | |
| # Parse cells | |
| for xml_cell in xml_pin.findall("cell"): | |
| cell_name = xml_cell.attrib["name"] | |
| cell_loc = get_loc_of_cell(cell_name, tile_grid) | |
| if cell_loc is None: | |
| continue | |
| cell_names.append(cell_name) | |
| cell_locs.append(cell_loc) | |
| # Location not found | |
| if not cell_locs: | |
| print( | |
| "WARNING: No locs for package pin '{}' of package '{}'". | |
| format(pin_name, pkg_name) | |
| ) | |
| continue | |
| # Add the pin mapping | |
| for cell_name, cell_loc in zip(cell_names, cell_locs): | |
| # Find the cell | |
| if cell_loc not in tile_grid: | |
| print( | |
| "WARNING: No tile for package pin '{}' at '{}'".format( | |
| pin_name, cell_loc | |
| ) | |
| ) | |
| continue | |
| tile = tile_grid[cell_loc] | |
| cell = find_cell_in_tile(cell_name, tile) | |
| if cell is None: | |
| print( | |
| "WARNING: No cell in tile '{}' for package pin '{}' at '{}'" | |
| .format(tile.name, pin_name, cell_loc) | |
| ) | |
| continue | |
| # Store the mapping | |
| pkg_pin_map[pin_name].add( | |
| PackagePin( | |
| name=pin_name, | |
| alias=pin_alias, | |
| loc=cell_loc, | |
| cell=cell | |
| ) | |
| ) | |
| # Check if there is a CLOCK cell at the same location | |
| cells = [c for c in tile.cells if c.type == "CLOCK"] | |
| if len(cells): | |
| assert len(cells) == 1, cells | |
| # Store the mapping for the CLOCK cell | |
| pkg_pin_map[pin_name].add( | |
| PackagePin( | |
| name=pin_name, | |
| alias=pin_alias, | |
| loc=cell_loc, | |
| cell=cells[0] | |
| ) | |
| ) | |
| # Convert to list | |
| pkg_pin_map[pin_name] = list(pkg_pin_map[pin_name]) | |
| # Convert to a regular dict | |
| pin_map[pkg_name] = dict(**pkg_pin_map) | |
| return pin_map | |
| # ============================================================================= | |
| def import_data(xml_root): | |
| """ | |
| Imports the Quicklogic FPGA tilegrid and routing data from the given | |
| XML tree | |
| """ | |
| # Get the "Library" section | |
| xml_library = xml_root.find("Library") | |
| assert xml_library is not None | |
| # Import cells from the library | |
| cells = parse_library(xml_library) | |
| # Get the "Placement" section | |
| xml_placement = xml_root.find("Placement") | |
| assert xml_placement is not None | |
| cells_library = {cell.type: cell for cell in cells} | |
| quadrants, tile_types, tile_grid = parse_placement( | |
| xml_placement, cells_library | |
| ) | |
| # Import global clock network definition | |
| xml_clock_network = xml_placement.find("CLOCK_NETWORK") | |
| assert xml_clock_network is not None | |
| clock_cells = parse_clock_network(xml_clock_network) | |
| # Get the "Routing" section | |
| xml_routing = xml_root.find("Routing") | |
| assert xml_routing is not None | |
| # Import switchboxes | |
| switchbox_grid = {} | |
| switchbox_types = {} | |
| for xml_node in xml_routing: | |
| # Not a switchbox | |
| if not xml_node.tag.endswith("_SBOX"): | |
| continue | |
| # Load all "variants" of the switchbox | |
| xml_common = xml_node.find("COMMON_STAGES") | |
| for xml_sbox in xml_node: | |
| if xml_sbox != xml_common: | |
| # Parse the switchbox definition | |
| switchbox = parse_switchbox(xml_sbox, xml_common) | |
| assert switchbox.type not in switchbox_types, switchbox.type | |
| switchbox_types[switchbox.type] = switchbox | |
| # Populate switchboxes onto the tilegrid | |
| populate_switchboxes(xml_sbox, switchbox_grid) | |
| # Get the "DeviceWireMappingTable" section | |
| xml_wiremap = xml_routing.find("DeviceWireMappingTable") | |
| if xml_wiremap is not None: | |
| # Import wire mapping | |
| wire_maps = parse_wire_mapping_table( | |
| xml_wiremap, switchbox_grid, switchbox_types | |
| ) | |
| # Get the "DevicePortMappingTable" section | |
| xml_portmap = xml_routing.find("DevicePortMappingTable") | |
| assert xml_portmap is not None | |
| # Import switchbox port mapping | |
| port_maps = parse_port_mapping_table(xml_portmap, switchbox_grid) | |
| # Supply port mapping table with global clock mux map | |
| populate_clk_mux_port_maps( | |
| port_maps, clock_cells, tile_grid, cells_library | |
| ) | |
| if xml_wiremap is not None: | |
| # Specialize switchboxes with wire maps | |
| specialize_switchboxes_with_wire_maps( | |
| switchbox_types, switchbox_grid, port_maps, wire_maps | |
| ) | |
| # Specialize switchboxes with local port maps | |
| specialize_switchboxes_with_port_maps( | |
| switchbox_types, switchbox_grid, port_maps | |
| ) | |
| # Remove switchbox types not present in the grid anymore due to their | |
| # specialization. | |
| for type in list(switchbox_types.keys()): | |
| if type not in switchbox_grid.values(): | |
| del switchbox_types[type] | |
| # Get the "Packages" section | |
| xml_packages = xml_root.find("Packages") | |
| assert xml_packages is not None | |
| # Import BIDIR cell names to package pin mapping | |
| package_pinmaps = parse_pinmap(xml_packages, tile_grid) | |
| return { | |
| "quadrants": quadrants, | |
| "cells_library": cells_library, | |
| "tile_types": tile_types, | |
| "tile_grid": tile_grid, | |
| "switchbox_types": switchbox_types, | |
| "switchbox_grid": switchbox_grid, | |
| "clock_cells": clock_cells, | |
| "package_pinmaps": package_pinmaps | |
| } | |
| # ============================================================================= | |
| def import_routing_timing(csv_file): | |
| """ | |
| Reads and parses switchbox delay data from the CSV file. Returns a tree of | |
| dicts indexed by: | |
| [switchbox_type][stage_id][switch_id][mux_id][pin_id][num_inputs]. | |
| The last dict holds tuples with rise and fall delays. Delay values are | |
| expressed in [ns]. | |
| """ | |
| # Read and parse CSV | |
| with open(csv_file, "r") as fp: | |
| # Read the first line, it should specify timing units | |
| line = fp.readline() | |
| line = line.strip().split(",") | |
| assert len(line) >= 3, line | |
| assert line[0] == "unit", line[0] | |
| # FIXME: For now support "ns" only | |
| assert line[2] == "ns", line[2] | |
| scale = 1.0 # Set the timing scale to 1ns | |
| # Read the rest of the timing data | |
| data = [r for r in csv.DictReader(fp)] | |
| # Try to guess whether the stage/switch/mux indexing starts from 0 or 1 | |
| stage_ofs = min([int(t["Stage_Num"]) for t in data]) | |
| switch_ofs = min([int(t["Switch_Num"]) for t in data]) | |
| mux_ofs = min([int(t["Output_Num"]) for t in data]) | |
| pin_ofs = min([int(t["Input_Num"]) for t in data]) | |
| # Reformat | |
| switchbox_timings = {} | |
| for timing in data: | |
| # Switchbox type | |
| switchbox_type = timing["SBox_Type"] | |
| if switchbox_type not in switchbox_timings: | |
| switchbox_timings[switchbox_type] = {} | |
| # Stage id | |
| stage_timings = switchbox_timings[switchbox_type] | |
| stage_id = int(timing["Stage_Num"]) - stage_ofs | |
| if stage_id not in stage_timings: | |
| stage_timings[stage_id] = {} | |
| # Switch id | |
| switch_timings = stage_timings[stage_id] | |
| switch_id = int(timing["Switch_Num"]) - switch_ofs | |
| if switch_id not in switch_timings: | |
| switch_timings[switch_id] = {} | |
| # Mux id | |
| mux_timings = switch_timings[switch_id] | |
| mux_id = int(timing["Output_Num"]) - mux_ofs | |
| if mux_id not in mux_timings: | |
| mux_timings[mux_id] = {} | |
| # Mux route (edge), correspond to its input pin id. | |
| edge_timings = mux_timings[mux_id] | |
| pin_id = int(timing["Input_Num"]) - pin_ofs | |
| if pin_id not in edge_timings: | |
| edge_timings[pin_id] = {} | |
| # Load count and delays. Apply scaling so the timing is expressed | |
| # always is nanoseconds. | |
| edge_timing = edge_timings[pin_id] | |
| num_loads = int(timing["Num_Loads"]) | |
| rise_delay = float(timing["Rise_Delay"]) * scale | |
| fall_delay = float(timing["Fall_Delay"]) * scale | |
| edge_timing[num_loads] = (rise_delay, fall_delay) | |
| return switchbox_timings | |
| # ============================================================================= | |
| def main(): | |
| # Parse arguments | |
| parser = argparse.ArgumentParser( | |
| description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter | |
| ) | |
| parser.add_argument( | |
| "--techfile", | |
| type=str, | |
| required=True, | |
| help="Quicklogic 'TechFile' XML file" | |
| ) | |
| parser.add_argument( | |
| "--routing-timing", | |
| type=str, | |
| default=None, | |
| help="Quicklogic routing delay CSV file" | |
| ) | |
| parser.add_argument( | |
| "--db", | |
| type=str, | |
| default="phy_database.pickle", | |
| help="Device name for the parsed 'database' file" | |
| ) | |
| args = parser.parse_args() | |
| # Read and parse the XML techfile | |
| xml_tree = ET.parse(args.techfile) | |
| xml_techfile = xml_tree.getroot() | |
| # Load data from the techfile | |
| data = import_data(xml_techfile) | |
| # Build the connection map | |
| connections = build_connections( | |
| data["tile_types"], | |
| data["tile_grid"], | |
| data["switchbox_types"], | |
| data["switchbox_grid"], | |
| data["clock_cells"], | |
| ) | |
| check_connections(connections) | |
| # Load timing data if given | |
| if args.routing_timing is not None: | |
| switchbox_timing = import_routing_timing(args.routing_timing) | |
| else: | |
| switchbox_timing = None | |
| # Prepare the database | |
| db_root = { | |
| "phy_quadrants": data["quadrants"], | |
| "cells_library": data["cells_library"], | |
| "tile_types": data["tile_types"], | |
| "phy_tile_grid": data["tile_grid"], | |
| "phy_clock_cells": data["clock_cells"], | |
| "switchbox_types": data["switchbox_types"], | |
| "switchbox_grid": data["switchbox_grid"], | |
| "connections": connections, | |
| "package_pinmaps": data["package_pinmaps"] | |
| } | |
| if switchbox_timing is not None: | |
| db_root["switchbox_timing"] = switchbox_timing | |
| # FIXME: Use something more platform-independent than pickle. | |
| with open(args.db, "wb") as fp: | |
| pickle.dump(db_root, fp, protocol=3) | |
| # ============================================================================= | |
| if __name__ == "__main__": | |
| main() |