| #!/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) |