blob: 580e1b7700ed597303d17478638b8e844b89d4d2 [file] [log] [blame]
#!/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 re
from collections import namedtuple, defaultdict
from f4pga.utils.quicklogic.pp3.data_structs import PinDirection
Element = namedtuple("Element", "loc type name ios")
Wire = namedtuple("Wire", "srcloc name inverted")
VerilogIO = namedtuple("VerilogIO", "name direction ioloc")
def loc2str(loc):
return "X" + str(loc.x) + "Y" + str(loc.y)
class VModule(object):
"""Represents a Verilog module for QLAL4S3B FASM"""
def __init__(
self,
vpr_tile_grid,
vpr_tile_types,
cells_library,
pcf_data,
belinversions,
interfaces,
designconnections,
org_loc_map,
cand_map,
inversionpins,
io_to_fbio,
useinversionpins=True,
):
"""Prepares initial structures.
Refer to fasm2bels.py for input description.
"""
self.vpr_tile_grid = vpr_tile_grid
self.vpr_tile_types = vpr_tile_types
self.cells_library = cells_library
self.pcf_data = pcf_data
self.belinversions = belinversions
self.interfaces = interfaces
self.designconnections = designconnections
self.org_loc_map = org_loc_map
self.cand_map = cand_map
self.inversionpins = inversionpins
self.useinversionpins = useinversionpins
self.io_to_fbio = io_to_fbio
# dictionary holding inputs, outputs
self.ios = {}
# dictionary holding declared wires (wire value;)
self.wires = {}
# dictionary holding Verilog elements
self.elements = defaultdict(dict)
# dictionary holding assigns (assign key = value;)
self.assigns = {}
# helper representing last input id
self.last_input_id = 0
# helper representing last output id
self.last_output_id = 0
self.qlal4s3bmapping = {
"LOGIC": "logic_cell_macro",
"ASSP": "qlal4s3b_cell_macro",
"BIDIR": "gpio_cell_macro",
"RAM": "ram8k_2x1_cell_macro",
"MULT": "qlal4s3_mult_cell_macro",
"GMUX": "gclkbuff",
"QMUX": "qhsckbuff",
"CLOCK": "ckpad",
"inv": "inv",
}
self.qlal4s3_pinmap = {
"ckpad": {
"IP": "P",
"IC": "Q",
},
"gclkbuff": {
"IC": "A",
"IZ": "Z",
},
"qhsckbuff": {"HSCKIN": "A", "IZ": "Z"},
}
def group_vector_signals(self, signals, io=False):
# IOs beside name, have also direction, convert them to format
# we can process
if io:
orig_ios = signals
ios = dict()
for s in signals:
id = Wire(s.name, "io", False)
ios[id] = s.name
signals = ios
vectors = dict()
new_signals = dict()
array = re.compile(r"(?P<varname>[a-zA-Z_][a-zA-Z_0-9$]+)\[(?P<arrindex>[0-9]+)\]")
# first find the vectors
for signalid in signals:
match = array.match(signals[signalid])
if match:
varname = match.group("varname")
arrayindex = int(match.group("arrindex"))
if varname not in vectors:
vectors[varname] = dict()
vectors[varname]["max"] = 0
vectors[varname]["min"] = 0
if arrayindex > vectors[varname]["max"]:
vectors[varname]["max"] = arrayindex
if arrayindex < vectors[varname]["min"]:
vectors[varname]["min"] = arrayindex
# if signal is not a part of a vector leave it
else:
new_signals[signalid] = signals[signalid]
# add vectors to signals dict
for vec in vectors:
name = "[{max}:{min}] {name}".format(max=vectors[vec]["max"], min=vectors[vec]["min"], name=vec)
id = Wire(name, "vector", False)
new_signals[id] = name
if io:
# we need to restore the direction info
new_ios = list()
for s in new_signals:
signalname = new_signals[s].split()
signalname = signalname[-1]
io = [x.direction for x in orig_ios if x.name.startswith(signalname)]
direction = io[0]
new_ios.append((direction, new_signals[s]))
return new_ios
else:
return new_signals
def group_array_values(self, parameters: dict):
"""Groups pin names that represent array indices.
Parameters
----------
parameters: dict
A dictionary holding original parameters
Returns
-------
dict: parameters with grouped array indices
"""
newparameters = dict()
arraydst = re.compile(r"(?P<varname>[a-zA-Z_][a-zA-Z_0-9$]+)\[(?P<arrindex>[0-9]+)\]")
for dst, src in parameters.items():
match = arraydst.match(dst)
if match:
varname = match.group("varname")
arrindex = int(match.group("arrindex"))
if varname not in newparameters:
newparameters[varname] = {arrindex: src}
else:
newparameters[varname][arrindex] = src
else:
newparameters[dst] = src
return newparameters
def form_simple_assign(self, loc, parameters):
ioname = self.get_io_name(loc)
assign = ""
direction = self.get_io_config(parameters)
if direction == "input":
assign = " assign {} = {};".format(parameters["IZ"], ioname)
elif direction == "output":
assign = " assign {} = {};".format(ioname, parameters["OQI"])
elif direction is None:
pass
else:
assert False, "Unknown IO configuration"
return assign
def form_verilog_element(self, loc, typ: str, name: str, parameters: dict):
"""Creates an entry representing single Verilog submodule.
Parameters
----------
loc: Loc
Cell coordinates
typ: str
Cell type
name: str
Name of the submodule
parameters: dict
Map from input pin to source wire
Returns
-------
str: Verilog entry
"""
if typ == "BIDIR":
# We do not emit the BIDIR cell for non inout IOs
direction = self.get_io_config(parameters)
if direction is None:
return ""
elif direction != "inout":
return self.form_simple_assign(loc, parameters)
params = []
moduletype = self.qlal4s3bmapping[typ]
pin_map = self.qlal4s3_pinmap.get(moduletype, dict())
result = f" {moduletype} {name} ("
fixedparameters = self.group_array_values(parameters)
# get inputs, strip vector's pin indexes
input_pins = [
pin.name.split("[")[0] for pin in self.cells_library[typ].pins if pin.direction == PinDirection.INPUT
]
dummy_wires = []
for inpname, inp in fixedparameters.items():
mapped_inpname = pin_map.get(inpname, inpname)
if isinstance(inp, dict):
arr = []
dummy_wire = f"{moduletype}_{name}_{inpname}"
max_dummy_index = 0
need_dummy = False
maxindex = max([val for val in inp.keys()])
for i in reversed(range(maxindex + 1)):
if i not in inp:
# do not assign constants to outputs
if inpname in input_pins:
arr.append("1'b0")
else:
if i > max_dummy_index:
max_dummy_index = i
arr.append("{}[{}]".format(dummy_wire, i))
need_dummy = True
else:
arr.append(inp[i])
arrlist = ", ".join(arr)
params.append(f".{mapped_inpname}({{{arrlist}}})")
if need_dummy:
dummy_wires.append(f" wire [{max_dummy_index}:0] {dummy_wire};")
else:
params.append(f".{mapped_inpname}({inp})")
if self.useinversionpins:
if typ in self.inversionpins:
for toinvert, inversionpin in self.inversionpins[typ].items():
if toinvert in self.belinversions[loc][typ]:
params.append(f".{inversionpin}(1'b1)")
else:
params.append(f".{inversionpin}(1'b0)")
# handle BIDIRs and CLOCKs
if typ in ["CLOCK", "BIDIR"]:
ioname = self.get_io_name(loc)
moduletype = self.qlal4s3bmapping[typ]
pin_map = self.qlal4s3_pinmap.get(moduletype, dict())
params.append(".{}({})".format(pin_map.get("IP", "IP"), ioname))
result += f',\n{" " * len(result)}'.join(sorted(params)) + ");\n"
wires = ""
for wire in dummy_wires:
wires += f"\n{wire}"
result = wires + "\n\n" + result
return result
@staticmethod
def get_element_name(type, loc):
"""Forms element name from its type and FASM feature name."""
return f"{type}_X{loc.x}_Y{loc.y}"
@staticmethod
def get_element_type(type):
match = re.match(r"(?P<type>[A-Za-z]+)(?P<index>[0-9]+)?", type)
assert match is not None, type
return match.group("type")
def get_bel_type_and_connections(self, loc, connections, direction):
"""For a given connection list returns a dictionary
with bel types and connections to them
"""
cells = self.vpr_tile_grid[loc].cells
if type(connections) == str:
inputnames = [connections]
# convert connections to dict
connections = dict()
connections[inputnames[0]] = inputnames[0]
else:
inputnames = [name for name in connections.keys()]
# Some switchbox inputs are named like "<cell><cell_index>_<pin>"
# Make a list of them for compatison in the following step.
cell_input_names = defaultdict(lambda: [])
for name in inputnames:
fields = name.split("_", maxsplit=1)
if len(fields) == 2:
cell, pin = fields
cell_input_names[cell].append(pin)
used_cells = []
for cell in cells:
cell_name = "{}{}".format(cell.type, cell.index)
cellpins = [pin.name for pin in self.cells_library[cell.type].pins if pin.direction == direction]
# check every connection pin if it has
for pin in cellpins:
# Cell name and pin name match
if cell_name in cell_input_names:
if pin in cell_input_names[cell_name]:
used_cells.append(cell)
break
# The pin name matches exactly the specified input name
if pin in inputnames:
used_cells.append(cell)
break
# assing connection to a cell
cell_connections = {}
for cell in used_cells:
cell_name = "{}{}".format(cell.type, cell.index)
cell_connections[cell_name] = dict()
cellpins = [pin.name for pin in self.cells_library[cell.type].pins if pin.direction == direction]
for key in connections.keys():
if key in cellpins:
cell_connections[cell_name][key] = connections[key]
# Some switchbox inputs are named like
# "<cell><cell_index>_<pin>". Break down the name and check.
fields = key.split("_", maxsplit=1)
if len(fields) == 2:
key_cell, key_pin = fields
if key_cell == cell_name and key_pin in cellpins:
cell_connections[cell_name][key_pin] = connections[key]
return cell_connections
def new_io_name(self, direction):
"""Creates a new IO name for a given direction.
Parameters
----------
direction: str
Direction of the IO, can be 'input' or 'output'
"""
# TODO add support for inout
assert direction in ["input", "output", "inout"]
if direction == "output":
name = f"out_{self.last_output_id}"
self.last_output_id += 1
elif direction == "input":
name = f"in_{self.last_input_id}"
self.last_input_id += 1
else:
pass
return name
def get_wire(self, loc, wire, inputname):
"""Creates or gets an existing wire for a given source.
Parameters
----------
loc: Loc
Location of the destination cell
wire: tuple
A tuple of location of the source cell and source pin name
inputname: str
A name of the destination pin
Returns
-------
str: wire name
"""
isoutput = self.vpr_tile_grid[loc].type == "SYN_IO"
if isoutput:
# outputs are never inverted
inverted = False
else:
# determine if inverted
inverted = inputname in self.belinversions[loc][self.vpr_tile_grid[loc].type]
wireid = Wire(wire[0], wire[1], inverted)
if wireid in self.wires:
# if wire already exists, use it
return self.wires[wireid]
# first create uninverted wire
uninvertedwireid = Wire(wire[0], wire[1], False)
if uninvertedwireid in self.wires:
# if wire already exists, use it
wirename = self.wires[uninvertedwireid]
else:
srcname = self.vpr_tile_grid[wire[0]].name
type_connections = self.get_bel_type_and_connections(wire[0], wire[1], PinDirection.OUTPUT)
# there should be only one type here
srctype = [type for type in type_connections.keys()][0]
srconame = wire[1]
if srctype == "SYN_IO":
# if source is input, use its name
if wire[0] not in self.ios:
self.ios[wire[0]] = VerilogIO(name=self.new_io_name("input"), direction="input", ioloc=wire[0])
assert self.ios[wire[0]].direction == "input"
wirename = self.ios[wire[0]].name
else:
# form a new wire name
wirename = f"{srcname}_{srconame}"
if srctype not in self.elements[wire[0]]:
# if the source element does not exist, create it
self.elements[wire[0]][srctype] = Element(
wire[0],
self.get_element_type(srctype),
self.get_element_name(srctype, wire[0]),
{srconame: wirename},
)
else:
# add wirename to the existing element
self.elements[wire[0]][srctype].ios[srconame] = wirename
if not isoutput and srctype != "SYN_IO":
# add wire
self.wires[uninvertedwireid] = wirename
elif isoutput:
# add assign to output
self.assigns[self.ios[loc].name] = wirename
if not inverted or (self.useinversionpins and inputname in self.inversionpins[self.vpr_tile_grid[loc].type]):
# if not inverted or we're not inverting, just finish
return wirename
# else create an inverted and wire for it
invertername = f"{wirename}_inverter"
invwirename = f"{wirename}_inv"
inverterios = {"Q": invwirename, "A": wirename}
inverterelement = Element(wire[0], "inv", invertername, inverterios)
self.elements[wire[0]]["inv"] = inverterelement
invertedwireid = Wire(wire[0], wire[1], True)
self.wires[invertedwireid] = invwirename
return invwirename
def parse_bels(self):
"""Converts BELs to Verilog-like structures."""
# TODO add support for direct input-to-output
# first parse outputs to create wires for them
# parse outputs first to properly handle namings
for currloc, connections in self.designconnections.items():
type_connections = self.get_bel_type_and_connections(currloc, connections, PinDirection.OUTPUT)
for currtype, connections in type_connections.items():
currname = self.get_element_name(currtype, currloc)
outputs = {}
# Check each output
for output_name, (
loc,
wire,
) in connections.items():
# That wire is connected to something. Skip processing
# of the cell here
if loc is not None:
continue
# Connect the global wire
outputs[output_name] = wire
# No outputs connected, don't add.
if not len(outputs):
continue
# If Element does not exist, create it
if currtype not in self.elements[currloc]:
self.elements[currloc][currtype] = Element(
currloc, self.get_element_type(currtype), currname, outputs
)
# Else update IOs
else:
self.elements[currloc][currtype].ios.update(outputs)
# process of BELs
for currloc, connections in self.designconnections.items():
# Extract type and form name for the BEL
# Current location may be a multi cell location.
# Split the connection list into a to a set of connections
# for each used cell type
type_connections = self.get_bel_type_and_connections(currloc, connections, PinDirection.INPUT)
for currtype in type_connections:
currname = self.get_element_name(currtype, currloc)
connections = type_connections[currtype]
inputs = {}
# form all inputs for the BEL
for inputname, wire in connections.items():
if wire[1] == "VCC":
inputs[inputname] = "1'b1"
continue
elif wire[1] == "GND":
inputs[inputname] = "1'b0"
continue
elif wire[1].startswith("CAND"):
dst = (currloc, inputname)
dst = self.org_loc_map.get(dst, dst)
if dst[0] in self.cand_map:
inputs[inputname] = self.cand_map[dst[0]][wire[1]]
continue
srctype = self.vpr_tile_grid[wire[0]].type
srctype_cells = self.vpr_tile_types[srctype].cells
if len(set(srctype_cells).intersection(set(["BIDIR", "LOGIC", "ASSP", "RAM", "MULT"]))) > 0:
# FIXME handle already inverted pins
# TODO handle inouts
wirename = self.get_wire(currloc, wire, inputname)
inputs[inputname] = wirename
else:
raise Exception("Not supported cell type {}".format(srctype))
if currtype not in self.elements[currloc]:
# If Element does not exist, create it
self.elements[currloc][currtype] = Element(
currloc, self.get_element_type(currtype), currname, inputs
)
else:
# else update IOs
self.elements[currloc][currtype].ios.update(inputs)
# Prune BELs that do not drive anythin (have all outputs disconnected)
for loc, elements in list(self.elements.items()):
for type, element in list(elements.items()):
# Handle IO cells
if element.type in ["CLOCK", "BIDIR", "SDIOMUX"]:
if element.type == "CLOCK":
direction = "input"
else:
direction = self.get_io_config(element.ios)
# Remove the cell if none is connected
if direction is None:
del elements[type]
# Handle non-io cells
else:
# Get connected pin names and output pin names
connected_pins = set(element.ios.keys())
output_pins = set(
[
pin.name.split("[")[0]
for pin in self.cells_library[element.type].pins
if pin.direction == PinDirection.OUTPUT
]
)
# Remove the cell if none is connected
if not len(connected_pins & output_pins):
del elements[type]
# Prune the whole location
if not len(elements):
del self.elements[loc]
def get_io_name(self, loc):
# default pin name
name = loc2str(loc) + "_inout"
# check if we have the original name for this io
if self.pcf_data is not None:
pin = self.io_to_fbio.get(loc, None)
if pin is not None and pin in self.pcf_data:
name = self.pcf_data[pin]
name = name.replace("(", "[")
name = name.replace(")", "]")
return name
def get_io_config(self, ios):
# decode direction
# direction is configured by routing 1 or 0 to certain inputs
if "IE" in ios:
output_en = ios["IE"] != "1'b0"
else:
# outputs is enabled by default
output_en = True
if "INEN" in ios:
input_en = ios["INEN"] != "1'b0"
else:
# inputs are disabled by default
input_en = False
if input_en and output_en:
direction = "inout"
elif input_en:
direction = "input"
elif output_en:
direction = "output"
else:
direction = None
# Output unrouted. Discard
if direction == "input":
if "IZ" not in ios:
return None
return direction
def generate_ios(self):
"""Generates IOs and their wires
Returns
-------
None
"""
for eloc, locelements in self.elements.items():
for element in locelements.values():
if element.type in ["CLOCK", "BIDIR", "SDIOMUX"]:
if element.type == "CLOCK":
direction = "input"
else:
direction = self.get_io_config(element.ios)
# Add the input if used
if direction is not None:
name = self.get_io_name(eloc)
self.ios[eloc] = VerilogIO(name=name, direction=direction, ioloc=eloc)
def generate_verilog(self):
"""Creates Verilog module
Returns
-------
str: A Verilog module for given BELs
"""
ios = ""
wires = ""
assigns = ""
elements = ""
self.generate_ios()
if len(self.ios) > 0:
sortedios = sorted(self.ios.values(), key=lambda x: (x.direction, x.name))
grouped_ios = self.group_vector_signals(sortedios, True)
ios = "\n "
ios += ",\n ".join([f"{x[0]} {x[1]}" for x in grouped_ios])
grouped_wires = self.group_vector_signals(self.wires)
if len(grouped_wires) > 0:
wires += "\n"
for wire in grouped_wires.values():
wires += f" wire {wire};\n"
if len(self.assigns) > 0:
assigns += "\n"
for dst, src in self.assigns.items():
assigns += f" assign {dst} = {src};\n"
if len(self.elements) > 0:
for eloc, locelements in self.elements.items():
for element in locelements.values():
if element.type != "SYN_IO":
elements += "\n"
elements += self.form_verilog_element(eloc, element.type, element.name, element.ios)
verilog = f"module top ({ios});\n" f"{wires}" f"{assigns}" f"{elements}" f"\n" f"endmodule"
return verilog
def generate_pcf(self):
pcf = ""
for io in self.ios.values():
if io.ioloc in self.io_to_fbio:
pcf += f"set_io {io.name} {self.io_to_fbio[io.ioloc]}\n"
return pcf
def generate_qcf(self):
qcf = "#[Fixed Pin Placement]\n"
for io in self.ios.values():
if io.ioloc in self.io_to_fbio:
qcf += f"place {io.name} {self.io_to_fbio[io.ioloc]}\n"
return qcf