Move code from arch-defs Signed-off-by: Karol Gugala <kgugala@antmicro.com>
diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8a18954 --- /dev/null +++ b/setup.cfg
@@ -0,0 +1,8 @@ +[metadata] +license_files = COPYING + +[bdist_wheel] +universal=1 + +[aliases] +test=pytest
diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d5d3bef --- /dev/null +++ b/setup.py
@@ -0,0 +1,28 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="v2x", + version="0.0.1", + author="SymbiFlow Authors", + author_email="symbiflow@lists.librecores.org", + description="Python library for generating VPR architecture \ + description files from Verilog models.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/SymbiFlow/python-symbiflow-v2x", + packages=setuptools.find_packages(), + install_requires=['ply', 'pyjson'], + setup_requires=["pytest-runner"], + tests_require=["pytest"], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: ISC License", + "Operating System :: OS Independent", + ], + #entry_points={ + # 'console_scripts': ['sdf_timing_parse=sdf_timing.sdfparse:main'], + #}, +)
diff --git a/v2x/__init__.py b/v2x/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/v2x/__init__.py
diff --git a/v2x/__main__.py b/v2x/__main__.py new file mode 100644 index 0000000..f038ba4 --- /dev/null +++ b/v2x/__main__.py
@@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +from .v2x import vlog_to_pbtype + +def main(): + echo("main") + +if __name__ = '__main__': + main()
diff --git a/v2x/vlog_to_model.py b/v2x/vlog_to_model.py new file mode 100755 index 0000000..4280d0d --- /dev/null +++ b/v2x/vlog_to_model.py
@@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +""" +Convert a Verilog simulation model to a VPR `model.xml` + +The following Verilog attributes are considered on ports: + - `(* CLOCK *)` : force a given port to be a clock + + - `(* ASSOC_CLOCK="RDCLK" *)` : force a port's associated clock to a given value + +The following Verilog attributes are considered on modules: + - `(* MODEL_NAME="model" *)` : override the name used for <model> and for + ".subckt name" in the BLIF model. Mostly intended for use with w.py, when several + different pb_types implement the same model. + + - `(* CLASS="lut|routing|mux|flipflop|mem" *)` : specify the class of an given + instance. A model will not be generated for the `lut`, `routing` or `flipflop` + class. +""" +import argparse +import os +import re +import sys + +import lxml.etree as ET + +from .yosys import run +from .yosys.json import YosysJSON + +from .lib import xmlinc + + +def is_clock_assoc(infiles, module, clk, port, direction): + """Checks if a specific port is associated with a clk clock + + Returns a boolean value + ------- + is_clock_assoc: bool + """ + clock_assoc_signals = run.get_clock_assoc_signals( + infiles, module, clk + ) + + if direction == "input": + assoc_outputs = run.get_related_output_for_input( + infiles, module, port + ) + for out in assoc_outputs: + if out in clock_assoc_signals: + return True + elif direction == "output": + if port in clock_assoc_signals: + return True + else: + assert False, "Bidirectional ports are not supported yet" + + return False + + +def is_registered_path(tmod, pin, pout): + """Checks if a i/o path is sequential. If that is the case + no combinational_sink_port is needed + + Returns a boolean value + """ + + for cell, ctype in tmod.all_cells: + if ctype != "$dff": + continue + + if tmod.port_conns(pin) == tmod.cell_conn_list( + cell, "D") and tmod.port_conns(pout) == tmod.cell_conn_list( + cell, "Q"): + return True + + return False + +def vlog_to_model(infiles, includes, top, outfile=None): + iname = os.path.basename(infiles[0]) + + if outfile is None: + outfile = "model.xml" + + if includes: + for include in includes.split(','): + run.add_include(include) + + aig_json = run.vlog_to_json(infiles, flatten=True, aig=True) + + if top is not None: + top = top.upper() + else: + yj = YosysJSON(aig_json) + if yj.top is not None: + top = yj.top + else: + wm = re.match(r"([A-Za-z0-9_]+)\.sim\.v", iname) + if wm: + top = wm.group(1).upper() + else: + print( + """\ + ERROR file name not of format %.sim.v ({}), cannot detect top level. + Manually specify the top level module using --top""" + ).format(iname) + sys.exit(1) + + assert top is not None + yj = YosysJSON(aig_json, top) + if top is None: + print( + """\ + ERROR: more than one module in design, cannot detect top level. + Manually specify the top level module using --top""" + ) + sys.exit(1) + + tmod = yj.top_module + models_xml = ET.Element("models", nsmap={'xi': xmlinc.xi_url}) + + inc_re = re.compile(r'^\s*`include\s+"([^"]+)"') + + deps_files = set() + # XML dependencies need to correspond 1:1 with Verilog includes, so we have + # to do this manually rather than using Yosys + with open(infiles[0], 'r') as f: + for line in f: + im = inc_re.match(line) + if not im: + continue + deps_files.add(im.group(1)) + + if len(deps_files) > 0: + # Has dependencies, not a leaf model + for df in sorted(deps_files): + abs_base = os.path.dirname(os.path.abspath(infiles[0])) + abs_dep = os.path.normpath(os.path.join(abs_base, df)) + module_path = os.path.dirname(abs_dep) + module_basename = os.path.basename(abs_dep) + wm = re.match(r"([A-Za-z0-9_]+)\.sim\.v", module_basename) + if wm: + model_path = "{}/{}.model.xml".format( + module_path, + wm.group(1).lower() + ) + else: + assert False, "included Verilog file name {} does not follow pattern %%.sim.v".format( + module_basename + ) + xmlinc.include_xml( + parent=models_xml, + href=model_path, + outfile=outfile, + xptr="xpointer(models/child::node())" + ) + else: + # Is a leaf model + topname = tmod.attr("MODEL_NAME", top) + assert topname == topname.upper( + ), "Leaf model names should be all uppercase!" + modclass = tmod.attr("CLASS", "") + + if modclass not in ("lut", "routing", "flipflop"): + model_xml = ET.SubElement(models_xml, "model", {'name': topname}) + ports = tmod.ports + + inports_xml = ET.SubElement(model_xml, "input_ports") + outports_xml = ET.SubElement(model_xml, "output_ports") + + clocks = run.list_clocks(infiles, top) + + for name, width, bits, iodir in ports: + attrs = dict(name=name) + sinks = run.get_combinational_sinks(infiles, top, name) + + # Removes comb sinks if path from in to out goes through a dff + for sink in sinks: + if is_registered_path(tmod, name, sink): + sinks.remove(sink) + + # FIXME: Check if ignoring clock for "combination_sink_ports" is a + # valid thing to do. + if name in clocks or "clk" in name.lower(): + attrs["is_clock"] = "1" + else: + clks = list() + if len(sinks) > 0 and iodir == "input": + attrs["combinational_sink_ports"] = " ".join(sinks) + for clk in clocks: + if is_clock_assoc(infiles, top, clk, name, iodir): + clks.append(clk) + if clks: + attrs["clock"] = " ".join(clks) + if iodir == "input": + ET.SubElement(inports_xml, "port", attrs) + elif iodir == "output": + ET.SubElement(outports_xml, "port", attrs) + else: + assert False, "bidirectional ports not permitted in VPR models" + + if len(models_xml) == 0: + models_xml.insert(0, ET.Comment("this file is intentionally left blank")) + + f = open(outfile, 'w') + f.write(ET.tostring(models_xml, pretty_print=True).decode('utf-8')) + f.close() + print("Generated {} from {}".format(outfile, iname)) + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description=__doc__.strip()) + parser.add_argument( + 'infiles', + metavar='input.v', + type=str, + nargs='+', + help="""\ + One or more Verilog input files, that will be passed to Yosys internally. + They should be enough to generate a flattened representation of the model, + so that paths through the model can be determined. + """ + ) + parser.add_argument( + '--top', + help="""\ + Top level module, will usually be automatically determined from the file name + %.sim.v + """ + ) + parser.add_argument( + '--includes', + help="""\ + Comma separate list of include directories. + """, + default="" + ) + parser.add_argument('-o', help="""\ + Output filename, default 'model.xml' + """) + + args = parser.parse_args()
diff --git a/v2x/vlog_to_pbtype.py b/v2x/vlog_to_pbtype.py new file mode 100755 index 0000000..9cb7a6c --- /dev/null +++ b/v2x/vlog_to_pbtype.py
@@ -0,0 +1,878 @@ +#!/usr/bin/env python3 +"""\ +Convert a Verilog simulation model to a VPR `pb_type.xml` + +The following are allowed on a top level module: + + - `(* blackbox *)` : specify that the module has no interconnect or child + pb_types (but if modes are used then its modes are allowed to have these). + This will also set the BLIF model to be `.subckt <name>` unless CLASS is + also specified. + + - `(* CLASS="lut|routing|mux|flipflop|mem" *)` : specify the class of an given + instance. + + - `(* MODES="mode1; mode2; ..." *)` : specify that the module has more than one functional + mode, each with a given name. The module will be evaluated n times, each time setting + the MODE parameter to the nth value in the list of mode names. Each evaluation will be + put in a pb_type `<mode>` section named accordingly. + + - `(* MODEL_NAME="model" *)` : override the name used for <model> and for + ".subckt name" in the BLIF model. Mostly intended for use with w.py, when several + different pb_types implement the same model. + +The following are allowed on nets within modules (TODO: use proper Verilog timing): + - `(* SETUP="clk 10e-12" *)` : specify setup time for a given clock + + - `(* HOLD="clk 10e-12" *)` : specify hold time for a given clock + + - `(* CLK_TO_Q="clk 10e-12" *)` : specify clock-to-output time for a given clock + + - `(* DELAY_CONST_{input}="30e-12" *)` : specify a constant max delay from an input + (applied to the output) + + - `(* DELAY_MATRIX_{input}="30e-12 35e-12; 20e-12 25e-12; ..." *)` : specify a VPR + delay matrix (semicolons indicate rows). In this format columns specify + inputs bits and rows specify output bits. This should be applied to the output. + + - `(* carry = ADDER *)` : specify carry chain pack_pattern associated with this wire + +The following are allowed on ports: + - `(* CLOCK *)` : force a given port to be a clock + + - `(* ASSOC_CLOCK="RDCLK" *)` : force a port's associated clock to a given value + + - `(* PORT_CLASS="clock" *)` : specify the VPR "port_class" + + - `(* carry = ADDER *)` : specify carry chain pack_pattern associated with this port + +The Verilog define "PB_TYPE" is set during generation. +""" + +import os +import sys +import argparse +import re + +from typing import List, Dict, Tuple +from collections import defaultdict + +import lxml.etree as ET + +from .yosys import run +from .yosys.json import YosysJSON + +from .lib import xmlinc # noqa: E402 + + +def normalize_pb_name(pb_name): + """ Some pb_type names generatedby the tool + are illegal in VPR. This function converts them to + legal ones e.g: + + output_dffs_gen[0].q_out_ff -> output_dffs_gen_q_out_ff_0 + """ + if pb_name is None: + return None + + index = re.search(r'\[[0-9]+\](?!$)', pb_name) + normalized_name = pb_name.replace('.', '_') + if index is not None: + normalized_name = normalized_name.replace( + index.group(0), "" + ) + index.group(0).replace('[', '_').replace(']', '') + + return normalized_name + + +def is_mod_blackbox(mod): + """ Returns true if module is annotated with blackbox (or equivalent). + + Yosys supports 3 attributes that denote blackbox behavior: + + "blackbox" - Blackbox with no internal wiring + "whitebox" - Blackbox with internal connections and timing. + "lib_whitebox" - Like "whitebox" when read with "-lib", otherwise + attribute is removed. + + """ + + return (mod.attr("lib_whitebox", 0) == 1) or \ + (mod.attr("whitebox", 0) == 1) or \ + (mod.attr("blackbox", 0) == 1) + + +# $genblock$/vlog/tests/multiple_instance/multiple_instance.sim.v:12$64[57].\comb +GENBLOCK_REGEX = re.compile( + "^\\$genblock\\$.*:[0-9]+\\$[0-9]+\\[(.*)\\]\\.\\\\(.*)" +) + + +def strip_name(name: str, include_index=True) -> str: + """Convert generate block into normal array form. + + >>> n = r"$genblock$/vlog/tests/multiple_instance/multiple_instance.sim.v:12$64[57].\\comb" + >>> strip_name(n) + 'comb[57]' + >>> strip_name(n, False) + 'comb' + >>> n = r"$genblock$/vlog/tests/multiple_instance/multiple_instance.sim.v:15$10[3].\\comba" + >>> strip_name(n) + 'comba[3]' + """ + if name.startswith('$genblock$'): + index, name = GENBLOCK_REGEX.match(name).groups() + if include_index: + return "{}[{}]".format(name, index) + else: + return name + return name + + +CellName = str +CellType = str +PinName = str +CellPin = Tuple[CellName, PinName] + + +def create_port( + dir_xml: ET.Element, cell_pin: CellPin, direction: str, metadata=None +) -> ET.Element: + cell_name, pin_name = cell_pin + port = dict() + port['name'] = pin_name + port['type'] = direction + if cell_name: + port['from'] = cell_name + port_xml = ET.SubElement(dir_xml, 'port', port) + + if metadata: + meta_root = ET.SubElement(port_xml, "metadata") + for name, value in metadata.items(): + meta_type = ET.SubElement(meta_root, 'meta', {'name': name}) + meta_type.text = value + + return port_xml + + +def copy_attrs(dst, srcs): + # Find attributes which are on all srcs dictionaries + all_have = [] + for attr in set(sum((list(s.keys()) for s in srcs), [])): + if len(srcs) != sum(bool(attr in s) for s in srcs): + continue + all_have.append(attr) + + for attr in all_have: + avalue = srcs[0][attr] + avalues = [s[attr] for s in srcs[1:]] + + for other_avalue in avalues: + if avalue == other_avalue: + continue + raise ValueError('{} values: {}'.format(attr, [avalue] + avalues)) + + if attr in dst: + assert avalue == dst[attr] + raise ValueError( + '{} on net has value {} but pins have {}'.format( + attr, dst[attr], avalue + ) + ) + dst[attr] = avalue + + +def net_and_pin_attrs(yj, mod, driver: CellPin, sink: CellPin, netid: int): + def filter_src(x): + d = {} + for k, v in x.items(): + if k == 'src': + continue + d[k] = v + return d + + potential_attrs = [] + + driver_cell, driver_pin = driver + driver_type = mod.cell_type(driver_cell) + if driver_type is not None: + dmod = yj.module(driver_type) + potential_attrs.append(filter_src(dmod.port_attrs(driver_pin))) + + sink_cell, sink_pin = sink + sink_type = mod.cell_type(sink_cell) + if sink_type is not None: + smod = yj.module(sink_type) + potential_attrs.append(filter_src(smod.port_attrs(sink_pin))) + + net_attrs = filter_src(mod.net_attrs(mod.net_name(netid))) + copy_attrs(net_attrs, potential_attrs) + return net_attrs + + +def make_direct_conn( + ic_xml: ET.Element, driver: CellPin, sink: CellPin, path_attr: dict +) -> ET.Element: + dir_xml = ET.SubElement(ic_xml, 'direct') + create_port(dir_xml, driver, "input") + create_port(dir_xml, sink, "output") + + pack_name = path_attr.get('pack', False) + if pack_name: + pp_xml = ET.SubElement( + dir_xml, 'pack_pattern', { + 'name': pack_name, + 'type': 'pack' + } + ) + create_port(pp_xml, driver, "input") + create_port(pp_xml, sink, "output") + + carry_name = path_attr.get('carry', None) + if carry_name: + pp_xml = ET.SubElement( + dir_xml, 'pack_pattern', { + 'name': carry_name, + 'type': 'carry' + } + ) + create_port(pp_xml, driver, "input") + create_port(pp_xml, sink, "output") + + return dir_xml + + +def make_mux_conn( + ic_xml: ET.Element, mux_name: str, mux_inputs: Dict[CellPin, CellPin], + mux_outputs: Dict[CellPin, List[CellPin]] +) -> ET.Element: + + mux_xml = ET.SubElement(ic_xml, "mux", {"name": mux_name}) + for mux_input, driver in mux_inputs.items(): + create_port(mux_xml, driver, "input", metadata={'fasm_mux': mux_input}) + assert len(mux_outputs) == 1, mux_outputs + for mux_pin, sinks in mux_outputs.items(): + assert len(sinks) == 1, sinks + for sink_pin, path_attr in sinks: + create_port(mux_xml, sink_pin, "output") + + # <metadata> + meta_root = ET.SubElement(mux_xml, 'metadata') + # <meta name="type">bel</meta> + meta_type = ET.SubElement(meta_root, 'meta', {'name': 'type'}) + meta_type.text = "bel" + # <meta name="subtype">routing</meta> + meta_subtype = ET.SubElement(meta_root, 'meta', {'name': 'subtype'}) + meta_subtype.text = "routing" + + return mux_xml + + +def get_interconnects(yj, mod, mod_pname: str, + valid_names) -> Dict[CellPin, List[CellPin]]: + """Get the connectivity of module. + + Returns: + A dictionary containing with a list of sink pins for each driver pin. + """ + interconn = defaultdict(list) + for cname, ctype in mod.cells: + pb_name = strip_name(cname) + assert pb_name in valid_names + if pb_name == mod_pname: + pb_name = None + + # All interconnect going INTO a cell (top level or children). + inp_cons = mod.cell_conns(cname, "input") + for pin, net in inp_cons: + drvs = mod.net_drivers(net) + assert len(drvs) > 0, ( + "ERROR: pin {}.{} has no driver, interconnect will be missing\n{}" + .format(cname, pin, mod) + ) + assert len(drvs) < 2, ( + "ERROR: pin {}.{} has multiple drivers, interconnect will be overspecified" + .format(cname, pin) + ) + for drv_cell, drv_pin in drvs: + net_attr = net_and_pin_attrs( + yj, mod, (drv_cell, drv_pin), (pb_name, pin), net + ) + + drv_cell_name = strip_name(drv_cell) + assert drv_cell_name in valid_names + if drv_cell_name == mod_pname: + drv_cell_name = None + interconn[(drv_cell_name, + drv_pin)].append(((pb_name, pin), net_attr)) + + # Only consider outputs from cell to top level IO. + # Inputs to other cells will be dealt with in those cells. + out_cons = mod.cell_conns(cname, "output") + for pin, net in out_cons: + sinks = mod.net_sinks(net) + for sink_cell, sink_pin in sinks: + if sink_cell != mod.name: + continue + net_attr = net_and_pin_attrs( + yj, mod, (pb_name, pin), (None, sink_pin), net + ) + interconn[(pb_name, pin)].append(((None, sink_pin), net_attr)) + + import pprint + pprint.pprint(list(interconn.values())) + + def pin_sort(p): + pin, attr = p + if pin[0] is None: + return ('', pin[1]) + else: + return pin + + for l in interconn.values(): + l.sort(key=pin_sort) + + return interconn + + +def mode_interconnects(mod, mode_name) -> List[(CellPin)]: + interconn = [] + for name, width, bits, iodir in mod.ports: + if iodir == "input": + interconn.append(((None, name), (mode_name, name))) + else: + interconn.append(((mode_name, name), (None, name))) + return interconn + + +CellPrefix = str +ChildrenDict = Dict[CellPrefix, Tuple[CellType, List[CellName]]] + + +def get_children(yj, mod) -> Tuple[ChildrenDict, ChildrenDict]: + routing = dict() + children = dict() + for cname, ctype in mod.cells: + # We currently special case routing muxes + cell = yj.module(ctype) + if cell.CLASS == "routing": + d = routing + else: + d = children + cname_prefix = strip_name(cname, False) + if cname_prefix not in children: + d[cname_prefix] = (ctype, []) + assert d[cname_prefix][ + 0 + ] == ctype, \ + "Type of {} with prefix {} doesn't match existing. Type: {}, existing: {}".format( + cname, cname_prefix, ctype, children[cname_prefix] + ) + d[cname_prefix][-1].append(strip_name(cname)) + + for d in (routing, children): + for _, l in children.values(): + if len(l) > 1: + l.sort() + _, _ = get_list_name_and_length(l) + + return routing, children + + +def get_cellname_from_shortname(shortname, mod): + for cname, ctype in mod.cells: + if shortname != strip_name(cname): + continue + return cname + raise NameError("No cell named {}".format(shortname)) + + +def get_list_name_and_length(l: List[str]) -> Tuple[str, int]: + """ + >>> get_list_name_and_length(['i[{}]'.format(i) for i in range(10)]) + ('i', 10) + + Assertion failure on missing value + >>> get_list_name_and_length(['i[0]', 'i[2]']) + Traceback (most recent call last): + ... + AssertionError: index 1 expected: i[1] != actual: i[2] + ['i[0]', 'i[2]'] + + Assertion failure when not starting at zero + >>> get_list_name_and_length(['i[1]', 'i[2]']) + Traceback (most recent call last): + ... + AssertionError: index 0 expected: i[0] != actual: i[1] + ['i[1]', 'i[2]'] + + Assertion failure when duplicate values + + >>> get_list_name_and_length(['i[0]', 'i[0]']) + Traceback (most recent call last): + ... + AssertionError: index 1 expected: i[1] != actual: i[0] + ['i[0]', 'i[0]'] + + >>> get_list_name_and_length(['i[0]', 'i[1]', 'i[1]']) + Traceback (most recent call last): + ... + AssertionError: index 2 expected: i[2] != actual: i[1] + ['i[0]', 'i[1]', 'i[1]'] + + >>> get_list_name_and_length(['i[0]', 'i[1]', 'i[1]', 'i[2]']) + Traceback (most recent call last): + ... + AssertionError: index 2 expected: i[2] != actual: i[1] + ['i[0]', 'i[1]', 'i[1]', 'i[2]'] + + Assertion failure on incorrect formatting + >>> get_list_name_and_length(['i{}'.format(i) for i in range(4)]) + Traceback (most recent call last): + ... + AssertionError: No index brackets found in item 0: i0 + ['i0', 'i1', 'i2', 'i3'] + + Allow square brackets in name + >>> get_list_name_and_length(['i[1][{}]'.format(i) for i in range(4)]) + ('i[1]', 4) + """ + if not l: + return True + + assert '[' in l[0], "No index brackets found in item 0: {}\n{}".format( + l[0], l + ) + list_name = l[0][:l[0].rfind('[')] + sl = sorted(l, key=len) + for i in range(0, len(l)): + expected_item = "{}[{}]".format(list_name, i) + assert expected_item == sl[ + i], "index {} expected: {} != actual: {}\n{}".format( + i, expected_item, sl[i], sl + ) + return list_name, len(l) + + +def make_ports(clocks, mod, pb_type_xml, only_type=None): + for name, width, bits, iodir in mod.ports: + ioattrs = {"name": name, "num_pins": str(width)} + pclass = mod.net_attr(name, "PORT_CLASS") + if pclass is not None: + ioattrs["port_class"] = pclass + if name in clocks: + if only_type and only_type != "clocks": + continue + port_xml = ET.SubElement(pb_type_xml, "clock", ioattrs) + elif iodir == "input": + if only_type and only_type != "inputs": + continue + port_xml = ET.SubElement(pb_type_xml, "input", ioattrs) + elif iodir == "output": + if only_type and only_type != "outputs": + continue + port_xml = ET.SubElement(pb_type_xml, "output", ioattrs) + else: + assert False, "bidirectional ports not supported in VPR pb_types" + + port_attrs = mod.port_attrs(name) + + carry_name = port_attrs.get('carry', None) + if carry_name: + ET.SubElement( + port_xml, 'pack_pattern', { + 'name': carry_name, + 'type': 'carry' + } + ) + + +def make_container_pb( + outfile, yj, mod, mod_pname, pb_type_xml, routing, children +): + # Containers have to include children + # ------------------------------------------------------------ + for child_prefix, (child_type, children_names) in children.items(): + # Work out were the child pb_type file can be found + module_file = yj.get_module_file(child_type) + module_path = os.path.dirname(module_file) + module_basename = os.path.basename(module_file) + module_prefix = re.match(r"([A-Za-z0-9_]+)\.sim\.v", + module_basename).groups()[0] + + pb_type_path = "{}/{}.pb_type.xml".format(module_path, module_prefix) + + include_as_is = True + comment_str = "" + # Read the top level properties of the pb_type + with open(pb_type_path, 'r') as inc_xml: + xml_inc = ET.fromstring(inc_xml.read().encode('utf-8')) + inc_attrib = xml_inc.attrib + normalized_name = normalize_pb_name(child_prefix) + num_pb = str(len(children_names)) + if normalized_name != inc_attrib['name']: + comment_str += "old_name {}".format(inc_attrib['name']) + inc_attrib['name'] = normalize_pb_name(child_prefix) + include_as_is = False + if num_pb != inc_attrib['num_pb']: + comment_str += " old_num_pb {}".format(inc_attrib['num_pb']) + inc_attrib['num_pb'] = str(len(children_names)) + include_as_is = False + + xptr = None + parent_xml = pb_type_xml + if include_as_is is not True: + xptr = "xpointer(pb_type/child::node())" + parent_xml = ET.SubElement(pb_type_xml, 'pb_type', inc_attrib) + parent_xml.append(ET.Comment(comment_str)) + + xmlinc.include_xml( + parent=parent_xml, href=pb_type_path, outfile=outfile, xptr=xptr + ) + + # Contains need interconnect to their children + # ------------------------------------------------------------ + # Work out valid names for cells to sanity check the interconnects. + valid_names = [mod_pname] + + routing_cells = [] + for _, routing_names in routing.values(): + routing_cells.extend(routing_names) + valid_names.extend(routing_cells) + + for _, children_names in children.values(): + valid_names.extend(children_names) + + # Extract the interconnect from the module + interconn = get_interconnects(yj, mod, mod_pname, valid_names) + import pprint + print(mod_pname) + print("--") + pprint.pprint(interconn) + print("--") + print(routing_cells) + pprint.pprint(routing) + print("--") + + # Generate the actual interconnect + ic_xml = ET.SubElement(pb_type_xml, "interconnect") + for (driver_cell, driver_pin), sinks in interconn.items(): + if driver_cell in routing_cells: + continue + for (sink_cell, sink_pin), path_attr in sinks: + if sink_cell in routing_cells: + continue + make_direct_conn( + ic_xml, (normalize_pb_name(driver_cell), driver_pin), + (normalize_pb_name(sink_cell), sink_pin), path_attr + ) + + # Generate the mux interconnects + for mux_cell in routing_cells: + mux_outputs = defaultdict(list) + for (driver_cell, driver_pin), sinks in interconn.items(): + if driver_cell != mux_cell: + continue + mux_outputs[driver_pin].extend(sinks) + + assert len(mux_outputs) == 1, """\ +Mux {} has multiple outputs ({})! +Currently muxes can only drive a single output.""".format( + mux_cell, ", ".join(mux_outputs.keys()) + ) + for mux_output_pin, sinks in mux_outputs.items(): + assert len(sinks) == 1, """\ +Mux {}.{} has multiple outputs ({})! +Currently muxes can only drive a single output.""".format( + mux_cell, mux_output_pin, + ", ".join("{}.{}".format(*pin) for pin, path_attr in sinks) + ) + for (sink_cell, sink_pin), path_attr in sinks: + assert sink_cell not in routing_names, """\ +Mux {}.{} is trying to drive mux input pin {}.{}""".format( + mux_cell, mux_output_pin, sink_cell, sink_pin + ) + + mux_inputs = {} + for (driver_cell, driver_pin), sinks in interconn.items(): + for (sink_cell, mux_pin), path_attr in sinks: + if sink_cell != mux_cell: + continue + assert driver_cell not in routing_names, \ + "Mux {}.{} is trying to drive mux {}.{}".format( + driver_cell, driver_pin, mux_cell, sink_pin + ) + assert sink_pin not in mux_inputs, """\ +Pin {}.{} is trying to drive mux pin {}.{} (already driving by {}.{})""".format( + driver_cell, driver_pin, mux_cell, mux_pin, + *mux_inputs[sink_pin] + ) + mux_inputs[mux_pin] = (driver_cell, driver_pin) + + make_mux_conn(ic_xml, mux_cell, mux_inputs, mux_outputs) + + +def make_leaf_pb(outfile, yj, mod, mod_pname, pb_type_xml): + # As leaf node, need to generate timing information + def process_clocked_tmg(tmgspec, port, iodir, xmltype, xml_parent): + """Add a suitable timing spec if necessary to the pb_type""" + if tmgspec is not None: + splitspec = tmgspec.split(" ") + assert len( + splitspec + ) == 2, 'bad timing specification "{}", must be of format "clock value"'.format( + tmgspec + ) + attrs = {"port": port, "clock": splitspec[0]} + if xmltype == "T_clock_to_Q": + assert iodir == "output", \ + "Only output ports can have T_clock_to_Q timing definition." + "Port {}, direction {}.".format(port, iodir) + attrs["max"] = splitspec[1] + else: + assert iodir == "input", \ + "Only input ports can have {} timing definition." + "Port {}, direction {}.".format(xmltype, port, iodir) + attrs["value"] = splitspec[1] + ET.SubElement(xml_parent, xmltype, attrs) + + for name, width, bits, iodir in mod.ports: + port = "{}".format(name) + # Clocked timing + Tsetup = mod.net_attr(name, "SETUP") + Thold = mod.net_attr(name, "HOLD") + Tctoq = mod.net_attr(name, "CLK_TO_Q") + process_clocked_tmg(Tsetup, port, iodir, "T_setup", pb_type_xml) + process_clocked_tmg(Thold, port, iodir, "T_hold", pb_type_xml) + process_clocked_tmg(Tctoq, port, iodir, "T_clock_to_Q", pb_type_xml) + + # Combinational delays + dly_prefix = "DELAY_CONST_" + dly_mat_prefix = "DELAY_MATRIX_" + for attr, atvalue in mod.net_attrs(name).items(): + if attr.startswith(dly_prefix): + # Single, constant delays + inp = attr[len(dly_prefix):] + inport = "{}".format(inp) + ET.SubElement( + pb_type_xml, "delay_constant", { + "in_port": inport, + "out_port": port, + "max": str(atvalue) + } + ) + elif attr.startswith(dly_mat_prefix): + # Constant delay matrices + inp = attr[len(dly_mat_prefix):] + inport = "{}".format(inp) + mat = "\n" + atvalue.replace(";", "\n") + "\n" + xml_mat = ET.SubElement( + pb_type_xml, "delay_matrix", { + "in_port": inport, + "out_port": port, + "type": "max" + } + ) + xml_mat.text = mat + + +def make_pb_type( + infiles, outfile, yj, mod, mode_processing=False, mode_xml=None, mode_name=None +): + """Build the pb_type for a given module. mod is the YosysModule object to + generate.""" + + modes = mod.attr("MODES", None) + if modes is not None: + modes = modes.split(";") + mod_pname = mod.name + assert mod_pname == mod_pname.upper( + ), "pb_type name should be all uppercase. {}".format(mod_pname) + + pb_attrs = dict() + # If we are a blackbox with no modes, then generate a blif_model + is_blackbox = is_mod_blackbox(mod) or not mod.cells + has_modes = modes is not None + + print("is_blackbox", is_blackbox, "has_modes?", has_modes) + + # Process type and class of module + model_name = mod.attr("MODEL_NAME", mod.name) + assert model_name == model_name.upper( + ), "Model name should be uppercase. {}".format(model_name) + mod_cls = mod.CLASS + if mod_cls is not None: + if mod_cls == "lut": + pb_attrs["blif_model"] = ".names" + pb_attrs["class"] = "lut" + elif mod_cls == "routing": + # TODO: pb_attrs["class"] = "routing" + pass + elif mod_cls == "mux": + # TODO: ? + pass + elif mod_cls == "flipflop": + pb_attrs["blif_model"] = ".latch" + pb_attrs["class"] = "flipflop" + else: + assert False, "unknown class {}".format(mod_cls) + elif is_blackbox and not has_modes: + pb_attrs["blif_model"] = ".subckt " + model_name + + # set num_pb to 1, it will be updated if this pb_type + # will be included by another one + if mode_xml is None: + pb_type_xml = ET.Element( + "pb_type", { + "num_pb": "1", + "name": mod_pname + }, + nsmap={'xi': xmlinc.xi_url} + ) + else: + pb_type_xml = ET.SubElement( + mode_xml, + "pb_type", { + "num_pb": "1", + "name": mode_name + }, + nsmap={'xi': xmlinc.xi_url} + ) + + if 'blif_model' in pb_attrs: + ET.SubElement(pb_type_xml, "blif_model", + {}).text = pb_attrs["blif_model"] + + if 'class' in pb_attrs: + ET.SubElement(pb_type_xml, "pb_class", {}).text = pb_attrs["class"] + + # Create the pins for this pb_type + clocks = run.list_clocks(infiles, mod.name) + make_ports(clocks, mod, pb_type_xml, "clocks") + make_ports(clocks, mod, pb_type_xml, "inputs") + make_ports(clocks, mod, pb_type_xml, "outputs") + + if modes and not mode_processing: + for mode in modes: + smode = mode.strip() + mode_xml = ET.SubElement(pb_type_xml, "mode", {"name": smode}) + # Rerun Yosys with mode parameter + mode_yj = YosysJSON( + run.vlog_to_json( + infiles, + flatten=False, + aig=False, + mode=smode, + module_with_mode=mod.name + ) + ) + mode_mod = mode_yj.module(mod.name) + make_pb_type(infiles, outfile, mode_yj, mode_mod, True, mode_xml, smode) + + # if mode pb_type contains interconnect tag, add new connctions there + ic_xml = mode_xml.find("interconnect") + print("ic_xml is", ic_xml, file=sys.stderr) + if ic_xml is None: + ic_xml = ET.SubElement(mode_xml, "interconnect") + + for (driver_cell, + driver_pin), (sink_cell, + sink_pin) in mode_interconnects(mod, smode): + make_direct_conn( + ic_xml, (driver_cell, driver_pin), (sink_cell, sink_pin), + {} + ) + + if not modes or mode_processing: + routing = children = [] + if not is_blackbox: + routing, children = get_children(yj, mod) + + if routing or children: + make_container_pb( + outfile, yj, mod, mod_pname, pb_type_xml, routing, children + ) + else: + make_leaf_pb(outfile, yj, mod, mod_pname, pb_type_xml) + + return pb_type_xml + + +parser = argparse.ArgumentParser( + description=__doc__.strip(), formatter_class=argparse.RawTextHelpFormatter +) +parser.add_argument( + 'infiles', + metavar='input.v', + type=str, + nargs='+', + help="""\ +One or more Verilog input files, that will be passed to Yosys internally. +They should be enough to generate a flattened representation of the model, +so that paths through the model can be determined. +""" +) +parser.add_argument( + '--top', + help="""\ +Top level module, will usually be automatically determined from the file name +%.sim.v +""" +) +parser.add_argument( + '--outfile', + '-o', + type=argparse.FileType('w'), + default="pb_type.xml", + help="""\ +Output filename, default 'model.xml' +""" +) + + +def vlog_to_pbtype(infiles, outfile, top=None): + iname = os.path.basename(infiles[0]) + + outfile = open(outfile, 'w') + + run.add_define("PB_TYPE") + vjson = run.vlog_to_json(infiles, flatten=False, aig=False) + yj = YosysJSON(vjson) + + if top is not None: + top = top + else: + wm = re.match(r"([A-Za-z0-9_]+)\.sim\.v", iname) + if wm: + top = wm.group(1).upper() + else: + print( + "ERROR file name not of format %.sim.v ({}), cannot detect top level." + " Manually specify the top level module using --top". + format(iname) + ) + sys.exit(1) + + top = top.upper() + + tmod = yj.module(top) + + pb_type_xml = make_pb_type(infiles, outfile.name, yj, tmod) + + outfile.write( + ET.tostring( + pb_type_xml, + pretty_print=True, + encoding="utf-8", + xml_declaration=True + ).decode('utf-8') + ) + print("Generated {} from {}".format(outfile.name, iname)) + outfile.close() + + +if __name__ == "__main__": + import doctest + doctest.testmod() + args = parser.parse_args() + sys.exit(main(args))
diff --git a/v2x/yosys/__init__.py b/v2x/yosys/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/v2x/yosys/__init__.py
@@ -0,0 +1 @@ +#!/usr/bin/env python3
diff --git a/v2x/yosys/json.py b/v2x/yosys/json.py new file mode 100755 index 0000000..d743127 --- /dev/null +++ b/v2x/yosys/json.py
@@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +""" +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) + if len(names) != 1: + raise KeyError("Net id {} not found".format(netid)) + return names[0] + + 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]
diff --git a/v2x/yosys/run.py b/v2x/yosys/run.py new file mode 100755 index 0000000..842f7e9 --- /dev/null +++ b/v2x/yosys/run.py
@@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +import json +import os +import re +import subprocess +import tempfile +from . import utils + + +def get_verbose(): + """Return if in verbose mode.""" + verbose = 0 + for e in ["V", "VERBOSE"]: + if e not in os.environ: + continue + verbose = int(os.environ[e]) + break + return verbose > 0 + + +def get_yosys(): + """Return how to execute Yosys: the value of $YOSYS if set, otherwise just + `yosys`.""" + return os.getenv("YOSYS", "yosys") + + +def get_yosys_common_args(): + return ["-e", "wire '[^']*' is assigned in a block", "-q"] + + +def get_output(params): + """Run Yosys with given command line parameters, and return stdout as a string""" + + verbose = get_verbose() + + cmd = [get_yosys()] + get_yosys_common_args() + params + if verbose: + print(cmd) + + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Get the output + stdout, stderr = p.communicate() + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") + + retcode = p.wait() + + msg = "" + msg += "stdout" + "=" * 75 + "\n" + msg += stdout + "\n" + msg += "stderr" + "-" * 75 + "\n" + msg += stderr + "\n" + msg += "=" * 75 + "\n" + msg += "Return Code: {}".format(retcode) + + if verbose: + print(msg) + + if retcode != 0: + emsg = "" + emsg += "Failed to run {}".join(" ".join(cmd)) + "\n" + emsg += msg + + raise subprocess.CalledProcessError(retcode, cmd, emsg) + return stdout + + +defines = [] +includes = [] + + +def add_define(defname): + """Add a Verilog define to the list of defines to set in Yosys""" + defines.append(defname) + + +def get_defines(): + """Return a list of set Verilog defines, as a list of arguments to pass to Yosys +`read_verilog`""" + return " ".join(["-D" + _ for _ in defines]) + + +def add_include(path): + """ Add a path to search when reading verilog to the list of includes set in Yosys""" + includes.append(path) + + +def get_includes(): + """Return a list of include directories, as a list of arguments to pass to Yosys +`read_verilog`""" + return " ".join(["-I" + _ for _ in includes]) + + +def commands(commands, infiles=[]): + """Run a given string containing Yosys commands + + Inputs + ------- + commands : string of Yosys commands to run + infiles : list of input files + """ + commands = "read_verilog {} {} {}; ".format( + get_defines(), get_includes(), " ".join(infiles) + ) + commands + params = ["-q", "-p", commands] + return get_output(params) + + +def script(script, infiles=[]): + """Run a Yosys script given a path to the script + + Inputs + ------- + script : path to Yosys script to run + infiles : list of input files + """ + params = ["-q", "-s", script] + infiles + return get_output(params) + + +def vlog_to_json( + infiles, flatten=False, aig=False, mode=None, module_with_mode=None +): + """ + Convert Verilog to a JSON representation using Yosys + + Inputs + ------- + infiles : list of input files + flatten : set to flatten output hierarchy + aig : generate And-Inverter-Graph modules for gates + mode : set to a value other than None to use `chparam` to set the value of the MODE parameter + module_with_mode : the name of the module to apply `mode` to + """ + prep_opts = "-flatten" if flatten else "" + json_opts = "-aig" if aig else "" + if mode is not None: + mode_str = 'chparam -set MODE "{}" {}; '.format(mode, module_with_mode) + else: + mode_str = "" + cmds = "{}prep {}; write_json {}".format(mode_str, prep_opts, json_opts) + j = utils.strip_yosys_json(commands(cmds, infiles)) + """with open('dump.json', 'w') as dbg: + print(j,file=dbg)""" + return json.loads(j) + + +def extract_pin(module, pstr, _regex=re.compile(r"([^/]+)/([^/]+)")): + """ + Extract the pin from a line of the result of a Yosys select command, or + None if the command result is irrelevant (e.g. does not correspond to the + correct module) + + Inputs + ------- + module: Name of module to extract pins from + pstr: Line from Yosys select command (`module/pin` format) + """ + m = re.match(r"([^/]+)/([^/]+)", pstr) + if m and m.group(1) == module: + return m.group(2) + else: + return None + + +def do_select(infiles, module, expr, prep=False, flatten=False): + """ + Run a Yosys select command (given the expression and input files) on a module + and return the result as a list of pins + + Inputs + ------- + infiles: List of Verilog source files to pass to Yosys + module: Name of module to run command on + expr: Yosys selector expression for select command + + prep: Run prep command before selecting. + flatten: Flatten module when running prep. + """ + + # TODO: All of these functions involve a fairly large number of calls to + # Yosys. Although performance here is unlikely to be a major priority any + # time soon, it might be worth investigating better options? + + f = "" + if flatten: + f = "-flatten" + + p = "" + if prep: + p = "prep -top {} {};".format(module, f) + else: + p = "proc;" + + outfile = tempfile.mktemp() + sel_cmd = "{} cd {}; select -write {} {}".format(p, module, outfile, expr) + commands(sel_cmd, infiles) + pins = [] + with open(outfile, 'r') as f: + for net in f: + snet = net.strip() + if (len(snet) > 0): + pin = extract_pin(module, snet) + if pin is not None: + pins.append(pin) + os.remove(outfile) + return pins + + +def get_combinational_sinks(infiles, module, innet): + """Return a list of output ports which are combinational sinks of a given + input. + + Inputs + ------- + infiles: List of Verilog source files to pass to Yosys + module: Name of module to run command on + innet: Name of input net to find sinks of + """ + return do_select( + infiles, module, "{} %co* o:* %i {} %d".format(innet, innet) + ) + + +def list_clocks(infiles, module): + """Return a list of clocks in the module + + Inputs + ------- + infiles: List of Verilog source files to pass to Yosys + module: Name of module to run command on + """ + return do_select( + infiles, module, + "c:* %x:+[CLK]:+[clk]:+[clock]:+[CLOCK] a:CLOCK=1 %u c:* %d x:* %i" + ) + + +def get_clock_assoc_signals(infiles, module, clk): + """Return the list of signals associated with a given clock. + + Inputs + ------- + infiles: List of Verilog source files to pass to Yosys + module: Name of module to run command on + clk: Name of clock to find associated signals + """ + return do_select( + infiles, module, + "select -list {} %a %co* %x i:* o:* %u %i a:ASSOC_CLOCK={} %u {} %d". + format(clk, clk, clk) + ) + + +# Find things which affect the given output +# show w:*D_IN_0 %a %ci* + +# Find things which are affected by the given clock. +# show w:*INPUT_CLK %a %co* + +# Find things which are affect by the given signal - combinational only. +# select -list w:*INPUT_CLK %a %co* %x x:* %i + + +def get_related_output_for_input(infiles, module, signal): + """. + + Inputs + ------- + infiles: List of Verilog source files to pass to Yosys + module: Name of module to run command on + clk: Name of clock to find associated signals + """ + return do_select( + infiles, module, "select -list w:*{} %a %co* o:* %i".format(signal) + ) + + +def get_related_inputs_for_input(infiles, module, signal): + """. + + Inputs + ------- + infiles: List of Verilog source files to pass to Yosys + module: Name of module to run command on + clk: Name of clock to find associated signals + """ + return [ + x for x in do_select( + infiles, module, + "select -list w:*{} %a %co* %x i:* %i".format(signal) + ) if x != signal + ]
diff --git a/v2x/yosys/utils.py b/v2x/yosys/utils.py new file mode 100644 index 0000000..722ac9b --- /dev/null +++ b/v2x/yosys/utils.py
@@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import re +"""The JSON Yosys outputs isn't acutally compliant JSON, as it contains C-style +comments. These must be stripped.""" + + +def strip_yosys_json(text): + stripped = re.sub(r'\\\n', '', text) + stripped = re.sub(r'//.*\n', '\n', stripped) + stripped = re.sub(r'/\*.*\*/', '', stripped) + return stripped