| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # |
| # Copyright 2020-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 is intended to provide a range of helper functions around the output of |
| Yosys' `write_json`. Depending on the tasks, this may need to be flattened |
| and/or in AIG format. In any case, at minimum `proc` and usually `prep` should |
| be used before outputting the JSON. |
| """ |
| |
| import json |
| import pprint |
| import re |
| |
| |
| class YosysModule: |
| def __init__(self, name, module_data): |
| self.name = name |
| self.data = module_data |
| |
| def __str__(self): |
| return "YosysModule({},\n{})".format( |
| self.name, pprint.pformat(self.data)) |
| |
| @property |
| def ports(self): |
| """List of ports on a module. |
| |
| Returns a list of tuples: |
| ------- |
| name : str |
| width : int |
| The width in bits |
| dir : str |
| The direction, should be either `input` or `output` |
| """ |
| plist = [] |
| for port, pdata in sorted(self.data["ports"].items()): |
| plist.append( |
| (port, len(pdata["bits"]), pdata["bits"], pdata["direction"])) |
| return plist |
| |
| @property |
| def cells(self): |
| """List of cells of a module, excluding Yosys-internal cells |
| beginning with $. |
| |
| Returns a list of tuples: |
| ------- |
| name : str |
| type : str |
| """ |
| clist = [] |
| for cell, cdata in sorted(self.data["cells"].items()): |
| if cdata["type"].startswith('$'): |
| continue |
| clist.append((cell, cdata["type"])) |
| return clist |
| |
| @property |
| def all_cells(self): |
| """List of cells of a module, including Yosis-internal cells |
| |
| Returns a list of tuples: |
| ------- |
| name : str |
| type : str |
| """ |
| clist = [] |
| for cell, cdata in sorted(self.data["cells"].items()): |
| clist.append((cell, cdata["type"])) |
| return clist |
| |
| @property |
| def net_names(self): |
| """List the net names avilable in the design.""" |
| return self.data["netnames"].keys() |
| |
| @property |
| def nets(self): |
| """List the net ids available in the design.""" |
| return list( |
| sorted(set(n['bits'][0] for n in self.data["netnames"].values()))) |
| |
| def cell_type(self, cell): |
| """Return the type of a given cell""" |
| for cname, cdata in self.data["cells"].items(): |
| if cname == cell: |
| return cdata["type"] |
| return None |
| |
| @property |
| def module_attrs(self): |
| """All attributes of a module as a dictionary""" |
| return self.data["attributes"] |
| |
| def attr(self, attr, defval=None): |
| """Get an attribute, or defval is not set""" |
| if attr in self.module_attrs: |
| return self.module_attrs[attr] |
| else: |
| return defval |
| |
| def __getattr__(self, attr): |
| return self.attr(attr) |
| |
| def has_attr(self, attr): |
| """Return true if an attribute exists""" |
| return attr in self.module_attrs |
| |
| def cell_attrs(self, cell): |
| """All attributes of a given cell as a dictionary""" |
| return self.data["cells"][cell]["attributes"] |
| |
| def cell_attr(self, cell, attr, defval=None): |
| """Get an attribute of a given cell, or defval is not set""" |
| if attr in self.cell_attrs(cell): |
| return self.cell_attrs(cell)[attr] |
| else: |
| return defval |
| |
| def net_attrs(self, netname): |
| """Get all attributes of a given net as a dictionary""" |
| return self.data["netnames"][netname]["attributes"] |
| |
| def net_attr(self, netname, attr, defval=None): |
| """Get an attribute of a given net (specified by name), |
| or defval is not set""" |
| if attr in self.net_attrs(netname): |
| return self.net_attrs(netname)[attr] |
| else: |
| return defval |
| |
| PORT_REGEX = re.compile('(.*)(\\[([0-9]+)\\])') |
| |
| def port_attrs(self, port_pin): |
| """Gets all the attributes for for given pin |
| |
| Returns attributes dictionary: |
| ------- |
| |
| port_pin: str |
| """ |
| m = self.PORT_REGEX.match(port_pin) |
| if not m: |
| return self.net_attrs(port_pin) |
| port_name, _, port_idx = m.groups() |
| return self.net_attrs(port_name) |
| |
| # TODO: the below code is kind of ugly, but because module and cell IO |
| # specifications are inconsistent in how they are represented in the JSON, |
| # it's hard to make any nicer... |
| |
| def port_conns(self, port): |
| """The connections of a port |
| |
| Returns a list of connections: |
| ------- |
| net : int |
| """ |
| for pname, pdata in sorted(self.data["ports"].items()): |
| if pname == port: |
| return pdata["bits"] |
| |
| def cell_conns(self, cell, direction="input"): |
| """The connections of a cell in a given direction as a 2-tuple |
| |
| Returns a list of tuples: |
| ------- |
| port : str |
| net : int |
| """ |
| cdata = self.data["cells"][cell] |
| conns = [] |
| for port, condata in sorted(cdata["connections"].items()): |
| if cdata["port_directions"][port] == direction: |
| N = len(condata) |
| if N == 1: |
| conns.append((port, condata[0])) |
| else: |
| for i in range(N): |
| conns.append(("{}[{}]".format(port, i), condata[i])) |
| return conns |
| |
| def cell_conn_list(self, cell, port): |
| """The connections of a cell in a given port as a list |
| |
| Returns a list of connections: |
| ------- |
| net : list |
| |
| """ |
| data = self.data["cells"][cell] |
| |
| conns = [] |
| |
| for cport, condata in sorted(data["connections"].items()): |
| if cport == port: |
| conns = condata |
| |
| return conns |
| |
| def cell_clk_conn(self, cell): |
| """The clock net related to a given cell |
| |
| If exists returns the clock net |
| |
| net : list |
| """ |
| data = self.data["cells"][cell] |
| |
| if "CLK" in data["connections"].keys(): |
| return data["connections"]["CLK"] |
| else: |
| return None |
| |
| def conn_io(self, net, iodir): |
| """Returns a list of top level IO matching a direction and connected net number |
| |
| Returns a list: |
| ------- |
| port : str |
| """ |
| conn_io = [] |
| for port, pdata in sorted(self.data["ports"].items()): |
| if pdata["direction"] == iodir: |
| if net in pdata["bits"]: |
| if len(pdata["bits"]) == 1: |
| conn_io.append(port) |
| else: |
| conn_io.append( |
| "{}[{}]".format(port, pdata["bits"].index(net))) |
| return conn_io |
| |
| def conn_ports(self, net, pdir): |
| """Returns any cell ports matching a direction and connected net number |
| |
| Returns a list of tuples: |
| ------- |
| cell : str |
| port : str |
| """ |
| conn_ports = [] |
| for cell in sorted(self.data["cells"].keys()): |
| cdata = self.data["cells"][cell] |
| for port, condata in sorted(cdata["connections"].items()): |
| if cdata["port_directions"][port] == pdir: |
| if net in condata: |
| if len(condata) == 1: |
| conn_ports.append((cell, port)) |
| else: |
| conn_ports.append( |
| ( |
| cell, "{}[{}]".format( |
| port, condata.index(net)))) |
| return conn_ports |
| |
| def net_name(self, netid): |
| """Get a net name from a given netid. "netid" is a design net unique id |
| number. |
| |
| Returns string: |
| ------- |
| netid : int |
| """ |
| import sys |
| print("Netid", netid, file=sys.stderr) |
| names = [] |
| for n, props in self.data["netnames"].items(): |
| if netid in props['bits']: |
| names.append(n) |
| # FIXME: This fails when there are two wires with different names |
| # connected to the same netid (yes, that's possible). Which name |
| # should be returned in that case ? |
| if len(names) != 1: |
| raise KeyError("Net id {} not found".format(netid)) |
| return names[0] |
| |
| def net_attrs_by_netid(self, netid): |
| """ |
| Returns attributes of a given netid. Raises RuntimeError if the same |
| attribute is defined for two or more wires belonging to the same net. |
| The attribute 'src' is an exception, 'src' strings are concatenated |
| when defined for more than one wire. |
| |
| Returns dict: |
| ------- |
| netid : int |
| """ |
| |
| attributes = {} |
| |
| for name, data in self.data["netnames"].items(): |
| if netid in data['bits']: |
| |
| # Join attributes |
| for attr, value in data['attributes'].items(): |
| |
| # Allow multiple definitions of the same attribute only |
| # for the 'src'. Otherwise raise an exception |
| if attr in attributes and attr != 'src': |
| raise RuntimeError( |
| "Conflicting attributes '{}' on netid {}".format( |
| attr, netid)) |
| |
| # Join 'src' attribute strings |
| if attr in attributes and attr == 'src': |
| attributes[attr] += ";{}".format(value) |
| # Store the value |
| else: |
| attributes[attr] = value |
| |
| return attributes |
| |
| def net_drivers(self, net): |
| """Returns a list of drivers of a given net, both top level inputs. |
| and cell outputs. "cell" is set to the name of the module for top level |
| IO. |
| |
| Returns a list of tuples: |
| ------- |
| cell : str |
| port : str |
| """ |
| # top level *inputs* and cell outputs are both drivers |
| io_drivers = [(self.name, _) for _ in self.conn_io(net, "input")] |
| cell_drivers = self.conn_ports(net, "output") |
| return io_drivers + cell_drivers |
| |
| def net_sinks(self, net): |
| """Returns a list of sinks of a given net, both top level outputs. |
| and cell inputs. "cell" is set to the name of the module for top level |
| IO. |
| |
| Returns a list of tuples: |
| ------- |
| cell : str |
| port : str |
| """ |
| # top level *outputs* and cell inputs are both drivers |
| io_drivers = [(self.name, _) for _ in self.conn_io(net, "output")] |
| cell_drivers = self.conn_ports(net, "input") |
| return io_drivers + cell_drivers |
| |
| |
| class YosysJSON: |
| def __init__(self, j, top=None): |
| """Takes either the filename to a JSON file, or already parsed JSON |
| data as a dictionary. Optionally the top level module can be specified |
| too.""" |
| if (isinstance(j, str)): |
| with open(j, 'r') as f: |
| self.data = json.load(f) |
| else: |
| self.data = j |
| if top is None: |
| if len(self.data["modules"]) == 1: |
| self.top = list(self.data["modules"].keys())[0] |
| else: |
| self.top = None |
| else: |
| self.top = top |
| |
| def module(self, module): |
| """Get a given module (by name) as a `YosysModule`""" |
| if module not in self.data["modules"]: |
| raise KeyError( |
| "No yosys module named {} (only have {})".format( |
| module, self.data["modules"].keys())) |
| return YosysModule(module, self.data["modules"][module]) |
| |
| def modules_with_attr(self, attr_name, attr_value): |
| """Return a list of `YosysModule`s, selecting based on |
| a given attribute""" |
| mods = [] |
| for mod in self.data["modules"]: |
| ymod = self.module(mod) |
| if ymod.has_attr(attr_name) and ymod.attr(attr_name) == attr_value: |
| mods.append(ymod) |
| return mods |
| |
| def all_modules(self): |
| """Return a list of the names of all modules in the design, |
| sorted alphabetically""" |
| return sorted(self.data["modules"].keys()) |
| |
| @property |
| def top_module(self): |
| """Get a given module (by name) as a `YosysModule`""" |
| return self.module(self.top) |
| |
| def has_module(self, module): |
| """Return true if a module exists""" |
| return module in self.data["modules"] |
| |
| def get_module_file(self, module): |
| """Return the filename in which a given module lives""" |
| src = self.module(module).attr("src") |
| cpos = src.rfind(":") |
| return src[0:cpos] |