| #!/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"] |
| |
| # Skip this index - empty switchbox |
| if (pin_name == "-1"): |
| continue |
| |
| # 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() |