diff options
Diffstat (limited to 'host/python/uhd/imgbuilder')
15 files changed, 1460 insertions, 0 deletions
diff --git a/host/python/uhd/imgbuilder/__init__.py b/host/python/uhd/imgbuilder/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/host/python/uhd/imgbuilder/__init__.py diff --git a/host/python/uhd/imgbuilder/image_builder.py b/host/python/uhd/imgbuilder/image_builder.py new file mode 100755 index 000000000..4f5f416d6 --- /dev/null +++ b/host/python/uhd/imgbuilder/image_builder.py @@ -0,0 +1,847 @@ +""" +Copyright 2019 Ettus Research, A National Instrument Brand + +SPDX-License-Identifier: GPL-3.0-or-later + +RFNoC image builder: All the algorithms required to turn either a YAML +description or a GRC file into an rfnoc_image_core.v file. +""" + +from collections import deque +from collections import OrderedDict + +import logging +import os +import re +import sys + +import six +import mako.lookup +import mako.template +from mako import exceptions +from ruamel import yaml + +### DATA ###################################################################### +# Directory under the FPGA repo where the device directories are +USRP3_TOP_DIR = os.path.join('usrp3', 'top') + +USRP3_LIB_RFNOC_DIR = os.path.join('usrp3', 'lib', 'rfnoc') + +# Subdirectory for the core YAML files +RFNOC_CORE_DIR = os.path.join('rfnoc', 'core') + +# Path to the system's bash executable +BASH_EXECUTABLE = '/bin/bash' # FIXME this should come from somewhere + +# Map device names to the corresponding directory under usrp3/top +DEVICE_DIR_MAP = { +    'x300': 'x300', +    'x310': 'x300', +    'e300': 'e300', +    'e310': 'e31x', +    'e320': 'e320', +    'n300': 'n3xx', +    'n310': 'n3xx', +    'n320': 'n3xx', +} + +# Picks the default make target per device +DEVICE_DEFAULTTARGET_MAP = { +    'x300': 'X300_HG', +    'x310': 'X310_HG', +    'e310': 'E310_SG3', +    'e320': 'E320_1G', +    'n300': 'N300_HG', +    'n310': 'N310_HG', +    'n320': 'N320_XG', +} + + +# Adapted from code found at +# https://stackoverflow.com/questions/5121931/ +#     in-python-how-can-you-load-yaml-mappings-as-ordereddicts +# (Accessed 17 October 2019) +def ordered_load(stream, Loader=yaml.SafeLoader, object_pairs_hook=OrderedDict): +    """ +    In Python 3.5, element insertion order into dictionaries is not preserved. +    This function uses an OrderedDict to read a YAML file, which does preserve order. +    """ +    class OrderedLoader(Loader): +        pass +    def construct_mapping(loader, node): +        loader.flatten_mapping(node) +        return object_pairs_hook(loader.construct_pairs(node)) +    OrderedLoader.add_constructor( +        yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, +        construct_mapping) +    return yaml.load(stream, OrderedLoader) + +def split(iterable, function): +    """ +    Split an iterable by condition. Matching items are returned in the first +    deque of the returned tuple unmatched in the second +    :param iterable: an iterable to split +    :param function: an expression that returns True/False for iterable values +    :return: 2-tuple with deque for matching/non-matching items +    """ +    dq_true = deque() +    dq_false = deque() + +    deque(((dq_true if function(item) else dq_false).append(item) +           for item in iterable), maxlen=0) + +    return dq_true, dq_false + + +def expand_io_port_desc(io_ports, signatures): +    """ +    Add a wires entry to each io port dictionary entry which contains a +    complete list of wires for the specific port according to the information +    in signature file. Each wire entry contains: +    * fully resolved wire name (wire name as in signature or replaced name +      with respect to regular expression if some is given, regular expression +      should use back references to retain original wire name). +    * width in bits +    * direction as input/output depending on whether the port is a +      master/broadcaster or slave/listener and the wire is described as from +      or to master +    :param io_ports: io port dictionary from yml configuration +    :param signatures: signature description from yml configuration +    :return: None +    """ +    for name, io_port in six.iteritems(io_ports): +        wires = [] +        for signature in signatures[io_port["type"]]["ports"]: +            width = signature.get("width", 1) +            wire_type = signature.get("type", None) +            drive = io_port["drive"] +            direction = {"master": {"from-master": "input ", "to-master": "output"}, +                         "slave":  {"from-master": "output", "to-master": "input "}, +                         "broadcaster":  {None: "input "}, +                         "listener":  {None: "output"}}[drive][wire_type] + +            signature_name = signature["name"] +            if "rename" in io_port: +                signature_name = re.sub(io_port["rename"]["pattern"], +                                        io_port["rename"]["repl"], +                                        signature_name, 1) + +            wires.append({"direction": direction, +                          "width": width, +                          "name": signature_name}) +        io_port["wires"] = wires + +# pylint: disable=too-few-public-methods +class IOConfig: +    """ +    Class containing configuration from a yml file. + +    Each top level entry is translated into a class member variable. If the +    configuration contains an io_ports section the ports get a wire list which +    is derived from the signature file. This allows easier processing of IO +    ports in the mako templates and failures from yml configuration files fail +    in this script rather than during template processing which is easier to +    track and debug. +    """ +    def __init__(self, config, signatures): +        # read configuration from config dictionary +        # TODO: Is this guaranteed ordered? +        self.__dict__.update(**config) +        if hasattr(self, "io_ports"): +            expand_io_port_desc(getattr(self, "io_ports"), signatures) + + +class ImageBuilderConfig: +    """ +    Holds the complete image configuration settings. This includes +    * the image configuration itself as it is passed to the script +    * all noc block configurations found by the script +    * device configuration information as found in the bsp.yml of the device +      information passed to the script. +    """ +    # pylint: disable=too-many-instance-attributes +    def __init__(self, config, blocks, device): +        self.noc_blocks = OrderedDict() +        self.stream_endpoints = OrderedDict() +        self.connections = [] +        self.clk_domains = [] +        self.block_ports = OrderedDict() +        self.io_ports = OrderedDict() +        self.clocks = OrderedDict() +        self.block_con = [] +        self.io_port_con_ms = [] +        self.io_port_con_bl = [] +        self.clk_domain_con = [] +        # read configuration from config dictionary +        self.__dict__.update(**config) +        self.blocks = blocks +        self.device = device +        self._update_sep_defaults() +        self._set_indices() +        self._collect_noc_ports() +        self._collect_io_ports() +        self._collect_clocks() +        self.pick_connections() +        self.pick_clk_domains() + +    def _update_sep_defaults(self): +        """ +        Update any missing stream endpoint attributes with default values +        """ +        for sep in self.stream_endpoints: +            if "num_data_i" not in self.stream_endpoints[sep]: +                self.stream_endpoints[sep]["num_data_i"] = 1 +            if "num_data_o" not in self.stream_endpoints[sep]: +                self.stream_endpoints[sep]["num_data_o"] = 1 + +    def _set_indices(self): +        """ +        Add an index for each port of each stream endpoint and noc block. +        These indices are used to generate static_router.hex +        """ +        start = 1 +        i = 0 +        for i, sep in enumerate(self.stream_endpoints.values()): +            sep["index"] = i + start +        start = start + i + 1 +        for i, block in enumerate(self.noc_blocks.values()): +            block["index"] = start + i + +    def _collect_noc_ports(self): +        """ +        Create lookup table for noc blocks. The key is a tuple of block +        name, port name and flow direction. If any block port has num_ports > 1 +        then unroll that port into multiple ports of the same name plus a +        number to make its name unique. +        """ +        for name, block in six.iteritems(self.noc_blocks): +            desc = self.blocks[block["block_desc"]] +            # Update per-instance parameters +            if not hasattr(desc, "parameters"): +                setattr(desc, "parameters", {}) +            if "parameters" not in block: +                block["parameters"] = OrderedDict() +            for key in block["parameters"].keys(): +                if key not in desc.parameters: +                    logging.error("Unknown parameter %s for block %s", key, name) +                    del block["parameters"][key] +            for param, value in six.iteritems(desc.parameters): +                if param not in block["parameters"]: +                    block["parameters"][param] = value +            # Generate list of block ports, adding 'index' to each port's dict +            for direction in ("inputs", "outputs"): +                index = 0 +                for port_name, port_info in desc.data[direction].items(): +                    num_ports = 1 +                    if "num_ports" in port_info: +                        parameter = port_info["num_ports"] +                        num_ports = parameter +                        if parameter in block["parameters"]: +                            num_ports = block["parameters"][parameter] +                    # Make sure the parameter resolved to a number +                    if not isinstance(num_ports, int): +                        logging.error( +                            "'num_ports' of port '%s' on block '%s' " +                            "resolved to invalid value of '%s'", +                            port_name, name, str(num_ports)) +                        sys.exit(1) +                    if num_ports < 1 or num_ports > 64: +                        logging.error( +                            "'num_ports' of port '%s' on block '%s' " +                            "has invalid value '%s', must be in [1, 64]", +                            port_name, name, str(num_ports)) +                        sys.exit(1) +                    if "num_ports" in port_info: +                        # If num_ports was a variable in the YAML, unroll into +                        # multiple ports +                        for i in range(num_ports): +                            new_port_info = port_info.copy() +                            new_port_info['index'] = index +                            index = index + 1 +                            self.block_ports.update({(name, port_name + "_" \ +                                + str(i), direction[:-1]) : new_port_info}) +                    else: +                        port_info['index'] = index +                        self.block_ports.update( +                            {(name, port_name, direction[:-1]) : port_info}) +                        index = index + 1 +        ports = self.stream_endpoints +        for sep in self.stream_endpoints: +            inputs = {(sep, "in%d" % port, "input") : +                      ports[sep] for port in range(ports[sep]["num_data_i"])} +            self.block_ports.update(inputs) +            outputs = {(sep, "out%d" % port, "output") : +                       ports[sep] for port in range(ports[sep]["num_data_o"])} +            self.block_ports.update(outputs) + +    def _collect_io_ports(self): +        """ +        Create lookup table for io ports. The key is a tuple of block name +        (_device_ for io ports of the bsp), the io port name and flow +        direction. +        """ +        for name, block in six.iteritems(self.noc_blocks): +            desc = self.blocks[block["block_desc"]] +            if hasattr(desc, "io_ports"): +                self.io_ports.update({ +                    (name, io, desc.io_ports[io]["drive"]): +                    desc.io_ports[io] for io in desc.io_ports}) +        self.io_ports.update({ +            ("_device_", io, self.device.io_ports[io]["drive"]): +            self.device.io_ports[io] for io in self.device.io_ports}) + +    def _collect_clocks(self): +        """ +        Create lookup table for clocks. The key is a tuple of block name +        (_device_ for clocks of the bsp), the clock name and flow +        direction +        """ +        for name, block in six.iteritems(self.noc_blocks): +            desc = self.blocks[block["block_desc"]] +            if hasattr(desc, "clocks"): +                self.clocks.update({ +                    (name, clk["name"]): clk for clk in desc.clocks}) +        if hasattr(self.device, "clocks"): +            self.clocks.update({ +                ("_device_", clk["name"]): clk for clk in self.device.clocks}) +        # Add the implied clocks for the BSP +        self.clocks[("_device_", "rfnoc_ctrl")] = {"freq": '[]', "name": "rfnoc_ctrl"} +        self.clocks[("_device_", "rfnoc_chdr")] = {"freq": '[]', "name": "rfnoc_chdr"} + +    def pick_clk_domains(self): +        """ +        Filter clock domain list into a local list for easier access. +        Remaining connection items are printed as error and execution is +        aborted. Likewise, checks for unconnected clocks. +        """ +        (self.clk_domain_con, self.clk_domains) = split( +            self.clk_domains, lambda con: +            (con["srcblk"], con["srcport"]) in self.clocks and +            (con["dstblk"], con["dstport"]) in self.clocks) + +        # Check if there are unconnected clocks +        connected = [(con["dstblk"], con["dstport"]) for con in self.clk_domain_con] +        unconnected = [] +        for clk in self.clocks: +            if clk[0] != "_device_" and \ +               clk[1] not in ["rfnoc_ctrl", "rfnoc_chdr"] and \ +               clk not in connected: +                unconnected.append(clk) +        if unconnected: +            logging.error("%d unresolved clk domain(s)", len(unconnected)) +            for clk in unconnected: +                logging.error("    %s:%s", clk[0], clk[1]) +            logging.error("Please specify the clock(s) to connect") +            sys.exit(1) + +        if self.clk_domains: +            logging.error("%d Unresolved clk domain(s)", len(self.clk_domains)) + +            for connection in self.clk_domains: +                logging.error("    (%s-%s -> %s-%s)", +                              connection["srcblk"], connection["srcport"], +                              connection["dstblk"], connection["dstport"]) +            logging.error("Source or destination domain not found") +            sys.exit(1) + +    def pick_connections(self): +        """ +        Sort connection list into three local lists for +         * input => output (block port to block port) +         * master => slave (io port to io port) +         * broadcaster => listener (io port to io port) +        Remaining connection items are printed as error and execution is +        aborted. Possible reasons are +         * undeclared block or io port +         * connection direction wrong (e.g. output => input) +         * mixed connection type (e.g. master => listener) +        """ +        block_types = lambda type: filter(lambda key: key[2] == type, self.block_ports) +        io_types = lambda type: filter(lambda key: key[2] == type, self.io_ports) +        (self.block_con, self.connections) = split( +            self.connections, lambda con: +            (con["srcblk"], con["srcport"], "output") in block_types("output") and +            (con["dstblk"], con["dstport"], "input") in block_types("input")) +        (self.io_port_con_ms, self.connections) = split( +            self.connections, lambda con: +            (con["srcblk"], con["srcport"], "master") in io_types("master") and +            (con["dstblk"], con["dstport"], "slave") in  io_types("slave")) +        (self.io_port_con_bl, self.connections) = split( +            self.connections, lambda con: +            (con["srcblk"], con["srcport"], "broadcaster") in io_types("broadcaster") and +            (con["dstblk"], con["dstport"], "listener") in io_types("listener")) + +        if self.connections: +            logging.error("%d Unresolved connection(s)", len(self.connections)) + +            for connection in self.connections: +                logging.error("    (%s-%s -> %s-%s)", +                              connection["srcblk"], connection["srcport"], +                              connection["dstblk"], connection["dstport"]) +            logging.debug("    Make sure block ports are connected output " +                          "(src) to input (dst)") +            logging.debug("    Available block ports for connections:") +            for block in self.block_ports: +                logging.debug("        %s", (block,)) +            logging.debug("    Make sure io ports are connected master      " +                          "(src) to slave    (dst)") +            logging.debug("                                  or broadcaster " +                          "(src) to listener (dst)") +            logging.debug("    Available io ports for connections:") +            for io_port in self.io_ports: +                logging.info("        %s", (io_port,)) +            sys.exit(1) + +def load_config(filename): +    """ +    Loads yml configuration from filename. + +    Configuration files are searched in folder returned by get_get_config_path. +    This method logs error and exits on IO failure + +    :param filename: yml configuration to load +    :return: IO signatures as dictionary +    """ +    dirname, basename = os.path.split(filename) +    try: +        with open(filename) as stream: +            logging.info( +                "Using %s from %s.", basename, os.path.normpath(dirname)) +            config = ordered_load(stream) +        return config +    except IOError: +        logging.error("%s misses %s", os.path.normpath(dirname), basename) +        sys.exit(1) + +def device_config(config_path, device): +    """ +    Load device config from bsp.yml + +    Location of bsp.yml is derived from the device chosen in the arguments + +    :param config_path: location of core configuration files +    :param device: device to build for +    :return: device configuration as dictionary +    """ +    return load_config(os.path.join(config_path, "%s_bsp.yml" % device.lower())) + + +def io_signatures(config_path): +    """ +    Load IO signatures from io_signatures.yml + +    :param config_path: location of core configuration files +    :return: IO signatures as dictionary +    """ +    return load_config(os.path.join(config_path, "io_signatures.yml")) + + +def read_grc_block_configs(path): +    """ +    Reads RFNoC config block used by Gnuradio Companion +    :param path: location of grc block configuration files +    :return: dictionary of block (id mapped to description) +    """ +    result = {} + +    for root, dirs, names in os.walk(path): +        for name in names: +            if re.match(r".*\.block\.yml", name): +                with open (os.path.join(root, name)) as stream: +                    config = ordered_load(stream) +                    result[config["id"]] = config + +    return result + + +def convert_to_image_config(grc, grc_config_path): +    """ +    Converts Gnuradio Companion grc into image configuration. +    :param grc: +    :return: image configuration as it would be returned by image_config(args) +    """ +    grc_blocks = read_grc_block_configs(grc_config_path) +    #filter all blocks that have no block representation +    all = {item["name"]: item for item in grc["blocks"]} +    seps = {item["name"]: item for item in grc["blocks"] if item["parameters"]["type"] == 'sep'} +    blocks = {item["name"]: item for item in grc["blocks"] if item["parameters"]["type"] == 'block'} +    device = [item for item in grc["blocks"] if item["parameters"]["type"] == 'device'] +    if len(device) == 1: +        device = device[0] +    else: +        logging.error("More than one or no device found in grc file") +        return None + +    result = { +        "schema": "rfnoc_imagebuilder", +        "copyright": "Ettus Research, A National Instruments Brand", +        "license": "SPDX-License-Identifier: LGPL-3.0-or-later", +        "version": 1.0, +        "rfnoc_version": 1.0} +    # for param in [item for item in grc["blocks"] if item["id"] == "parameter"]: +    #     result[param["name"]] = { +    #         "str": lambda value: str, +    #         "": lambda value: str, +    #         "complex": str, +    #         "intx": int, +    #         "long": int, +    #     }[param["parameters"]["type"]](param["parameters"]["value"]) + +    result["stream_endpoints"] = {} +    for sep in seps.values(): +        result["stream_endpoints"][sep["name"]] = { "ctrl": bool(sep["parameters"]["ctrl"]), +                                                    "data": bool(sep["parameters"]["data"]), +                                                    "buff_size": int(sep["parameters"]["buff_size"]) } + +    result["noc_blocks"] = {} +    for block in blocks.values(): +        result["noc_blocks"][block["name"]] = { "block_desc": block["parameters"]["desc"] } + +    device_clocks = {port["id"]: port for port in grc_blocks[device['id']]["outputs"] if port["dtype"] == "message"} + +    for connection in grc["connections"]: +        if connection[0] == device["name"]: +            connection[0] = "_device_" +        if connection[2] == device["name"]: +            connection[2] = "_device_" +    device["name"] = "_device_" + +    (clk_connections, connections) = split( +        grc["connections"], lambda con: +        con[0] == device["name"] and con[1] in device_clocks) + +    result["connections"] = [] +    for connection in connections: +        result["connections"].append( +            {"srcblk":  connection[0], +             "srcport": connection[1], +             "dstblk":  connection[2], +             "dstport": connection[3]} +        ) + +    result["clk_domains"] = [] +    for connection in clk_connections: +        result["clk_domains"].append( +            {"srcblk":  connection[0], +             "srcport": connection[1], +             "dstblk":  connection[2], +             "dstport": connection[3]} +        ) + +    return result + + +def collect_module_paths(config_path, include_paths): +    """ +    Create a list of directories that contain noc block configuration files. +    :param config_path: root path holding configuration files +    :return: list of noc block directories +    """ +    # rfnoc blocks +    result = [os.path.join(config_path, 'rfnoc', 'blocks')] + \ +            [os.path.join(x, 'blocks') for x in include_paths] +    return result + + +def read_block_descriptions(signatures, *paths): +    """ +    Recursive search all pathes for block definitions. +    :param signatures: signature passed to IOConfig initialization +    :param paths: paths to be searched +    :return: dictionary of noc blocks. Key is filename of the block, value +             is an IOConfig object +    """ +    blocks = OrderedDict() +    for path in paths: +        for root, dirs, files, in os.walk(path): +            for filename in files: +                if re.match(r".*\.yml$", filename): +                    with open(os.path.join(root, filename)) as stream: +                        block = ordered_load(stream) +                        if "schema" in block and \ +                                block["schema"] == "rfnoc_modtool_args": +                            logging.info("Adding block description from " +                                         "%s (%s).", filename, os.path.normpath(root)) +                            blocks[filename] = IOConfig(block, signatures) +            for dirname in dirs: +                blocks.update(read_block_descriptions( +                    os.path.join(root, dirname))) +    return blocks + + +def write_edges(config, destination): +    """ +    Write edges description files. The file is a simple text file. Each line +    contains 8 hexadecimal digits. +    First line is the number of following entries. +    Starting with the second line each line describes a port to port connection +    The 32 bit value has 16 bit for each node where the node is represented by +    10 bit for the block number and 6 bit for the port number. +    :param config: ImageBuilderConfig derived from script parameter +    :param destination: folder to write the file (next to device top level files +    :return: None +    """ +    logging.info("Writing static routing table to %s", destination) +    with open(destination, "w") as stream: +        stream.write("%08X\n" % len(config.block_con)) +        for connection in config.block_con: +            if connection["srcblk"] in config.stream_endpoints: +                sep = config.stream_endpoints[connection["srcblk"]] +                index_match = re.match(r"out(\d)", connection["srcport"]) +                if not index_match: +                    logging.error("Port %s is invalid on endpoint %s", +                                  connection["srcport"], connection["srcblk"]) +                port_index = int(index_match.group(1)) +                # Verify index < num_data_o +                if port_index >= sep["num_data_o"]: +                    logging.error("Port %s exceeds num_data_o for endpoint %s", +                                  connection["srcport"], connection["srcblk"]) +                src = (sep["index"], port_index) +            else: +                key = (connection["srcblk"], connection["srcport"], "output") +                src = (config.noc_blocks[connection["srcblk"]]["index"], +                       config.block_ports[key]["index"]) +            if connection["dstblk"] in config.stream_endpoints: +                sep = config.stream_endpoints[connection["dstblk"]] +                index_match = re.match(r"in(\d)", connection["dstport"]) +                if not index_match: +                    logging.error("Port %s is invalid on endpoint %s", +                                  connection["dstport"], connection["dstblk"]) +                # Verify index < num_data_i +                port_index = int(index_match.group(1)) +                if port_index >= sep["num_data_i"]: +                    logging.error("Port %s exceeds num_data_i for endpoint %s", +                                  connection["dstport"], connection["dstblk"]) +                dst = (sep["index"], port_index) +            else: +                key = (connection["dstblk"], connection["dstport"], "input") +                dst = (config.noc_blocks[connection["dstblk"]]["index"], +                       config.block_ports[key]["index"]) +            logging.debug("%s-%s (%d,%d) => %s-%s (%d,%d)", +                          connection["srcblk"], connection["srcport"], +                          src[0], src[1], +                          connection["dstblk"], connection["dstport"], +                          dst[0], dst[1]) +            stream.write("%08x\n" % +                         ((((src[0] << 6) | src[1]) << 16) | +                          ((dst[0] << 6) | dst[1]))) + + +def write_verilog(config, destination, source, source_hash): +    """ +    Generates rfnoc_image_core.v file for the device. + +    Mako templates from local template folder are used to generate the image +    core file. The template engine does not do any computation on the script +    parameter. Instead all necessary dependencies are resolved in this script +    to enforce early failure which is easier to track than errors in the +    template engine. +    :param config: ImageBuilderConfig derived from script parameter +    :param destination: Filepath to write to +    :return: None +    """ +    template_dir = os.path.join(os.path.dirname(__file__), "templates") +    lookup = mako.lookup.TemplateLookup(directories=[template_dir]) +    tpl_filename = os.path.join(template_dir, "rfnoc_image_core.v.mako") +    tpl = mako.template.Template( +        filename=tpl_filename, +        lookup=lookup, +        strict_undefined=True) + +    try: +        block = tpl.render(**{ +            "config": config, +            "source": source, +            "source_hash": source_hash, +            }) +    except: +        print(exceptions.text_error_template().render()) +        sys.exit(1) + +    logging.info("Writing image core to %s", destination) +    with open(destination, "w") as image_core_file: +        image_core_file.write(block) + + +def write_build_env(): +    """ +    # TODO update Makefile entries according to used blocks +    :return: +    """ + + +def build(fpga_path, device, image_core_path, edge_file, **args): +    """ +    Call FPGA toolchain to actually build the image + +    :param fpga_path: A path that holds the FPGA IP sources. +    :param device: The device to build for. +    :param **args: Additional options +                   target: The target to build (leave empty for default). +                   clean_all: passed to Makefile +                   GUI: passed to Makefile +                   source: The source of the build (YAML or GRC file path) +                   include_paths: List of paths to OOT modules +                   extra_makefile_srcs: An additional list of paths to modules +                   that don't follow the OOT module layout. These paths must +                   point directly to a Makefile.srcs file. +    :return: exit value of build process +    """ +    ret_val = 0 +    cwd = os.path.dirname(__file__) +    build_dir = os.path.join(get_top_path(fpga_path), target_dir(device)) +    if not os.path.isdir(build_dir): +        logging.error("Not a valid directory: %s", build_dir) +        return 1 +    makefile_src_paths = [ +        os.path.join( +            os.path.abspath(os.path.normpath(x)), +            os.path.join('fpga', 'Makefile.srcs')) +        for x in args.get("include_paths", []) +    ] + args.get("extra_makefile_srcs", []) +    logging.debug("Temporarily changing working directory to %s", build_dir) +    os.chdir(build_dir) +    make_cmd = ". ./setupenv.sh " +    if "clean_all" in args and args["clean_all"]: +        make_cmd = make_cmd + "&& make cleanall " +    target = args["target"] if "target" in args else "" +    make_cmd = make_cmd + "&& make " + default_target(device, target) +    make_cmd += " IMAGE_CORE={} EDGE_FILE={}".format(image_core_path, +                                                     edge_file) +    if makefile_src_paths: +        make_cmd += " RFNOC_OOT_MAKEFILE_SRCS=" + "\\ ".join(makefile_src_paths) +    if "GUI" in args and args["GUI"]: +        make_cmd = make_cmd + " GUI=1" +    logging.info("Launching build with the following settings:") +    logging.info(" * Build Directory: %s", build_dir) +    logging.info(" * Target: %s", target) +    logging.info(" * Image Core File: %s", image_core_path) +    logging.info(" * Edge Table File: %s", edge_file) +    # Wrap it into a bash call: +    make_cmd = '{bash} -c "{cmd}"'.format(bash=BASH_EXECUTABLE, cmd=make_cmd) +    logging.debug("Executing the following command: %s", make_cmd) +    ret_val = os.system(make_cmd) +    os.chdir(cwd) +    return ret_val + + +def target_dir(device): +    """ +    Target directory derived from chosen device +    :param device: device to build for +    :return: target directory (relative path) +    """ +    if not device.lower() in DEVICE_DIR_MAP: +        logging.error("Unsupported device %s. Supported devices are %s", +                      device, DEVICE_DIR_MAP.keys()) +        sys.exit(1) +    return DEVICE_DIR_MAP[device.lower()] + +def default_target(device, target): +    """ +    If no target specified, selects the default building target based on the +    targeted device +    """ +    if target is None: +        return DEVICE_DEFAULTTARGET_MAP.get(device.lower()) +    return target + +def get_top_path(fpga_root): +    """ +    returns the path where FPGA top level sources reside +    """ +    return os.path.join(fpga_root, USRP3_TOP_DIR) + +def get_core_config_path(config_path): +    """ +    returns the path where core configuration files are stored +    """ +    return os.path.join(config_path, RFNOC_CORE_DIR) + +def generate_image_core_path(output_path, source): +    """ +    Creates the path where the image core file gets to be stored. + +    output_path: If not None, this is returned +    source: Otherwise, this path is returned with a .v argument +    """ +    if output_path is not None: +        return output_path +    source = os.path.splitext(os.path.normpath(source))[0] +    return source + '.v' + +def generate_edge_file_path(output_path, device, source): +    """ +    Creates a valid path for the edge file to get stored. +    """ +    if output_path is not None: +        return output_path +    edge_path = os.path.split(os.path.abspath(os.path.normpath(source)))[0] +    return os.path.join(edge_path, "{}_static_router.hex".format(device)) + + +def build_image(config, fpga_path, config_path, device, **args): +    """ +    Generate image dependent Verilog code and trigger Xilinx toolchain, if +    requested. + +    :param config: A dictionary containing the image configuration options. +                   This must obey the rfnoc_imagebuilder_args schema. +    :param fpga_path: A path that holds the FPGA IP sources. +    :param device: The device to build for. +    :param **args: Additional options including +                   target: The target to build (leave empty for default). +                   generate_only: Do not build the code after generation. +                   clean_all: passed to Makefile +                   GUI: passed to Makefile +                   include_paths: Paths to additional blocks +    :return: Exit result of build process or 0 if generate-only is given. +    """ +    logging.info("Selected device %s", device) +    image_core_path = \ +        generate_image_core_path(args.get('output_path'), args.get('source')) +    edge_file = \ +        generate_edge_file_path( +            args.get('router_hex_path'), device, args.get('source')) + +    logging.debug("Image core output file: %s", image_core_path) +    logging.debug("Edge output file: %s", edge_file) + +    core_config_path = get_core_config_path(config_path) +    signatures_conf = io_signatures(core_config_path) +    device_conf = IOConfig(device_config(core_config_path, device), +                           signatures_conf) + +    block_paths = collect_module_paths(config_path, args.get('include_paths', [])) +    logging.debug("Looking for block descriptors in:") +    for path in block_paths: +        logging.debug("    %s", os.path.normpath(path)) +    blocks = read_block_descriptions(signatures_conf, *block_paths) + +    builder_conf = ImageBuilderConfig(config, blocks, device_conf) + +    write_edges(builder_conf, edge_file) +    write_verilog( +        builder_conf, +        image_core_path, +        source=args.get('source'), +        source_hash=args.get('source_hash')) +    write_build_env() + +    if "generate_only" in args and args["generate_only"]: +        logging.info("Skip build (generate only option given)") +        return 0 + +    # Check if the YAML files require additional Makefile.srcs +    extra_makefile_srcs = set() +    for block_info in builder_conf.noc_blocks.values(): +        block_desc = blocks[block_info['block_desc']] +        if hasattr(block_desc, 'makefile_srcs'): +            extra_path = mako.template.Template(block_desc.makefile_srcs).render(**{ +                "fpga_lib_dir": os.path.join(fpga_path, USRP3_LIB_RFNOC_DIR), +            }) +            if extra_path not in extra_makefile_srcs: +                logging.debug("Adding additional Makefile.srcs path: %s", extra_path) +                extra_makefile_srcs.add(extra_path) +    args['extra_makefile_srcs'] = list(extra_makefile_srcs) +    return build(fpga_path, device, image_core_path, edge_file, **args) diff --git a/host/python/uhd/imgbuilder/templates/modules/chdr_xb_sep_transport.v.mako b/host/python/uhd/imgbuilder/templates/modules/chdr_xb_sep_transport.v.mako new file mode 100644 index 000000000..0862a1ac1 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/chdr_xb_sep_transport.v.mako @@ -0,0 +1,23 @@ +<%page args="seps, transports"/>\ +\ +<% +  import re +  sep2xb = "" +  xb2sep = "" +  for sep in reversed(list(seps.keys())): +    sep2xb += "%s_to_xb_wire, " % sep +    xb2sep += "xb_to_%s_wire, " % sep +  for transport in reversed(transports): +    sep2xb += "s_%s_wire, " % transport["name"] +    xb2sep += "m_%s_wire, " % transport["name"] +  sep2xb = sep2xb[:-2] +  xb2sep = xb2sep[:-2] +%>\ +    .s_axis_tdata   ({${re.sub("wire", "tdata", sep2xb)}}), +    .s_axis_tlast   ({${re.sub("wire", "tlast", sep2xb)}}), +    .s_axis_tvalid  ({${re.sub("wire", "tvalid", sep2xb)}}), +    .s_axis_tready  ({${re.sub("wire", "tready", sep2xb)}}), +    .m_axis_tdata   ({${re.sub("wire", "tdata", xb2sep)}}), +    .m_axis_tlast   ({${re.sub("wire", "tlast", xb2sep)}}), +    .m_axis_tvalid  ({${re.sub("wire", "tvalid", xb2sep)}}), +    .m_axis_tready  ({${re.sub("wire", "tready", xb2sep)}}), diff --git a/host/python/uhd/imgbuilder/templates/modules/connect_clk_domains.v.mako b/host/python/uhd/imgbuilder/templates/modules/connect_clk_domains.v.mako new file mode 100644 index 000000000..df055645c --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/connect_clk_domains.v.mako @@ -0,0 +1,12 @@ +<%page args="connections, clocks"/>\ +\ +%for connection in connections: +<% +  src_name = connection["srcblk"] # Should always be "_device_" +  src = clocks[(src_name, connection["srcport"])] +  dst_name = connection["dstblk"] +  dst = clocks[(dst_name, connection["dstport"])] +%>\ +  assign ${dst_name}_${dst["name"]}_clk = ${src["name"]}_clk; +%endfor + diff --git a/host/python/uhd/imgbuilder/templates/modules/connect_io_ports.v.mako b/host/python/uhd/imgbuilder/templates/modules/connect_io_ports.v.mako new file mode 100644 index 000000000..c37697d88 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/connect_io_ports.v.mako @@ -0,0 +1,20 @@ +<%page args="connections, io_ports, names"/>\ +\ +%for connection in connections: +<% +  src_name = connection["srcblk"] +  src = io_ports[(src_name, connection["srcport"], names[0])] +  dst_name = connection["dstblk"] +  dst = io_ports[(dst_name, connection["dstport"], names[1])] +%>\ +  %for src_wire, dst_wire in zip(src["wires"], dst["wires"]): +<% +  swire = src_wire["name"] if src_name == "_device_" else "%s_%s" % (src_name, src_wire["name"]) +  dwire = dst_wire["name"] if dst_name == "_device_" else "%s_%s" % (dst_name, dst_wire["name"]) +  if src_wire["direction"] == "output": +    swire, dwire = dwire, swire +%>\ +  assign ${dwire} = ${swire}; +  %endfor + +%endfor diff --git a/host/python/uhd/imgbuilder/templates/modules/ctrl_crossbar.v.mako b/host/python/uhd/imgbuilder/templates/modules/ctrl_crossbar.v.mako new file mode 100644 index 000000000..5872e270e --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/ctrl_crossbar.v.mako @@ -0,0 +1,39 @@ +<%page args="seps, blocks"/>\ +\ +<% +  import re +  axisstr = "" +  for block in reversed(list(blocks.keys())): +    axisstr += "{0}_%s_ctrl_{1}, " % block +  for sep in reversed(list(seps.keys())): +    axisstr += "{0}_%s_ctrl_{1}, " % sep +  axisstr += "{0}_core_ctrl_{1}" +%>\ +%for block in blocks: +  wire [31:0]       m_${block}_ctrl_tdata ,   s_${block}_ctrl_tdata ; +  wire              m_${block}_ctrl_tlast ,   s_${block}_ctrl_tlast ; +  wire              m_${block}_ctrl_tvalid,   s_${block}_ctrl_tvalid; +  wire              m_${block}_ctrl_tready,   s_${block}_ctrl_tready; +%endfor + +  axis_ctrl_crossbar_nxn #( +    .WIDTH            (32), +    .NPORTS           (${len(seps) + len(blocks) + 1}), +    .TOPOLOGY         ("TORUS"), +    .INGRESS_BUFF_SIZE(5), +    .ROUTER_BUFF_SIZE (5), +    .ROUTING_ALLOC    ("WORMHOLE"), +    .SWITCH_ALLOC     ("PRIO") +  ) ctrl_xb_i ( +    .clk              (rfnoc_ctrl_clk), +    .reset            (rfnoc_ctrl_rst), +    .s_axis_tdata     ({${axisstr.format("m", "tdata ")}}), +    .s_axis_tvalid    ({${axisstr.format("m", "tvalid")}}), +    .s_axis_tlast     ({${axisstr.format("m", "tlast ")}}), +    .s_axis_tready    ({${axisstr.format("m", "tready")}}), +    .m_axis_tdata     ({${axisstr.format("s", "tdata ")}}), +    .m_axis_tvalid    ({${axisstr.format("s", "tvalid")}}), +    .m_axis_tlast     ({${axisstr.format("s", "tlast ")}}), +    .m_axis_tready    ({${axisstr.format("s", "tready")}}), +    .deadlock_detected() +  ); diff --git a/host/python/uhd/imgbuilder/templates/modules/device_io_ports.v.mako b/host/python/uhd/imgbuilder/templates/modules/device_io_ports.v.mako new file mode 100644 index 000000000..abfb86c98 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/device_io_ports.v.mako @@ -0,0 +1,9 @@ +<%page args="io_ports"/>\ +<%import six%>\ +//// IO ports ////////////////////////////////// +%for name, io_port in six.iteritems(io_ports): +//  ${name} +  %for wire in io_port["wires"]: +  ${wire["direction"]} wire [${"%3d" % wire["width"]}-1:0] ${wire["name"]}, +  %endfor +%endfor diff --git a/host/python/uhd/imgbuilder/templates/modules/device_transport.v.mako b/host/python/uhd/imgbuilder/templates/modules/device_transport.v.mako new file mode 100644 index 000000000..3d752ce13 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/device_transport.v.mako @@ -0,0 +1,13 @@ +<%page args="transports"/>\ +\ +%for i, transport in enumerate(transports): +  // Transport ${i} (${transport["name"]} ${transport["type"]}) +  input  wire [${transport["width"]}-1:0]  s_${transport["name"]}_tdata, +  input  wire         s_${transport["name"]}_tlast, +  input  wire         s_${transport["name"]}_tvalid, +  output wire         s_${transport["name"]}_tready, +  output wire [${transport["width"]}-1:0]  m_${transport["name"]}_tdata, +  output wire         m_${transport["name"]}_tlast, +  output wire         m_${transport["name"]}_tvalid, +  input  wire         m_${transport["name"]}_tready${"," if i < len(transports) - 1 else ""} +%endfor diff --git a/host/python/uhd/imgbuilder/templates/modules/drive_unused_ports.v.mako b/host/python/uhd/imgbuilder/templates/modules/drive_unused_ports.v.mako new file mode 100644 index 000000000..dfa1e7eb6 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/drive_unused_ports.v.mako @@ -0,0 +1,21 @@ +<%page args="connections, block_ports"/>\ +<% +  sources = [] +  destinations = [] +  for connection in connections: +    sources.append((connection["srcblk"], connection["srcport"])) +    destinations.append((connection["dstblk"], connection["dstport"])) +%>\ +%for (block_name, port_name, direction) in block_ports: +  %if direction == "input": +    %if not (block_name, port_name) in destinations: +  assign s_${block_name}_${port_name}_tdata  = {CHDR_W{1'b0}}; +  assign s_${block_name}_${port_name}_tlast  = 1'b0; +  assign s_${block_name}_${port_name}_tvalid = 1'b0; +    %endif +  %elif direction == "output": +    %if not (block_name, port_name) in sources: +  assign m_${block_name}_${port_name}_tready = 1'b1; +    %endif +  %endif +%endfor diff --git a/host/python/uhd/imgbuilder/templates/modules/rfnoc_block.v.mako b/host/python/uhd/imgbuilder/templates/modules/rfnoc_block.v.mako new file mode 100644 index 000000000..ee4a811a8 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/rfnoc_block.v.mako @@ -0,0 +1,91 @@ +<%page args="block_id, block_number, block_name, block, block_params, block_ports"/>\ +\ +<% +  import re +  import six + +  # Create two strings, one for the input and one for the output, that each +  # contains all the signal names to be connected to the input or output  +  # AXIS-CHDR ports of this block. +  axis_inputs = "" +  axis_outputs = "" +  for port_desc in block_ports: +    if port_desc[0] == block_name: +      port_name = port_desc[1] +      if port_desc[2] == "input": +        axis_inputs = "{0}_%s_%s_{1}, " % (block_name, port_name) + axis_inputs +      elif port_desc[2] == "output": +        axis_outputs = "{0}_%s_%s_{1}, " % (block_name, port_name) + axis_outputs +  axis_inputs = axis_inputs[:-2] +  axis_outputs = axis_outputs[:-2] +%>\ + +  // ---------------------------------------------------- +  // ${block_name} +  // ---------------------------------------------------- +%for clock in block.clocks: +  %if not clock["name"] in ["rfnoc_chdr", "rfnoc_ctrl"]: +  wire              ${block_name}_${clock["name"]}_clk; +  %endif +%endfor +  wire [CHDR_W-1:0] ${axis_inputs.format("s", "tdata ")}; +  wire              ${axis_inputs.format("s", "tlast ")}; +  wire              ${axis_inputs.format("s", "tvalid")}; +  wire              ${axis_inputs.format("s", "tready")}; +  wire [CHDR_W-1:0] ${axis_outputs.format("m", "tdata ")}; +  wire              ${axis_outputs.format("m", "tlast ")}; +  wire              ${axis_outputs.format("m", "tvalid")}; +  wire              ${axis_outputs.format("m", "tready")}; + +%if hasattr(block, "io_ports"): +  %for name, io_port in six.iteritems(block.io_ports): +  //  ${name} +    %for wire in io_port["wires"]: +  wire [${"%3d" % wire["width"]}-1:0] ${block_name}_${wire["name"]}; +    %endfor +  %endfor +%endif + +  rfnoc_block_${block.module_name} #( +    .THIS_PORTID(${block_id}), +    .CHDR_W(CHDR_W), +%for name, value in six.iteritems(block_params): +    .${name}(${value}), +%endfor +    .MTU(MTU) +  ) b_${block_name}_${block_number} ( +    .rfnoc_chdr_clk     (rfnoc_chdr_clk), +    .rfnoc_ctrl_clk     (rfnoc_ctrl_clk), +%for clock in block.clocks: +  %if not clock["name"] in ["rfnoc_chdr", "rfnoc_ctrl"]: +    .${clock["name"]}_clk(${block_name}_${clock["name"]}_clk), +  %endif +%endfor +    .rfnoc_core_config  (rfnoc_core_config[512*${block_number + 1}-1:512*${block_number}]), +    .rfnoc_core_status  (rfnoc_core_status[512*${block_number + 1}-1:512*${block_number}]), + +%if hasattr(block, "io_ports"): +  %for name, io_port in six.iteritems(block.io_ports): +    %for wire in io_port["wires"]: +    .${wire["name"]}(${block_name}_${wire["name"]}), +    %endfor +  %endfor +%endif + +    .s_rfnoc_chdr_tdata ({${axis_inputs.format("s", "tdata ")}}), +    .s_rfnoc_chdr_tlast ({${axis_inputs.format("s", "tlast ")}}), +    .s_rfnoc_chdr_tvalid({${axis_inputs.format("s", "tvalid")}}), +    .s_rfnoc_chdr_tready({${axis_inputs.format("s", "tready")}}), +    .m_rfnoc_chdr_tdata ({${axis_outputs.format("m", "tdata ")}}), +    .m_rfnoc_chdr_tlast ({${axis_outputs.format("m", "tlast ")}}), +    .m_rfnoc_chdr_tvalid({${axis_outputs.format("m", "tvalid")}}), +    .m_rfnoc_chdr_tready({${axis_outputs.format("m", "tready")}}), +    .s_rfnoc_ctrl_tdata (s_${block_name}_ctrl_tdata ), +    .s_rfnoc_ctrl_tlast (s_${block_name}_ctrl_tlast ), +    .s_rfnoc_ctrl_tvalid(s_${block_name}_ctrl_tvalid), +    .s_rfnoc_ctrl_tready(s_${block_name}_ctrl_tready), +    .m_rfnoc_ctrl_tdata (m_${block_name}_ctrl_tdata ), +    .m_rfnoc_ctrl_tlast (m_${block_name}_ctrl_tlast ), +    .m_rfnoc_ctrl_tvalid(m_${block_name}_ctrl_tvalid), +    .m_rfnoc_ctrl_tready(m_${block_name}_ctrl_tready) +  ); diff --git a/host/python/uhd/imgbuilder/templates/modules/sep_xb_wires.v.mako b/host/python/uhd/imgbuilder/templates/modules/sep_xb_wires.v.mako new file mode 100644 index 000000000..4aa7d56bb --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/sep_xb_wires.v.mako @@ -0,0 +1,12 @@ +<%page args="seps"/>\ +\ +%for sep in seps: +  wire [CHDR_W-1:0] xb_to_${sep}_tdata ; +  wire              xb_to_${sep}_tlast ; +  wire              xb_to_${sep}_tvalid; +  wire              xb_to_${sep}_tready; +  wire [CHDR_W-1:0] ${sep}_to_xb_tdata ; +  wire              ${sep}_to_xb_tlast ; +  wire              ${sep}_to_xb_tvalid; +  wire              ${sep}_to_xb_tready; +%endfor diff --git a/host/python/uhd/imgbuilder/templates/modules/static_router.v.mako b/host/python/uhd/imgbuilder/templates/modules/static_router.v.mako new file mode 100644 index 000000000..3649c278b --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/static_router.v.mako @@ -0,0 +1,15 @@ +<%page args="connections"/>\ +\ +%for connection in connections: +<% +  srcblk = connection["srcblk"] +  dstblk = connection["dstblk"] +  srcport = "in" if connection["srcport"] == None else connection["srcport"] +  dstport = "out" if connection["dstport"] == None else connection["dstport"] +%>\ +  assign s_${dstblk}_${dstport}_tdata = m_${srcblk}_${srcport}_tdata ; +  assign s_${dstblk}_${dstport}_tlast = m_${srcblk}_${srcport}_tlast ; +  assign s_${dstblk}_${dstport}_tvalid = m_${srcblk}_${srcport}_tvalid; +  assign m_${srcblk}_${srcport}_tready = s_${dstblk}_${dstport}_tready; + +%endfor
\ No newline at end of file diff --git a/host/python/uhd/imgbuilder/templates/modules/stream_endpoints.v.mako b/host/python/uhd/imgbuilder/templates/modules/stream_endpoints.v.mako new file mode 100644 index 000000000..f8ecccb77 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/modules/stream_endpoints.v.mako @@ -0,0 +1,92 @@ +<%page args="seps"/>\ +<% +  import math +  import re +  import six + +  axis_inputs = {} +  axis_outputs = {} +  for i, sep in enumerate(seps): +    inputs = "" +    outputs = "" +    for data_i in range(0,seps[sep]["num_data_i"]): +      inputs = "s_{0}_in%d_{1}, " % (data_i) + inputs +    axis_inputs[sep] = inputs[:-2] +    for data_o in range(0,seps[sep]["num_data_o"]): +      outputs = "m_{0}_out%d_{1}, " % (data_o) + outputs +    axis_outputs[sep] = outputs[:-2] +%>\ +\ +%for i, sep in enumerate(seps): +<% +# If buff_size == 0, then we assume that we will never transmit through this SEP +buff_size = seps[sep]["buff_size"] +if buff_size > 0: +    buff_size = int(math.ceil(math.log(buff_size, 2))) +    # FIXME MTU assumed to be 10 here -- forcing to at least accommodate 2 pkts +    buff_size = max(buff_size, 10+1) +else: +    buff_size = 5 +%>\ +  wire [CHDR_W-1:0] ${axis_outputs[sep].format(sep,"tdata")}; +  wire              ${axis_outputs[sep].format(sep,"tlast")}; +  wire              ${axis_outputs[sep].format(sep,"tvalid")}; +  wire              ${axis_outputs[sep].format(sep,"tready")}; +  wire [CHDR_W-1:0] ${axis_inputs[sep].format(sep,"tdata")}; +  wire              ${axis_inputs[sep].format(sep,"tlast")}; +  wire              ${axis_inputs[sep].format(sep,"tvalid")}; +  wire              ${axis_inputs[sep].format(sep,"tready")}; +  wire [31:0]       m_${sep}_ctrl_tdata , s_${sep}_ctrl_tdata ; +  wire              m_${sep}_ctrl_tlast , s_${sep}_ctrl_tlast ; +  wire              m_${sep}_ctrl_tvalid, s_${sep}_ctrl_tvalid; +  wire              m_${sep}_ctrl_tready, s_${sep}_ctrl_tready; + +  chdr_stream_endpoint #( +    .PROTOVER           (PROTOVER), +    .CHDR_W             (CHDR_W), +    .AXIS_CTRL_EN       (${int(seps[sep]["ctrl"])}), +    .AXIS_DATA_EN       (${int(seps[sep]["data"])}), +    .NUM_DATA_I         (${int(seps[sep]["num_data_i"])}), +    .NUM_DATA_O         (${int(seps[sep]["num_data_o"])}), +    .INST_NUM           (${i}), +    .CTRL_XBAR_PORT     (${i+1}), +    .INGRESS_BUFF_SIZE  (${buff_size}), +    .MTU                (MTU), +    .REPORT_STRM_ERRS   (1) +  ) ${sep}_i ( +    .rfnoc_chdr_clk     (rfnoc_chdr_clk    ), +    .rfnoc_chdr_rst     (rfnoc_chdr_rst    ), +    .rfnoc_ctrl_clk     (rfnoc_ctrl_clk    ), +    .rfnoc_ctrl_rst     (rfnoc_ctrl_rst    ), +    .device_id          (device_id         ), +    .s_axis_chdr_tdata  (xb_to_${sep}_tdata  ), +    .s_axis_chdr_tlast  (xb_to_${sep}_tlast  ), +    .s_axis_chdr_tvalid (xb_to_${sep}_tvalid ), +    .s_axis_chdr_tready (xb_to_${sep}_tready ), +    .m_axis_chdr_tdata  (${sep}_to_xb_tdata  ), +    .m_axis_chdr_tlast  (${sep}_to_xb_tlast  ), +    .m_axis_chdr_tvalid (${sep}_to_xb_tvalid ), +    .m_axis_chdr_tready (${sep}_to_xb_tready ), +    .s_axis_data_tdata  ({${axis_inputs[sep].format(sep,"tdata")}}), +    .s_axis_data_tlast  ({${axis_inputs[sep].format(sep,"tlast")}}), +    .s_axis_data_tvalid ({${axis_inputs[sep].format(sep,"tvalid")}}), +    .s_axis_data_tready ({${axis_inputs[sep].format(sep,"tready")}}), +    .m_axis_data_tdata  ({${axis_outputs[sep].format(sep,"tdata")}}), +    .m_axis_data_tlast  ({${axis_outputs[sep].format(sep,"tlast")}}), +    .m_axis_data_tvalid ({${axis_outputs[sep].format(sep,"tvalid")}}), +    .m_axis_data_tready ({${axis_outputs[sep].format(sep,"tready")}}), +    .s_axis_ctrl_tdata  (s_${sep}_ctrl_tdata ), +    .s_axis_ctrl_tlast  (s_${sep}_ctrl_tlast ), +    .s_axis_ctrl_tvalid (s_${sep}_ctrl_tvalid), +    .s_axis_ctrl_tready (s_${sep}_ctrl_tready), +    .m_axis_ctrl_tdata  (m_${sep}_ctrl_tdata ), +    .m_axis_ctrl_tlast  (m_${sep}_ctrl_tlast ), +    .m_axis_ctrl_tvalid (m_${sep}_ctrl_tvalid), +    .m_axis_ctrl_tready (m_${sep}_ctrl_tready), +    .strm_seq_err_stb   (                  ), +    .strm_data_err_stb  (                  ), +    .strm_route_err_stb (                  ), +    .signal_data_err    (1'b0              ) +  ); + +%endfor diff --git a/host/python/uhd/imgbuilder/templates/rfnoc_image_core.v.mako b/host/python/uhd/imgbuilder/templates/rfnoc_image_core.v.mako new file mode 100644 index 000000000..a18ff53a1 --- /dev/null +++ b/host/python/uhd/imgbuilder/templates/rfnoc_image_core.v.mako @@ -0,0 +1,159 @@ +<% +    import datetime +%>// +// Copyright ${datetime.datetime.now().year} ${config.copyright} +// +// ${config.license} +// + +// Module: rfnoc_image_core (for ${config.device.type}) +// This file was autogenerated by UHD's image builder tool (rfnoc_image_builder) +// Re-running that tool will overwrite this file! +// File generated on: ${datetime.datetime.now().isoformat()} +% if source: +// Source: ${source} +% endif +% if source_hash: +// Source SHA256: ${source_hash} +% endif + +module rfnoc_image_core #( +  parameter [15:0] PROTOVER = {8'd1, 8'd0} +)( +  // Clocks +  input  wire         chdr_aclk, +  input  wire         ctrl_aclk, +  input  wire         core_arst, +%for clock in config.device.clocks: +  input  wire         ${clock["name"]}_clk, +%endfor +  // Basic +  input  wire [15:0]  device_id, +<%include file="/modules/device_io_ports.v.mako" args="io_ports=config.device.io_ports"/>\ +<%include file="/modules/device_transport.v.mako" args="transports=config.device.transports"/>\ +); + +  localparam CHDR_W = 64; +  localparam MTU    = 10; +  localparam EDGE_TBL_FILE = `"`RFNOC_EDGE_TBL_FILE`"; + +  wire rfnoc_chdr_clk, rfnoc_chdr_rst; +  wire rfnoc_ctrl_clk, rfnoc_ctrl_rst; + +  // ---------------------------------------------------- +  // CHDR Crossbar +  // ---------------------------------------------------- +<%include file="/modules/sep_xb_wires.v.mako" args="seps=config.stream_endpoints"/>\ + +  chdr_crossbar_nxn #( +    .CHDR_W         (CHDR_W), +    .NPORTS         (${len(config.stream_endpoints) + len(config.device.transports)}), +    .DEFAULT_PORT   (0), +    .MTU            (MTU), +    .ROUTE_TBL_SIZE (6), +    .MUX_ALLOC      ("ROUND-ROBIN"), +    .OPTIMIZE       ("AREA"), +    .NPORTS_MGMT    (${len(config.device.transports)}), +    .EXT_RTCFG_PORT (0), +    .PROTOVER       (PROTOVER) +  ) chdr_xb_i ( +    .clk            (rfnoc_chdr_clk), +    .reset          (rfnoc_chdr_rst), +    .device_id      (device_id), +<%include file="/modules/chdr_xb_sep_transport.v.mako" args="seps=config.stream_endpoints, transports=config.device.transports"/>\ +    .ext_rtcfg_stb  (1'h0), +    .ext_rtcfg_addr (16'h0), +    .ext_rtcfg_data (32'h0), +    .ext_rtcfg_ack  () +  ); + +  // ---------------------------------------------------- +  // Stream Endpoints +  // ---------------------------------------------------- + +<%include file="/modules/stream_endpoints.v.mako" args="seps=config.stream_endpoints"/>\ + +<% +    from collections import OrderedDict +    ctrl_seps = OrderedDict((k, v) for k, v in config.stream_endpoints.items() if v.get('ctrl')) +%> +  // ---------------------------------------------------- +  // Control Crossbar +  // ---------------------------------------------------- + +  wire [31:0]       m_core_ctrl_tdata , s_core_ctrl_tdata ; +  wire              m_core_ctrl_tlast , s_core_ctrl_tlast ; +  wire              m_core_ctrl_tvalid, s_core_ctrl_tvalid; +  wire              m_core_ctrl_tready, s_core_ctrl_tready; +<%include file="/modules/ctrl_crossbar.v.mako" args="seps=ctrl_seps, blocks=config.noc_blocks"/>\ + +  // ---------------------------------------------------- +  // RFNoC Core Kernel +  // ---------------------------------------------------- +  wire [(512*${len(config.noc_blocks)})-1:0] rfnoc_core_config, rfnoc_core_status; + +  rfnoc_core_kernel #( +    .PROTOVER            (PROTOVER), +    .DEVICE_TYPE         (16'h${config.device.type_id}), +    .DEVICE_FAMILY       ("${config.device.family}"), +    .SAFE_START_CLKS     (0), +    .NUM_BLOCKS          (${len(config.noc_blocks)}), +    .NUM_STREAM_ENDPOINTS(${len(config.stream_endpoints)}), +    .NUM_ENDPOINTS_CTRL  (${len(ctrl_seps)}), +    .NUM_TRANSPORTS      (${len(config.device.transports)}), +    .NUM_EDGES           (${len(config.block_con)}), +    .CHDR_XBAR_PRESENT   (1), +    .EDGE_TBL_FILE       (EDGE_TBL_FILE) +  ) core_kernel_i ( +    .chdr_aclk          (chdr_aclk), +    .chdr_aclk_locked   (1'b1), +    .ctrl_aclk          (ctrl_aclk), +    .ctrl_aclk_locked   (1'b1), +    .core_arst          (core_arst), +    .core_chdr_clk      (rfnoc_chdr_clk), +    .core_chdr_rst      (rfnoc_chdr_rst), +    .core_ctrl_clk      (rfnoc_ctrl_clk), +    .core_ctrl_rst      (rfnoc_ctrl_rst), +    .s_axis_ctrl_tdata  (s_core_ctrl_tdata ), +    .s_axis_ctrl_tlast  (s_core_ctrl_tlast ), +    .s_axis_ctrl_tvalid (s_core_ctrl_tvalid), +    .s_axis_ctrl_tready (s_core_ctrl_tready), +    .m_axis_ctrl_tdata  (m_core_ctrl_tdata ), +    .m_axis_ctrl_tlast  (m_core_ctrl_tlast ), +    .m_axis_ctrl_tvalid (m_core_ctrl_tvalid), +    .m_axis_ctrl_tready (m_core_ctrl_tready), +    .device_id          (device_id), +    .rfnoc_core_config  (rfnoc_core_config), +    .rfnoc_core_status  (rfnoc_core_status) +  ); + +  // ---------------------------------------------------- +  // Blocks +  // ---------------------------------------------------- +%for i, name in enumerate(config.noc_blocks): +<%include file="/modules/rfnoc_block.v.mako" args="block_id=i + len(ctrl_seps) + 1, block_number=i, block_name=name, block=config.blocks[config.noc_blocks[name]['block_desc']], block_params=config.noc_blocks[name]['parameters'], block_ports=config.block_ports"/> +%endfor + +  // ---------------------------------------------------- +  // Static Router +  // ---------------------------------------------------- +<%include file="/modules/static_router.v.mako" args="connections=config.block_con"/>\ + +  // ---------------------------------------------------- +  // Unused Ports +  // ---------------------------------------------------- +<%include file="/modules/drive_unused_ports.v.mako" args="connections=config.block_con, block_ports=config.block_ports"/>\ + +  // ---------------------------------------------------- +  // Clock Domains +  // ---------------------------------------------------- +<%include file="/modules/connect_clk_domains.v.mako" args="connections=config.clk_domain_con, clocks=config.clocks"/>\ + +  // ---------------------------------------------------- +  // IO Port Connection +  // ---------------------------------------------------- +  // Master/Slave Connections: +<%include file="/modules/connect_io_ports.v.mako" args="connections=config.io_port_con_ms, io_ports=config.io_ports, names=('master', 'slave')"/>\ +  // Broadcaster/Listener Connections: +<%include file="/modules/connect_io_ports.v.mako" args="connections=config.io_port_con_bl, io_ports=config.io_ports, names=('broadcaster', 'listener')"/>\ +endmodule diff --git a/host/python/uhd/imgbuilder/yaml_utils.py b/host/python/uhd/imgbuilder/yaml_utils.py new file mode 100644 index 000000000..e64556334 --- /dev/null +++ b/host/python/uhd/imgbuilder/yaml_utils.py @@ -0,0 +1,107 @@ +""" +Copyright 2019 Ettus Research, A National Instrument Brand + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program.  If not, see <http://www.gnu.org/licenses/>. +""" + +import json +import logging +import os +import sys +from ruamel import yaml + +# Allow jsonschema import to fail. If not available no schema validation will +# be done (but warning will be printed for each skipped validation). +try: +    import jsonschema +except ImportError: +    logging.warning("Module jsonschema is not installed. Configuration files " +                    "will not be validated against their schema.") + + +def find_schema(schema_name, config_path): +    """ +    Recursive search for schema file. Only looks for a file with appropriate +    name without checking for content or accessibility. +    This check will be performed later when trying to load the schema. +    :param schema_name: name of schema file to search for +    :param config_path: root path to start search in +    :return: full path to schema file if found, None else +    """ +    for root, _, _ in os.walk(config_path): +        filename = os.path.join(root, schema_name) +        if os.path.isfile(filename): +            return filename +    return None + + +def validate_config(config, config_path): +    """ +    Try to validate config. + +    config contains a configuration loaded from a yaml file. config is assumed +    to be a dictionary which contains a key 'schema' which determines +    which schema to validate against. The schema (json formatted) needs to be +    located in config_path or any sub folder of it. +    If "jsonschema" module cannot be loaded validation is skipped and config +    is assumed to be valid. This way a configuration can also be loaded if +    "jsonschema" is not available. +    The method raises ValueError if no schema is defined in config or the +    schema defined in config cannot be found. The validation itself may throw +    a jsonschema.exceptions.ValidationError if config does not confirm to its +    schema. +    :param config: a dictionary to validate (loaded from yaml file). +    :param config_path: a path holding schema definitions +    """ +    if "jsonschema" not in sys.modules: +        logging.warning("Skip schema validation (missing module jsonschema).") +        return + +    if not "schema" in config: +        raise ValueError("Missing schema in configuration.") + +    schema_name = config["schema"] +    logging.debug("Validate against schema %s.", schema_name) + +    schema_file = find_schema('%s.json' % schema_name, config_path) +    if not schema_file: +        raise ValueError("Unknown schema %s." % schema_name) + +    logging.debug("Using schema file %s.", schema_file) + +    with open(schema_file) as stream: +        jsonschema.validate(instance=config, schema=json.load(stream)) +        logging.debug("Configuration successful validated.") + + +def load_config(config_file, config_path): +    """ +    Wrapper method to unify loading of configuration files. +    Beside loading the configuration (yaml file format) itself from config_file +    this method also validates the configuration against a schema. The root +    element of the configuration must load to a dictionary which contains a +    "schema" key. The value of schema points to a file named {value}.json which +    must be located in config_path or one of its sub folders. +    .. seealso:: validate_config. +    :param config_file: configuration to load +    :param config_path: root path of schema definition files +    :return: +    """ +    logging.debug("Load configuration %s.", config_file) +    with open(config_file) as stream: +        rt_yaml = yaml.YAML(typ='rt') +        config = rt_yaml.load(stream) +        logging.debug("Configuration successful loaded.") +        validate_config(config, config_path) +        return config  | 
