blob: 291c2142f737724c39daba5d57e44c6903ef9b5c [file] [log] [blame] [edit]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 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
import argparse
import pickle
import re
from collections import defaultdict, namedtuple
import fasm
from f4pga.utils.quicklogic.pp3.connections import get_name_and_hop
from pathlib import Path
from f4pga.utils.quicklogic.pp3.data_structs import Loc, SwitchboxPinLoc, PinDirection, ConnectionType
from f4pga.utils.quicklogic.pp3.utils import get_quadrant_for_loc
from f4pga.utils.quicklogic.pp3.verilogmodule import VModule
from quicklogic_fasm.qlfasm import load_quicklogic_database, get_db_dir
from quicklogic_fasm.qlfasm import QL732BAssembler
Feature = namedtuple("Feature", "loc typ signature value")
RouteEntry = namedtuple("RouteEntry", "typ stage_id switch_id mux_id sel_id")
MultiLocCellMapping = namedtuple("MultiLocCellMapping", "typ fromlocset toloc pinnames")
class Fasm2Bels(object):
"""Class for parsing FASM file and producing BEL representation.
It takes FASM lines and VPR database and converts the data to Basic
Elements and connections between them. It allows converting this data to
Verilog.
"""
class Fasm2BelsException(Exception):
"""Exception for Fasm2Bels errors and unsupported features."""
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
def __init__(self, phy_db, device_name, package_name):
"""Prepares required structures for converting FASM to BELs.
Parameters
----------
phy_db: dict
A dictionary containing cell_library, loc_map, vpr_tile_types,
vpr_tile_grid, vpr_switchbox_types, vpr_switchbox_grid,
connections, vpr_package_pinmaps
"""
# load phy_db data
self.quadrants = phy_db["phy_quadrants"]
self.cells_library = phy_db["cells_library"]
self.vpr_tile_types = phy_db["tile_types"]
self.vpr_tile_grid = phy_db["phy_tile_grid"]
self.vpr_switchbox_types = phy_db["switchbox_types"]
self.vpr_switchbox_grid = phy_db["switchbox_grid"]
self.connections = phy_db["connections"]
self.device_name = device_name
self.package_name = package_name
self.io_to_fbio = dict()
if self.package_name not in db["package_pinmaps"]:
raise self.Fasm2BelsException(
"ERROR: '{}' is not a vaild package for device '{}'. Valid ones are: {}".format(
self.package_name, self.device_name, ", ".join(db["package_pinmaps"].keys())
)
)
for name, package in db["package_pinmaps"][self.package_name].items():
self.io_to_fbio[package[0].loc] = name
# Add ASSP to all locations it covers
# TODO maybe this should be added in original vpr_tile_grid
# set all cels in row 1 and column 2 to ASSP
# In VPR grid, the ASSP tile is located in (1, 1)
assplocs = set()
ramlocs = dict()
multlocs = dict()
for phy_loc, tile in self.vpr_tile_grid.items():
tile_type = self.vpr_tile_types[tile.type]
if "ASSP" in tile_type.cells:
assplocs.add(phy_loc)
if "RAM" in tile_type.cells:
ramcell = [cell for cell in tile.cells if cell.type == "RAM"]
cellname = ramcell[0].name
if cellname not in ramlocs:
ramlocs[cellname] = set()
ramlocs[cellname].add(phy_loc)
if "MULT" in tile_type.cells:
multcell = [cell for cell in tile.cells if cell.type == "MULT"]
cellname = multcell[0].name
if cellname not in multlocs:
multlocs[cellname] = set()
multlocs[cellname].add(phy_loc)
# this map represents the mapping from input name to its inverter name
self.inversionpins = {
"LOGIC": {
"TA1": "TAS1",
"TA2": "TAS2",
"TB1": "TBS1",
"TB2": "TBS2",
"BA1": "BAS1",
"BA2": "BAS2",
"BB1": "BBS1",
"BB2": "BBS2",
"QCK": "QCKS",
}
}
# prepare helper structure for connections
self.connections_by_loc = defaultdict(list)
for connection in self.connections:
self.connections_by_loc[connection.dst].append(connection)
self.connections_by_loc[connection.src].append(connection)
# a mapping from the type of cell FASM line refers to to its parser
self.featureparsers = {
"LOGIC": self.parse_logic_line,
"QMUX": self.parse_logic_line,
"GMUX": self.parse_logic_line,
"INTERFACE": self.parse_interface_line,
"ROUTING": self.parse_routing_line,
"CAND0": self.parse_colclk_line,
"CAND1": self.parse_colclk_line,
"CAND2": self.parse_colclk_line,
"CAND3": self.parse_colclk_line,
"CAND4": self.parse_colclk_line,
"RAM": self.parse_ram_line,
}
# a mapping from cell type to a set of possible pin names
self.pinnames = defaultdict(set)
for celltype in self.cells_library.values():
typ = celltype.type
for pin in celltype.pins:
self.pinnames[typ].add(pin.name)
# a mapping from cell types that occupy multiple locations
# to a single location
self.multiloccells = {"ASSP": MultiLocCellMapping("ASSP", assplocs, Loc(1, 1, 0), self.pinnames["ASSP"])}
for ram in ramlocs:
self.multiloccells[ram] = MultiLocCellMapping(
ram, ramlocs[ram], list(ramlocs[ram])[0], self.pinnames["RAM"]
)
for mult in multlocs:
self.multiloccells[mult] = MultiLocCellMapping(
mult, multlocs[mult], list(multlocs[mult])[1], self.pinnames["MULT"]
)
# helper routing data
self.routingdata = defaultdict(list)
# a dictionary holding bit settings for BELs
self.belinversions = defaultdict(lambda: defaultdict(list))
# a dictionary holding bit settings for IOs
self.interfaces = defaultdict(lambda: defaultdict(list))
# a dictionary holding simplified connections between BELs
self.designconnections = defaultdict(dict)
# a dictionary holding hops from routing
self.designhops = defaultdict(dict)
# Clock column drivers (CAND) data
self.colclk_data = defaultdict(lambda: defaultdict(list))
# A map of clock wires that connect to switchboxes
self.cand_map = defaultdict(lambda: dict())
# A map of original (loc, pin) to new (loc, pin). Created during
# aggregation of multi-loc cells.
self.org_loc_map = {}
def parse_logic_line(self, feature: Feature):
"""Parses a setting for a BEL.
Parameters
----------
feature: Feature
FASM line for BEL
"""
belname, setting = feature.signature.split(".", 1)
if feature.value == 1:
# FIXME handle ZINV pins
if "ZINV." in setting:
setting = setting.replace("ZINV.", "")
elif "INV." in setting:
setting = setting.replace("INV.", "")
self.belinversions[feature.loc][belname].append(setting)
def parse_interface_line(self, feature: Feature):
"""Parses a setting for IO.
Parameters
----------
feature: Feature
FASM line for BEL
"""
belname, setting = feature.signature.split(".", 1)
if feature.value == 1:
setting = setting.replace("ZINV.", "")
setting = setting.replace("INV.", "")
self.interfaces[feature.loc][belname].append(setting)
def parse_routing_line(self, feature: Feature):
"""Parses a routing setting.
Parameters
----------
feature: Feature
FASM line for BEL
"""
match = re.match(r"^I_highway\.IM(?P<switch_id>[0-9]+)\.I_pg(?P<sel_id>[0-9]+)$", feature.signature)
if match:
typ = "HIGHWAY"
stage_id = 3 # FIXME: Get HIGHWAY stage id from the switchbox def
switch_id = int(match.group("switch_id"))
mux_id = 0
sel_id = int(match.group("sel_id"))
match = re.match(
r"^I_street\.Isb(?P<stage_id>[0-9])(?P<switch_id>[0-9])\.I_M(?P<mux_id>[0-9]+)\.I_pg(?P<sel_id>[0-9]+)$", # noqa: E501
feature.signature,
)
if match:
typ = "STREET"
stage_id = int(match.group("stage_id")) - 1
switch_id = int(match.group("switch_id")) - 1
mux_id = int(match.group("mux_id"))
sel_id = int(match.group("sel_id"))
self.routingdata[feature.loc].append(
RouteEntry(typ=typ, stage_id=stage_id, switch_id=switch_id, mux_id=mux_id, sel_id=sel_id)
)
def parse_colclk_line(self, feature: Feature):
self.colclk_data[feature.loc][feature.typ].append(feature)
def parse_ram_line(self, feature: Feature):
"""Parses a RAM line.
Parameters
----------
feature: Feature
FASM line for BEL
"""
raise NotImplementedError("Parsing RAM FASM lines is not supported")
def parse_fasm_lines(self, fasmlines):
"""Parses FASM lines.
Parameters
----------
fasmlines: list
A list of FasmLine objects
"""
loctyp = re.compile(r"^X(?P<x>[0-9]+)Y(?P<y>[0-9]+)\.(?P<type>[A-Z]+[0-4]?)\.(?P<signature>.*)$") # noqa: E501
for line in fasmlines:
if not line.set_feature:
continue
match = loctyp.match(line.set_feature.feature)
if not match:
raise self.Fasm2BelsException(
f"FASM features have unsupported format: {line.set_feature}"
) # noqa: E501
loc = Loc(x=int(match.group("x")), y=int(match.group("y")), z=0)
typ = match.group("type")
feature = Feature(loc=loc, typ=typ, signature=match.group("signature"), value=line.set_feature.value)
self.featureparsers[typ](feature)
def decode_switchbox(self, switchbox, features):
"""Decodes all switchboxes to extract full connections' info.
For every output, this method determines its input in the routing
switchboxes. In this representation, an input and output can be either
directly connected to a BEL, or to a hop wire.
Parameters
----------
switchbox: a Switchbox object from vpr_switchbox_types
features: features regarding given switchbox
Returns
-------
dict: a mapping from output pin to input pin for a given switchbox
"""
# Group switchbox connections by destinationa
conn_by_dst = defaultdict(set)
for c in switchbox.connections:
conn_by_dst[c.dst].add(c)
# Prepare data structure
mux_sel = {}
for stage_id, stage in switchbox.stages.items():
mux_sel[stage_id] = {}
for switch_id, switch in stage.switches.items():
mux_sel[stage_id][switch_id] = {}
for mux_id, mux in switch.muxes.items():
mux_sel[stage_id][switch_id][mux_id] = None
for feature in features:
assert mux_sel[feature.stage_id][feature.switch_id][feature.mux_id] is None, feature # noqa: E501
mux_sel[feature.stage_id][feature.switch_id][feature.mux_id] = feature.sel_id # noqa: E501
def expand_mux(out_loc):
"""
Expands a multiplexer output until a switchbox input is reached.
Returns name of the input or None if not found.
Parameters
----------
out_loc: the last output location
Returns
-------
str: None if input name not found, else string
"""
# Get mux selection, If it is set to None then the mux is
# not active
sel = mux_sel[out_loc.stage_id][out_loc.switch_id][out_loc.mux_id]
if sel is None:
return None # TODO can we return None?
stage = switchbox.stages[out_loc.stage_id]
switch = stage.switches[out_loc.switch_id]
mux = switch.muxes[out_loc.mux_id]
pin = mux.inputs[sel]
if pin.name is not None:
return pin.name
inp_loc = SwitchboxPinLoc(
stage_id=out_loc.stage_id,
switch_id=out_loc.switch_id,
mux_id=out_loc.mux_id,
pin_id=sel,
pin_direction=PinDirection.INPUT,
)
# Expand all "upstream" muxes that connect to the selected
# input pin
assert inp_loc in conn_by_dst, inp_loc
for c in conn_by_dst[inp_loc]:
inp = expand_mux(c.src)
if inp is not None:
return inp
# Nothing found
return None # TODO can we return None?
# For each output pin of a switchbox determine to which input is it
# connected to.
routes = {}
for out_pin in switchbox.outputs.values():
out_loc = out_pin.locs[0]
routes[out_pin.name] = expand_mux(out_loc)
return routes
def process_switchbox(self, loc, switchbox, features):
"""Processes all switchboxes and extract hops from connections.
The function extracts final connections from inputs to outputs, and
hops into separate structures for further processing.
Parameters
----------
loc: Loc
location of the current switchbox
switchbox: Switchbox
a switchbox
features: list
list of features regarding given switchbox
"""
routes = self.decode_switchbox(switchbox, features)
for k, v in routes.items():
if v is not None:
if re.match("[VH][0-9][LRBT][0-9]", k):
self.designhops[Loc(loc.x, loc.y, 0)][k] = v
else:
self.designconnections[loc][k] = v
def resolve_hops(self):
"""Resolves remaining hop wires.
It determines the absolute input for the given pin by resolving hop
wires and adds those final connections to the design connections.
"""
for loc, conns in self.designconnections.items():
for pin, source in conns.items():
hop = get_name_and_hop(source)
tloc = loc
while hop[1] is not None:
tloc = Loc(tloc[0] + hop[1][0], tloc[1] + hop[1][1], 0)
# in some cases BEL is distanced from a switchbox, in those
# cases the hop will not point to another hop. We should
# simply return the pin here in the correct location
if hop[0] in self.designhops[tloc]:
hop = get_name_and_hop(self.designhops[tloc][hop[0]])
else:
hop = (hop[0], None)
self.designconnections[loc][pin] = (tloc, hop[0])
def resolve_connections(self):
"""Resolves connections between BELs and IOs."""
keys = sorted(self.routingdata.keys(), key=lambda loc: (loc.x, loc.y))
for loc in keys:
routingfeatures = self.routingdata[loc]
if loc in self.vpr_switchbox_grid:
typ = self.vpr_switchbox_grid[loc]
switchbox = self.vpr_switchbox_types[typ]
self.process_switchbox(loc, switchbox, routingfeatures)
self.resolve_hops()
def remap_multiloc_loc(self, loc, pinname=None, celltype=None):
"""Unifies coordinates of cells occupying multiple locations.
Some cells, like ASSP, RAM or multipliers occupy multiple locations.
This method groups bits and connections for those cells into a single
artificial location.
Parameters
----------
loc: Loc
The current location
pinname: str
The optional name of the pin (used to determine to which cell
pin refers to)
celltype: str
The optional name of the cell type
Returns
-------
Loc: the new location of the cell
"""
finloc = loc
for multiloc in self.multiloccells.values():
if pinname is None or pinname in multiloc.pinnames or celltype == multiloc.typ:
if loc in multiloc.fromlocset:
finloc = multiloc.toloc
break
return finloc
def resolve_multiloc_cells(self):
"""Groups cells that are scattered around multiple locations."""
newbelinversions = defaultdict(lambda: defaultdict(list))
newdesignconnections = defaultdict(dict)
for bellockey, bellocpair in self.belinversions.items():
for belloctype, belloc in bellocpair.items():
if belloctype in self.multiloccells:
newbelinversions[self.remap_multiloc_loc(bellockey, celltype=belloctype)][belloctype].extend(belloc)
self.belinversion = newbelinversions
for loc, conns in self.designconnections.items():
for pin, src in conns.items():
dstloc = self.remap_multiloc_loc(loc, pinname=pin)
srcloc = self.remap_multiloc_loc(src[0], pinname=src[1])
if srcloc != src[0]:
k, v = ((srcloc, src[1]), src)
if k in self.org_loc_map:
assert v == self.org_loc_map[k], (k, self.org_loc_map[k], v)
self.org_loc_map[k] = v
if dstloc != loc:
k, v = ((dstloc, pin), (loc, pin))
if k in self.org_loc_map:
assert v == self.org_loc_map[k], (k, self.org_loc_map[k], v)
self.org_loc_map[k] = v
newdesignconnections[dstloc][pin] = (srcloc, src[1])
self.designconnections = newdesignconnections
def get_clock_for_gmux(self, gmux, loc):
"""Returns location of a CLOCK cell associated with the given GMUX
cell. Returns None if not found
Parameters
----------
gmux: str
The GMUX cell name
loc: Loc
The GMUX location
Returns
-------
Loc: the new location of the cell or None
"""
connections = [
c for c in self.connections if c.src.type == ConnectionType.TILE and c.dst.type == ConnectionType.TILE
]
for connection in connections:
# Only to a GMUX at the given location
dst = connection.dst
if dst.loc != loc or "GMUX" not in dst.pin:
continue
# GMUX cells are named "GMUX<index>".
cell, pin = dst.pin.split("_", maxsplit=1)
match = re.match(r"GMUX(?P<idx>[0-9]+)", cell)
if match is None:
continue
# Not the cell that we are looking for
if cell != gmux:
continue
# We are only interested in the IP connection
if pin != "IP":
continue
# Must go from CLOCK<n>.IC pin
cell, pin = connection.src.pin.split("_", maxsplit=1)
if not cell.startswith("CLOCK") or pin != "IC":
continue
# Return the source location
return connection.src.loc
# Not found
return None
def get_gmux_for_qmux(self, qmux, loc):
"""Returns a map of the given QMUX selection to driving GMUX cells.
Parameters
----------
qmux: str
The QMUX cell name
loc: Loc
The QMUX location
Returns
-------
Dict: A dict indexed by the selection index holding tuples with format:
(loc, cell, pin)
"""
sel_map = {}
connections = [c for c in self.connections if c.dst.type == ConnectionType.CLOCK]
for connection in connections:
# Only to a QMUX at the given location
dst = connection.dst
if dst.loc != loc or "QMUX" not in dst.pin:
continue
# QMUX cells are named "QMUX_<quad><index>".
cell, pin = dst.pin.split(".", maxsplit=1)
match = re.match(r"QMUX_(?P<quad>[A-Z]+)(?P<idx>[0-9]+)", cell)
if match is None:
continue
# This is not for the given QMUX
qmux_idx = int(match.group("idx"))
if qmux != "QMUX{}".format(qmux_idx):
continue
# Get the QCLKIN pin index. These are named "QCLKIN<index>"
match = re.match(r"QCLKIN(?P<idx>[0-9]+)", pin)
if match is None:
continue
qclkin_idx = int(match.group("idx"))
# Get the source endpoint of the connection
cell, pin = connection.src.pin.split("_", maxsplit=1)
match = re.match(r"GMUX(?P<idx>[0-9]+)", cell)
if match is None:
continue
gmux_idx = int(match.group("idx"))
# Since fasm2bels uses physical database, it is not aware of
# other QMUX inputs that QCLKIN0. Assume that the connection
# found is to the QCLKIN0 pin and add QCLKIN1..2 to the map
# as well.
assert qclkin_idx == 0, connection
# Make map entries
for i in range(3):
# Calculate GMUX index for QCLKIN<i> input of the QMUX
idx = (gmux_idx + i) % 5
# Add to the map
sel_map[i] = (
connection.src.loc,
"GMUX{}".format(idx),
pin,
)
return sel_map
def get_qmux_for_cand(self, cand, loc):
"""Returns a QMUX cell and its location that drives the given CAND
cell.
Parameters
----------
cand: str
The CAND cell name
loc: Loc
The CAND location
Returns
-------
Tuple: A tuple holding (loc, cell)
"""
connections = [c for c in self.connections if c.dst.type == ConnectionType.CLOCK]
for connection in connections:
# Only to a CAND at the given location
# Note: Check also the row above. CAND cells are located in two
# rows but with fasm features everything gets aligned to even rows
dst = connection.dst
if (dst.loc != loc and dst.loc != Loc(loc.x, loc.y - 1, loc.z)) or "CAND" not in dst.pin:
continue
# CAND cells are named "CAND<index>_<quad>_<column>".
cell, pin = dst.pin.split(".", maxsplit=1)
match = re.match(r"CAND(?P<idx>[0-9]+)_(?P<quad>[A-Z]+)_(?P<col>[0-9]+)", cell)
if match is None:
continue
# This is not for the given CAND
if cand != "CAND{}".format(match.group("idx")):
continue
# QMUX cells are named "QMUX_<quad><index>".
cell, pin = connection.src.pin.split(".", maxsplit=1)
match = re.match(r"QMUX_(?P<quad>[A-Z]+)(?P<idx>[0-9]+)", cell)
if match is None:
continue
# Return the QMUX and its location
return "QMUX{}".format(match.group("idx")), connection.src.loc
# None found
return None, None
def resolve_gmux(self):
"""Resolves GMUX cells, updates the designconnections map. Also creates
connections to CLOCK cells whenever necessary.
Returns
-------
Dict: A map of GMUX names to their output wires
"""
# Process GMUX
gmux_map = dict()
gmux_locs = [loc for loc, tile in self.vpr_tile_grid.items() if "GMUX" in tile.type]
for loc in gmux_locs:
# Group GMUX input pin connections by GMUX cell names
gmux_connections = defaultdict(lambda: dict())
for cell_pin, conn in self.designconnections[loc].items():
if cell_pin.startswith("GMUX"):
cell, pin = cell_pin.split("_", maxsplit=1)
gmux_connections[cell][pin] = conn
# Examine each GMUX config
for gmux, connections in gmux_connections.items():
# FIXME: Handle IS0 inversion (if any)
# The IS0 pin has to be routed
if "IS0" not in connections:
print("WARNING: Pin '{}.IS0' at '{}' is unrouted!".format(gmux, loc))
continue
# TODO: For now support only static GMUX settings
if connections["IS0"][1] not in ["GND", "VCC"]:
print("WARNING: Non-static GMUX selection (at '{}') not supported yet!".format(loc))
continue
# Static selection
sel = int(connections["IS0"][1] == "VCC")
# IP selected
if sel == 0:
# Create a global clock wire for the CLOCK pad
match = re.match(r"GMUX(?P<idx>[0-9]+)", gmux)
assert match is not None, gmux
idx = int(match.group("idx"))
wire = "CLK{}".format(idx)
# Get the clock pad location
clock_loc = self.get_clock_for_gmux(gmux, loc)
assert clock_loc is not None, gmux
# Check if the clock pad is enabled. If not then discard
# the GMUX
bel_features = []
for bel, features in self.interfaces.get(clock_loc, {}).items():
for feature in features:
bel_features.append("{}.{}".format(bel, feature))
if "ASSP.ASSPInvPortAlias" not in bel_features:
continue
# Connect it to the output wire of the GMUX
self.designconnections[clock_loc]["CLOCK0_IC"] = (None, wire)
# The GMUX is implicit. Remove all connections to it
self.designconnections[loc] = {
k: v for k, v in self.designconnections[loc].items() if not k.startswith(gmux)
}
# IC selected
else:
# Check if the IC pin has an active driver. If not then
# discard the mux.
if connections.get("IC", (None, None))[1] in [None, "GND", "VCC"]:
continue
# Create a wire for the GMUX output
wire = "{}_X{}Y{}".format(gmux, loc.x, loc.y)
# Remove the IS0 connection
del self.designconnections[loc]["{}_IS0".format(gmux)]
# Connect the output
self.designconnections[loc]["{}_IZ".format(gmux)] = (None, wire)
# Store the wire
gmux_map[gmux] = wire
return gmux_map
def resolve_qmux(self, gmux_map):
"""Resolves QMUX cells, updates the designconnections map.
Parameters
----------
gmux_map: Dict
A map of QMUX cells to their GMUX driving wires.
Returns
-------
Dict: A map of locations and QMUX names to their driving wires
"""
# Process QMUX
qmux_map = defaultdict(lambda: dict())
qmux_locs = [loc for loc, tile in self.vpr_tile_grid.items() if "QMUX" in tile.type]
for loc in qmux_locs:
# Group QMUX input pin connections by QMUX cell names
qmux_connections = defaultdict(lambda: dict())
for cell_pin, conn in self.designconnections[loc].items():
if cell_pin.startswith("QMUX"):
cell, pin = cell_pin.split("_", maxsplit=1)
qmux_connections[cell][pin] = conn
# Examine each QMUX config
for qmux, connections in qmux_connections.items():
# FIXME: Handle IS0 and IS1 inversion (if any)
# Both IS0 and IS1 must be routed to something
if "IS0" not in connections:
print("WARNING: Pin '{}.IS0' at '{}' is unrouted!".format(qmux, loc))
if "IS1" not in connections:
print("WARNING: Pin '{}.IS1' at '{}' is unrouted!".format(qmux, loc))
if "IS0" not in connections or "IS1" not in connections:
continue
# TODO: For now support only static QMUX settings
if connections["IS0"][1] not in ["GND", "VCC"]:
print("WARNING: Non-static QMUX selection (at '{}') not supported yet!".format(loc))
continue
if connections["IS1"][1] not in ["GND", "VCC"]:
print("WARNING: Non-static QMUX selection (at '{}') not supported yet!".format(loc))
continue
# Get associated GMUXes
sel_map = self.get_gmux_for_qmux(qmux, loc)
# Static selection
sel = int(connections["IS0"][1] == "VCC") * 2 + int(connections["IS1"][1] == "VCC")
# Input from the routing network selected, create a new wire
if sel == 3:
# Check if the HSCKIN input is connected to an active
# driver. If not then discard the QMUX
if connections.get("HSCKIN", (None, None))[1] in [None, "GND", "VCC"]:
continue
# Create a wire for the QMUX output
wire = "{}_X{}Y{}".format(qmux, loc.x, loc.y)
# Remove IS0 and IS1 from the connection map.
del self.designconnections[loc]["{}_IS0".format(qmux)]
del self.designconnections[loc]["{}_IS1".format(qmux)]
# Connect the output
self.designconnections[loc]["{}_IZ".format(qmux)] = (None, wire)
# Input from a GMUX is selected, assign its wire here
else:
# The GMUX is not active. Discard the QMUX
gmux_loc, gmux_cell, gmux_pin = sel_map[sel]
if gmux_cell not in gmux_map:
continue
# Use the wire of that GMUX
wire = gmux_map[gmux_cell]
# The QMUX is implicit. Remove all connections to it
self.designconnections[loc] = {
k: v for k, v in self.designconnections[loc].items() if not k.startswith(qmux)
}
# Store the wire
qmux_map[loc][qmux] = wire
return dict(qmux_map)
def resolve_cand(self, qmux_map):
"""Resolves CAND cells, creates the cand_map map.
Parameters
----------
qmux_map: Dict
A map of locations and CAND names to their driving QMUXes.
Returns
-------
None
"""
# Process CAND
for loc, all_features in self.colclk_data.items():
for cand, features in all_features.items():
hilojoint = False
enjoint = False
for feature in features:
if feature.signature == "I_hilojoint":
hilojoint = bool(feature.value)
if feature.signature == "I_enjoint":
enjoint = bool(feature.value)
# TODO: Do not support dynamically enabled CANDs for now.
assert enjoint is False, "Dynamically enabled CANDs are not supported yet"
# Statically disabled, skip this one
if hilojoint is False:
continue
# Find a QMUX driving this CAND cell
qmux_cell, qmux_loc = self.get_qmux_for_cand(cand, loc)
assert qmux_cell is not None, (cand, loc)
# The QMUX is not active, skip this one
if qmux_loc not in qmux_map:
continue
# Get the wire
wire = qmux_map[qmux_loc][qmux_cell]
# Populate the column clock to switchbox connection map
quadrant = get_quadrant_for_loc(loc, self.quadrants)
for y in range(quadrant.y0, quadrant.y1 + 1):
sb_loc = Loc(loc.x, y, 0)
self.cand_map[sb_loc][cand] = wire
def resolve_global_clock_network(self):
"""Resolves the global clock network. Creates the cand_map, updates
the designconnections.
Returns
-------
None
"""
# Resolve GMUXes
gmux_map = self.resolve_gmux()
# Resolve QMUXes
qmux_map = self.resolve_qmux(gmux_map)
# Resolve CANDs
self.resolve_cand(qmux_map)
def produce_verilog(self, pcf_data):
"""Produces string containing Verilog module representing FASM.
Returns
-------
str, str: a Verilog module and PCF
"""
module = VModule(
self.vpr_tile_grid,
self.vpr_tile_types,
self.cells_library,
pcf_data,
self.belinversions,
self.interfaces,
self.designconnections,
self.org_loc_map,
self.cand_map,
self.inversionpins,
self.io_to_fbio,
)
module.parse_bels()
verilog = module.generate_verilog()
pcf = module.generate_pcf()
qcf = module.generate_qcf()
return verilog, pcf, qcf
def convert_to_verilog(self, fasmlines):
"""Runs all methods required to convert FASM lines to Verilog module.
Parameters
----------
fasmlines: list
FASM lines to process
Returns
-------
str: a Verilog module
"""
self.parse_fasm_lines(fasmlines)
self.resolve_connections()
self.resolve_multiloc_cells()
self.resolve_global_clock_network()
verilog, pcf, qcf = self.produce_verilog(pcf_data)
return verilog, pcf, qcf
def parse_pcf(pcf):
pcf_data = {}
with open(pcf, "r") as fp:
for line in fp:
line = line.strip().split()
if len(line) < 3:
continue
if len(line) > 3 and not line[3].startswith("#"):
continue
if line[0] != "set_io":
continue
pcf_data[line[2]] = line[1]
return pcf_data
if __name__ == "__main__":
# Parse arguments
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("input_file", type=Path, help="Input fasm file")
parser.add_argument("--phy-db", type=str, required=True, help="Physical device database file")
parser.add_argument("--device-name", type=str, required=True, choices=["eos-s3", "pp3e"], help="Device name")
parser.add_argument("--package-name", type=str, required=True, help="Device package name")
parser.add_argument(
"--input-type",
type=str,
choices=["bitstream", "fasm"],
default="fasm",
help="Determines whether the input is a FASM file or bitstream",
)
parser.add_argument("--output-verilog", type=Path, required=True, help="Output Verilog file")
parser.add_argument(
"--input-pcf", type=Path, required=False, help="Pins constraint file to maintain original pin names"
)
parser.add_argument("--output-pcf", type=Path, help="Output PCF file")
parser.add_argument("--output-qcf", type=Path, help="Output QCF file")
args = parser.parse_args()
pcf_data = {}
if args.input_pcf is not None:
pcf_data = parse_pcf(args.input_pcf)
# Load data from the database
with open(args.phy_db, "rb") as fp:
db = pickle.load(fp)
# Initialize fasm2bels
f2b = Fasm2Bels(db, args.device_name, args.package_name)
# Disassemble bitstream / load FASM
if args.input_type == "bitstream":
qlfasmdb = load_quicklogic_database(get_db_dir("ql-" + args.device_name))
if args.device_name == "eos-s3":
assembler = QL732BAssembler(qlfasmdb)
elif args.device_name == "pp3e":
assembler = QL732BAssembler(qlfasmdb) # Workaround: use EOS-S3 assembler for PP3E
else:
assert False, args.device_name
assembler.read_bitstream(args.input_file)
fasmlines = assembler.disassemble()
fasmlines = [line for line in fasm.parse_fasm_string("\n".join(fasmlines))]
else:
fasmlines = [line for line in fasm.parse_fasm_filename(args.input_file)]
# Run fasm2bels
verilog, pcf, qcf = f2b.convert_to_verilog(fasmlines)
# Write output files
with open(args.output_verilog, "w") as outv:
outv.write(verilog)
if args.output_pcf:
with open(args.output_pcf, "w") as outpcf:
outpcf.write(pcf)
if args.output_qcf:
with open(args.output_qcf, "w") as outqcf:
outqcf.write(qcf)