diff --git a/f4pga/flows/common_modules/synth.py b/f4pga/flows/common_modules/synth.py index 46ff941..d1fd86b 100755 --- a/f4pga/flows/common_modules/synth.py +++ b/f4pga/flows/common_modules/synth.py @@ -23,6 +23,7 @@ from pathlib import Path from f4pga.flows.common import decompose_depname, get_verbosity_level, sub as common_sub from f4pga.flows.module import Module, ModuleContext from f4pga.wrappers.tcl import get_script_path as get_tcl_wrapper_path +from f4pga.utils import split_inouts class SynthModule(Module): @@ -83,14 +84,7 @@ class SynthModule(Module): ) yield f"Splitting in/outs..." - common_sub( - "python3", - str(Path(tcl_env["UTILS_PATH"]) / "split_inouts.py"), - "-i", - ctx.outputs.json, - "-o", - ctx.outputs.synth_json, - ) + split_inouts(ctx.outputs.json, ctx.outputs.synth_json) if not Path(ctx.produces.fasm_extra).is_file(): with Path(ctx.produces.fasm_extra).open("w") as wfptr: diff --git a/f4pga/setup.py b/f4pga/setup.py index f6e2c84..0a3ff03 100644 --- a/f4pga/setup.py +++ b/f4pga/setup.py @@ -78,7 +78,6 @@ setuptools_setup( author="F4PGA Authors", description="F4PGA.", url="https://github.com/chipsalliance/f4pga", - packages=["f4pga", "f4pga.flows", "f4pga.flows.common_modules", "f4pga.wrappers.sh", "f4pga.wrappers.tcl"], package_dir={"f4pga": "."}, package_data={ "f4pga.flows": [ diff --git a/f4pga/utils/__init__.py b/f4pga/utils/__init__.py new file mode 100644 index 0000000..079d74d --- /dev/null +++ b/f4pga/utils/__init__.py @@ -0,0 +1,20 @@ +#!/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 + +from f4pga.utils.split_inouts import main as split_inouts diff --git a/f4pga/utils/split_inouts.py b/f4pga/utils/split_inouts.py new file mode 100644 index 0000000..d4fd30f --- /dev/null +++ b/f4pga/utils/split_inouts.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (C) 2019-2022 F4PGA Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +This script provides a temporary solution for the problem of inout top level +port representation of the BLIF format. + +The script loads the design from a JSON file generated by Yosys. Then it splits +all inout ports along with their nets and connections to cell ports into two. +Suffixes are automatically added to distinguish between the input and the +output part. + +The JSON design format used by Yosys is documented there: +- http://www.clifford.at/yosys/cmd_write_json.html +- http://www.clifford.at/yosys/cmd_read_json.html + +For example in the given design (in verilog): + + module top( + input A, + output B, + inout C + ); + + IOBUF iobuf ( + .I(A), + .O(B), + .IO_$inp(C), + .IO_$out(C) + ); + + endmodule + +the resulting design would be: + + module top( + input A, + output B, + input C_$inp, + output C_$out + ); + + IOBUF iobuf ( + .I(A), + .O(B), + .IO_$inp(C_$inp), + .IO_$out(C_$out) + ); + + endmodule + +""" + + +from pathlib import Path +from os.path import splitext +import simplejson as json +from argparse import ArgumentParser, RawDescriptionHelpFormatter + + +def find_top_module(design): + """ + Looks for the top-level module in the design. Returns its name. Throws + an exception if none was found. + """ + for name, module in design["modules"].items(): + attrs = module["attributes"] + if "top" in attrs and int(attrs["top"]) == 1: + return name + raise RuntimeError("No top-level module found in the design!") + + +def get_nets(bits): + """ + Returns a set of numbers corresponding to net indices effectively skipping + connections to consts ("0", "1", "x"). + + >>> get_nets([0, 1, 2, "0", "1", "x", 3, 4, 5]) + {0, 1, 2, 3, 4, 5} + """ + return set([n for n in bits if isinstance(n, int)]) + + +def get_free_net(nets): + """ + Given a set of used net indices, returns a new, free index. + + >>> get_free_net({0, 1, 2, 4, 5, 6}) + 3 + >>> get_free_net({0, 1, 2, 3, 4, 5, 6}) + 7 + """ + sorted_nets = sorted(list(nets)) + + # Find a gap in the sequence + for i in range(len(nets) - 1): + n0 = sorted_nets[i] + n1 = sorted_nets[i + 1] + if n1 != (n0 + 1): + return n0 + 1 + + # No gap was found, return max + 1. + return sorted_nets[-1] + 1 + + +def main(input: str, output: str = None): + if output is None: + output = splitext(input)[0] + "_out.json" + + with Path(input).open("r") as fp: + design = json.load(fp) + + module_name = find_top_module(design) + + # Take a module from the design and split all of its inout ports into pairs of inputs and outputs. + # Newly created ports are given suffixed. + # For example an inout port named "A" is going to be replaced by a pair consisting of "A_$inp" and "A_$out" ports. + + # The function also looks for "netnames" that correspond to inout ports being split. + # These ones are removed and replaced with new ones related to newly added input and output ports. + + # If any other "netname" mentions a net index connected to a former inout port, then the index is removed from the + # "netname" (replaced by "x"). + # If there are only "x" left in the "netname", then it is removed. + + # The function returns port name map and net index map. + # The port map is a list of pairs (input name, input/output name). + # There are two entries per an inout. + # The net map is a dict indexed by indices of nets associated with inout ports. + # Each item contains a dict like {"i": int, "o": int} with indices of the inout net split products. + + # Get the module + module = design["modules"][module_name] + + # Find indices of all used nets + nets = set() + for port in module["ports"].values(): + nets |= get_nets(port["bits"]) + + for netname in module["netnames"].values(): + nets |= get_nets(netname["bits"]) + + for cell in module["cells"].values(): + for connection in cell["connections"].values(): + nets |= get_nets(connection) + + # Get all inout ports + inouts = {k: v for k, v in module["ports"].items() if v["direction"] == "inout"} + + # Split ports + new_ports = {} + net_map = {} + port_map = [] + for name, port in inouts.items(): + + # Remove the inout port from the module + del module["ports"][name] + nets -= get_nets(port["bits"]) + + # Make an input and output port + for dir in ["input", "output"]: + new_name = name + "_$" + dir[:3] + new_port = {"direction": dir, "bits": []} + + print("Mapping port '{}' to '{}'".format(name, new_name)) + + for n in port["bits"]: + if isinstance(n, int): + mapped_n = get_free_net(nets) + print("Mapping net {} to {} ({})".format(n, mapped_n, dir)) + + if n not in net_map: + net_map[n] = {} + net_map[n][dir[0]] = mapped_n + nets.add(mapped_n) + + new_port["bits"].append(mapped_n) + else: + new_port["bits"].append(n) + + port_map.append( + ( + name, + new_name, + ) + ) + new_ports[new_name] = new_port + + # Add inputs and outputs + module["ports"].update(new_ports) + + netnames = module["netnames"] + + # Remove netnames related to inout ports + for name, net in list(netnames.items()): + if name in inouts: + print(f"Removing netname '{name}'") + del netnames[name] + + # Remove remapped nets + for name, net in list(netnames.items()): + + # Remove "bits" used by the net that were re-mapped. + if len(set(net["bits"]) & set(net_map.keys())): + + # Remove + net["bits"] = ["x" if b in net_map else b for b in net["bits"]] + + # If there is nothing left, remove the whole net. + if all([b == "x" for b in net["bits"]]): + print(f"Removing netname '{name}'") + del netnames[name] + + # Add netnames related to new input and output ports + for name, port in new_ports.items(): + netnames[name] = {"hide_name": 0, "bits": port["bits"], "attributes": {}} + + # Remap cell connections that mention inout ports being split. + # Loop over all cells and their ports. + # If a port contains a connection to an inout net, then the connection is remapped according to the given + # net_map. + # Only ports which names ends on "_$inp" and "_$out" are affected. + + module = design["modules"][module_name] + cells = module["cells"] + + # Process cells + for name, cell in cells.items(): + if "port_directions" not in cell: + continue + + port_directions = cell["port_directions"] + connections = cell["connections"] + + # Process cell connections + for port_name, port_nets in list(connections.items()): + + # Skip if no net of this connection were remapped + if len(set(net_map.keys()) & set(port_nets)) == 0: + continue + + # Remove connections to the output net from input port and vice + # versa. + for dir in ["input", "output"]: + if port_directions[port_name] == dir and port_name.endswith("$" + dir[:3]): + + for i, n in enumerate(port_nets): + if n in net_map: + mapped_n = net_map[n][dir[0]] + port_nets[i] = mapped_n + print("Mapping connection {}.{}[{}] from {} to {}".format(name, port_name, i, n, mapped_n)) + + with Path(output).open("w") as fp: + json.dump(design, fp, sort_keys=True, indent=2) + + +if __name__ == "__main__": + parser = ArgumentParser(description=__doc__, formatter_class=RawDescriptionHelpFormatter) + parser.add_argument("-i", required=True, type=str, help="Input JSON") + parser.add_argument("-o", default=None, type=str, help="Output JSON") + args = parser.parse_args() + main(args.i, args.o) diff --git a/f4pga/wrappers/sh/quicklogic/synth.f4pga.sh b/f4pga/wrappers/sh/quicklogic/synth.f4pga.sh index a050c41..c758fd8 100755 --- a/f4pga/wrappers/sh/quicklogic/synth.f4pga.sh +++ b/f4pga/wrappers/sh/quicklogic/synth.f4pga.sh @@ -18,7 +18,6 @@ set -e -SPLIT_INOUTS="${F4PGA_SHARE_DIR}"/scripts/split_inouts.py CONVERT_OPTS="${F4PGA_SHARE_DIR}"/scripts/convert_compile_opts.py print_usage () { @@ -177,5 +176,5 @@ if [ ! -z "${YOSYS_COMMANDS}" ]; then fi `which yosys` -p "${YOSYS_SCRIPT}" -l $LOG -`which python3` ${SPLIT_INOUTS} -i ${OUT_JSON} -o ${SYNTH_JSON} +`which python3` -m f4pga.utils.split_inouts -i ${OUT_JSON} -o ${SYNTH_JSON} `which yosys` -p "read_json $SYNTH_JSON; tcl ${CONV_TCL_PATH}" diff --git a/f4pga/wrappers/sh/xc7/synth.f4pga.sh b/f4pga/wrappers/sh/xc7/synth.f4pga.sh index 0b818da..08830c5 100755 --- a/f4pga/wrappers/sh/xc7/synth.f4pga.sh +++ b/f4pga/wrappers/sh/xc7/synth.f4pga.sh @@ -139,5 +139,5 @@ else yosys -p "plugin -i uhdm" -p "read_verilog_with_uhdm ${SURELOG_CMD[*]} ${VERILOG_FILES[*]}" -p "tcl ${SYNTH_TCL_PATH}" -l $LOG fi -python3 ${UTILS_PATH}/split_inouts.py -i ${OUT_JSON} -o ${SYNTH_JSON} +python3 -m f4pga.utils.split_inouts -i ${OUT_JSON} -o ${SYNTH_JSON} yosys -p "read_json $SYNTH_JSON; tcl $(python3 -m f4pga.wrappers.tcl conv)"