| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| # | |
| # Copyright (C) 2020 The SymbiFlow Authors. | |
| # | |
| # Use of this source code is governed by a ISC-style | |
| # license that can be found in the LICENSE file or at | |
| # https://opensource.org/licenses/ISC | |
| # | |
| # SPDX-License-Identifier: ISC | |
| """ | |
| 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] |