diff --git a/host/cmake/Modules/copy_rfnoc_newmod.py b/host/cmake/Modules/copy_rfnoc_newmod.py new file mode 100644 index 000000000..47c2da7bb --- /dev/null +++ b/host/cmake/Modules/copy_rfnoc_newmod.py @@ -0,0 +1,50 @@ +# /usr/bin/env python3 +"""Image Builder Helper: Create the template for RFNoC OOT modules. + +Copyright 2024 Ettus Research, a National Instruments Brand + +SPDX-License-Identifier: GPL-3.0-or-later +""" + +import argparse +import importlib +import os +import sys + +from ruamel.yaml import YAML + + +def parse_args(): + """Create argument parser and return args.""" + arg_parser = argparse.ArgumentParser(description="Create the template for RFNoC OOT modules.") + arg_parser.add_argument("--source", required=True, help="Source directory") + arg_parser.add_argument("--dest", required=True, help="Destination directory") + arg_parser.add_argument("--module-dir", required=True, help="Module directory") + return arg_parser.parse_args() + + +def main(): + """Do the copying.""" + args = parse_args() + # Load the step configuratio as a Python object + yaml = YAML(typ="safe", pure=True) + step_yml = os.path.join(os.path.dirname(os.path.realpath(__file__)), "create_newmod.yml") + with open(step_yml, "r", encoding="utf-8") as f: + cmd = yaml.load(f) + # Load the step executor from within the tree + sys.path.insert(0, os.path.normpath(args.module_dir)) + importlib.import_module("rfnoc_utils.log").init_logging(color_mode="off", log_level="info") + executor_vars = { + "SOURCE_DIR": args.source, + "DEST_DIR": args.dest, + } + # Now we can run the steps + executor = importlib.import_module("rfnoc_utils.step_executor").StepExecutor( + executor_vars, args, cmd + ) + executor.run(cmd["steps"]) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/host/cmake/Modules/create_newmod.yml b/host/cmake/Modules/create_newmod.yml new file mode 100644 index 000000000..11f87538d --- /dev/null +++ b/host/cmake/Modules/create_newmod.yml @@ -0,0 +1,109 @@ +steps: +- rmtree: + dir: "${ DEST_DIR }" +- chdir: + dir: "${ SOURCE_DIR }" +- copy_dir: + src: "${ SOURCE_DIR }" + dst: "${ DEST_DIR }" + ignore_globs: + - "*.hpp" + - "apps/*.cpp" + - "build" + - "examples/*.py" + - "fpga/gain/ip" + - "fpga/gain/rfnoc_block_gain" + - "icores/*.yml" + - "lib/*.cpp" + - "gr-*" + - "rfnoc/**/*.yml" + - "rfnoc/dts" + - "xsim_proj" + - "modelsim_proj" +- chdir: + dir: "${ DEST_DIR }" +- multi_rename: + glob: "**/*gain*" + pattern: gain + repl: newmod +- comment_out: + file: apps/CMakeLists.txt + range: [7, -1] + character: "#" +- insert_after: + file: apps/CMakeLists.txt + pattern: "or-later..." + text: "\n# Edit this to add your own apps!\n" +- search_and_replace: + file: examples/CMakeLists.txt + pattern: "^ .+py$" + repl: " # Insert your example sources here if you want them to be installed" +- comment_out: + file: fpga/newmod/CMakeLists.txt + range: [9, 13] +- search_and_replace: + file: fpga/newmod/CMakeLists.txt + pattern: "add_subdirectory\\(([a-z0-9_]+)\\)" + repl: "#add_subdirectory(rfnoc_block_myblock)" +- insert_before: + file: fpga/newmod/CMakeLists.txt + pattern: "# IP gets" + text: "# Uncomment the following lines to add your own IP:\n" +- search_and_replace: + file: icores/CMakeLists.txt + pattern: "RFNOC_REGISTER.*$" + repl: "# Add one line for every image core here:\n#RFNOC_REGISTER_BLOCK(SRC my_image_core)" +- search_and_replace: + file: include/rfnoc/newmod/CMakeLists.txt + pattern: "gain_block_control.hpp" + repl: "#my_block_control.hpp" +- search_and_replace: + file: lib/CMakeLists.txt + pattern: "gain_block_control.cpp" + repl: "#my_block_control.cpp" +- search_and_replace: + file: python/rfnoc_newmod/__init__.py + pattern: "GainBlockControl.*$" + repl: "#MyBlockControl = lib.my_block_control" +- search_and_replace: + file: python/pyrfnoc-newmod.cpp + pattern: '#include "gain_block_control_python.hpp"\n' + repl: "" +- search_and_replace: + file: python/pyrfnoc-newmod.cpp + pattern: "export_gain_block_control\\(m\\);\n" + repl: "" +- search_and_replace: + file: README.md + pattern: ".*(?=## Directory Structure)" + repl: | + # RFNoC Out-of-Tree Module + + This is a template for creating an RFNoC out-of-tree module. It is based on the + rfnoc-gain example that ships with UHD. + + We recommend sticking to this directory structure and file layout.\n + count: 1 +- search_and_replace: + file: rfnoc/CMakeLists.txt + pattern: "add_subdirectory\\(dts\\)" + repl: "#add_subdirectory(dts)" +- search_and_replace: + files: + - CMakeLists.txt + - apps/CMakeLists.txt + - examples/CMakeLists.txt + - python/rfnoc_newmod/__init__.py + - python/CMakeLists.txt + pattern: r"rfnoc(.)gain" + repl: r"rfnoc\1newmod" +- search_and_replace: + files: + - CMakeLists.txt + - include/rfnoc/newmod/CMakeLists.txt + - lib/CMakeLists.txt + - python/pyrfnoc-newmod.cpp + - python/setup.py.in + - README.md + pattern: "gain" + repl: "newmod" diff --git a/host/cmake/debian/uhd-host.install b/host/cmake/debian/uhd-host.install index a5688819f..5109fd2a6 100644 --- a/host/cmake/debian/uhd-host.install +++ b/host/cmake/debian/uhd-host.install @@ -1,6 +1,7 @@ debian/NetworkManager-USRP /etc/NetworkManager/system-connections/ usr/lib/uhd* /usr/lib/ usr/bin/rfnoc_image_builder +usr/bin/rfnoc_modtool usr/bin/uhd_adc_self_cal usr/bin/uhd_cal_rx_iq_balance usr/bin/uhd_cal_tx_dc_offset diff --git a/host/docs/CMakeLists.txt b/host/docs/CMakeLists.txt index a8e73ccc9..2f7de45a2 100644 --- a/host/docs/CMakeLists.txt +++ b/host/docs/CMakeLists.txt @@ -121,6 +121,7 @@ set(man_page_sources uhd_usrp_probe.1 usrp_n2xx_simple_net_burner.1 usrp2_card_burner.1 + rfnoc_modtool.1 ) if (ENABLE_PYTHON_API) diff --git a/host/docs/rfnoc_modtool.1 b/host/docs/rfnoc_modtool.1 new file mode 100644 index 000000000..6765a3cf0 --- /dev/null +++ b/host/docs/rfnoc_modtool.1 @@ -0,0 +1,69 @@ +.TH "rfnoc_modtool" 1 "4.8.0" UHD "User Commands" +.SH NAME +rfnoc_modtool - RFNoC OOT module management tool + +.SH DESCRIPTION +Create and manage RFNoC OOT modules. + + +.SH SYNOPSIS +.B rfnoc_modtool [COMMAND] [OPTIONS] + +.SH COMMANDS +Run rfnoc_modtool COMMAND --help for more information on a specific command. + +.IP "create" +Create a new RFNoC OOT module. +.IP "add" +Add a new block to an existing RFNoC OOT module. + +.SH CREATING NEW MODULES +.sp +When running rfnoc_modtool create, a new directory will be created that contains +the necessary files for a new RFNoC OOT module. This directory will only contain +boilerplate code, and the user will need to add their own blocks to the module. + +.SH ADDING BLOCKS +.sp +When running rfnoc_modtool add, a new block will be added to an existing RFNoC OOT module. +This requires previously having run rfnoc_modtool create to create the module. + + +.SH EXAMPLES + +.SS Create a new RFNoC OOT module called "filter" +.sp +rfnoc_modtool create filter +.ft + +.SS Add a block called "fir" to the "filter" module (this requires having create a fir.yml block descriptor file) +.sp +rfnoc_modtool -C ./rfnoc-filter add fir -y rfnoc-foo/rfnoc/blocks/fir.yml +.ft + +.fi + +.SH SEE ALSO +UHD documentation: +.B http://files.ettus.com/manual/ +.LP +GR-UHD documentation: +.B http://gnuradio.org/doc/doxygen/page_uhd.html +.LP +Other UHD programs: +.sp +rfnoc_image_builder(1) +.SH AUTHOR +This manual page was written by Martin Braun. +.SH COPYRIGHT +Copyright (c) 2015-2022 Ettus Research, A National Instruments Brand +.LP +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. +.LP +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. diff --git a/host/python/CMakeLists.txt b/host/python/CMakeLists.txt index c1f03ddb2..fa7f57a77 100644 --- a/host/python/CMakeLists.txt +++ b/host/python/CMakeLists.txt @@ -42,6 +42,7 @@ if(NOT ENABLE_PYTHON_API AND ENABLE_PYMOD_UTILS) ${CMAKE_CURRENT_SOURCE_DIR}/uhd/dsp/*.py ${CMAKE_CURRENT_SOURCE_DIR}/uhd/rfnoc_utils/*.py ${CMAKE_CURRENT_SOURCE_DIR}/uhd/rfnoc_utils/*.mako + ${CMAKE_CURRENT_SOURCE_DIR}/uhd/rfnoc_utils/*.yml ) # This copies the contents of host/python/uhd into the build directory. We will # use that as a staging ground for installing the final module to the system. @@ -133,6 +134,8 @@ add_custom_command(TARGET pyuhd # dependency list until CMake is re-run. file(GLOB_RECURSE PYUHD_FILES ${CMAKE_CURRENT_SOURCE_DIR}/uhd/*.py + ${CMAKE_CURRENT_SOURCE_DIR}/uhd/*.mako + ${CMAKE_CURRENT_SOURCE_DIR}/uhd/*.yml ) if(ENABLE_SIM) diff --git a/host/python/copy_python_module.cmake b/host/python/copy_python_module.cmake index db8454b5b..3395df658 100644 --- a/host/python/copy_python_module.cmake +++ b/host/python/copy_python_module.cmake @@ -8,4 +8,4 @@ set(BINARY_DIR "" CACHE STRING "") set(SOURCE_DIR "" CACHE STRING "") file(COPY "${SOURCE_DIR}/uhd/" DESTINATION "${BINARY_DIR}/uhd" - FILES_MATCHING PATTERN "*.py" PATTERN "*.mako") + FILES_MATCHING PATTERN "*.py" PATTERN "*.mako" PATTERN "*.yml") diff --git a/host/python/setup.py.in b/host/python/setup.py.in index a1f75fe38..95c4b52dc 100755 --- a/host/python/setup.py.in +++ b/host/python/setup.py.in @@ -1,38 +1,50 @@ #!/usr/bin/env python3 -# -# Copyright 2017-2018 Ettus Research, a National Instruments Company -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -"""Setup file for uhd module""" +"""Setup file for uhd module. -from setuptools import setup, find_packages +Copyright 2017-2018 Ettus Research, a National Instruments Company -packages = find_packages() + ['uhd.rfnoc_utils.templates', 'uhd.rfnoc_utils.templates.modules'] +SPDX-License-Identifier: GPL-3.0-or-later +""" + +from setuptools import find_packages, setup + +packages = find_packages() + [ + "uhd.rfnoc_utils.templates", + "uhd.rfnoc_utils.templates.modules", + "uhd.rfnoc_utils.modtool_commands", +] print("Including packages in pyuhd:", packages) -setup(name='uhd', - version='${UHD_VERSION_MAJOR}.${UHD_VERSION_API}.${UHD_VERSION_ABI}', - description='Universal Software Radio Peripheral (USRP) Hardware Driver Python API', - classifiers=[ - 'Development Status :: 4 - Beta', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: C++', - 'Programming Language :: Python', - 'Topic :: System :: Hardware :: Hardware Drivers', - ], - keywords='SDR UHD USRP', - author='Ettus Research', - author_email='packages@ettus.com', - url='https://www.ettus.com/', - license='GPLv3', - package_dir={'': r'${NATIVE_CURRENT_BINARY_DIR}'}, - package_data={ - 'uhd': ['*.so'], - 'uhd.rfnoc_utils.templates': ['*.mako'], - 'uhd.rfnoc_utils.templates.modules': ['*.mako'], - }, - zip_safe=False, - packages=packages, - install_requires=['numpy']) +setup( + name="uhd", + version="${UHD_VERSION_MAJOR}.${UHD_VERSION_API}.${UHD_VERSION_ABI}", + description="Universal Software Radio Peripheral (USRP) Hardware Driver Python API", + classifiers=[ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: C++", + "Programming Language :: Python", + "Topic :: System :: Hardware :: Hardware Drivers", + ], + keywords="SDR UHD USRP", + author="Ettus Research", + author_email="packages@ettus.com", + url="https://www.ettus.com/", + license="GPLv3", + package_dir={"": r"${NATIVE_CURRENT_BINARY_DIR}"}, + package_data={ + "uhd": ["*.so"], + "uhd.rfnoc_utils.templates": ["*.mako"], + "uhd.rfnoc_utils.templates.modules": ["*.mako"], + "uhd.rfnoc_utils.modtool_commands": ["*.yml"], + }, + zip_safe=False, + packages=packages, + install_requires=["numpy", "ruamel.yaml", "mako"], + entry_points={ + "console_scripts": [ + "rfnoc_modtool = uhd.rfnoc_utils.rfnoc_modtool:main", + ] + }, +) diff --git a/host/python/uhd/rfnoc_utils/log.py b/host/python/uhd/rfnoc_utils/log.py new file mode 100644 index 000000000..976735e77 --- /dev/null +++ b/host/python/uhd/rfnoc_utils/log.py @@ -0,0 +1,77 @@ +"""RFNoC Utilities: Logging. + +SPDX-License-Identifier: GPL-3.0-or-later +""" + +import logging +import sys + + +class ColorFormatter(logging.Formatter): + """Logging Formatter to add colors and icons.""" + + RESET = 0 + BOLD = 1 + DIM = 2 + GREY = "38;20" + BLACK = "30;20" + YELLOW = "33;20" + RED = "31;20" + BRIGHTRED = 91 + BRIGHT = 99 + + def c(colors): # noqa -- should be staticmethod but that requires Python 3.10 + """Format escape sequence from list of colors.""" + return f"\x1b[{';'.join(str(c) for c in colors)}m" + + debug_color = c([DIM]) + info_color = c([BRIGHT, BOLD]) + warning_color = c([YELLOW]) + error_color = c([BRIGHTRED]) + crit_color = c([RED, BOLD]) + reset = c([RESET]) + + FORMATS = { + logging.DEBUG: debug_color + "[debug] %(message)s" + reset, + logging.INFO: info_color + "%(message)s" + reset, + logging.WARNING: warning_color + "⚠ %(message)s" + reset, + logging.ERROR: error_color + "⛔ %(message)s" + reset, + logging.CRITICAL: crit_color + "⛔ %(message)s" + reset, + } + + def format(self, record): + """Format a record the way we like it.""" + log_fmt = self.FORMATS.get(record.levelno) + return logging.Formatter(log_fmt).format(record) + + +class SimpleFormatter(logging.Formatter): + """Logging Formatter for non-interactive shells.""" + + FORMATS = { + logging.DEBUG: "[debug] %(message)s", + logging.INFO: "%(message)s", + logging.WARNING: "[warning] %(message)s", + logging.ERROR: "[error] %(message)s", + logging.CRITICAL: "[critical] %(message)s", + } + + def format(self, record): + """Format a record the way we like it.""" + log_fmt = self.FORMATS.get(record.levelno) + return logging.Formatter(log_fmt).format(record) + + +def init_logging(color_mode="auto", log_level="info"): + """Initialize the logging interface for RFNoC utilities.""" + use_color = (color_mode == "always") or ( + color_mode == "auto" and sys.__stdout__.isatty() and sys.__stderr__.isatty() + ) + handler = logging.StreamHandler() + handler.setFormatter(ColorFormatter() if use_color else SimpleFormatter()) + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + + if log_level: + logging.root.setLevel(log_level.upper()) diff --git a/host/python/uhd/rfnoc_utils/modtool_commands/add.yml b/host/python/uhd/rfnoc_utils/modtool_commands/add.yml new file mode 100644 index 000000000..6584a2b8b --- /dev/null +++ b/host/python/uhd/rfnoc_utils/modtool_commands/add.yml @@ -0,0 +1,107 @@ +# +# Copyright 2024 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +help: | + Add a new block to an RFNoC OOT module based on a descriptor file. + + Note: Must be called from within a valid RFNoC OOT module directory. Use -C to + specify the module directory if necessary. + +# These get turned into command line arguments for this command. May not contain +# variable references (they get evaluated later). +args: + blockname: + name_or_flags: blockname + type: str + help: Name of the new block to add to the module + yaml_descriptor: + name_or_flags: ["-y", "--yaml-descriptor"] + type: str + metavar: YAML_FILE + help: > + Path to the YAML descriptor file for the new block. Defaults to rfnoc/blocks/.yml. + If this file does not exist, the command will fail with an error. + copyright_holder: + name_or_flags: "--copyright-holder" + default: "" + license: + name_or_flags: "--license" + default: "SPDX-License-Identifier: GPL-3.0-or-later" + skip_testbench: + name_or_flags: "--skip-testbench" + action: store_true + help: Skip generating the testbench for the new block + +# Note: Variables get resolved in order, that means later vars can reference earlier ones +variables: + # Type is block or transport_adapter (latter not yet implemented) + type: block + type_d: blocks + blockname: "${ args.blockname }" + descriptor: "${ args.yaml_descriptor if args.yaml_descriptor else 'rfnoc/' + type_d + '/' + args.blockname + '.yml' }" + blockname_full: "${ f'rfnoc_{type}_{blockname}' }" + copyright_holder: "${ args.copyright_holder }" + license: "${ args.license }" + + +# Steps: +# - Create controller C++ +# - Create Python bindings + + +# This tells us that templates come from the blocktool/ subdirectory +template_namespace: blocktool + +steps: + - parse_descriptor: + source: "${ descriptor }" + # RFNoC block gateware + - write_template: + template: noc_shell_template.sv.mako + dest: "fpga/${ MODULE_NAME }/${ blockname_full }/noc_shell_${ blockname }.sv" + - write_template: + template: rfnoc_block_template.sv.mako + dest: "fpga/${ MODULE_NAME }/${ blockname_full }/${ blockname_full }.sv" + - write_template: + template: Makefile.srcs.mako + dest: "fpga/${ MODULE_NAME }/${ blockname_full }/Makefile.srcs" + # RFNoC block testbench + - run_if: + condition: "${ not args.skip_testbench }" + steps: + - write_template: + template: rfnoc_block_template_tb.sv.mako + dest: "fpga/${ MODULE_NAME }/${ blockname_full }/${ blockname_full }_tb.sv" + - write_template: + template: Makefile.mako + dest: "fpga/${ MODULE_NAME }/${ blockname_full }/Makefile" + # RFNoC block C++ controller + - write_template: + template: template_block_control.hpp.mako + dest: "include/rfnoc/${ MODULE_NAME }/${ blockname }_block_control.hpp" + - write_template: + template: template_block_control.cpp.mako + dest: "lib/${ blockname }_block_control.cpp" + - insert_after: + file: "include/rfnoc/${ MODULE_NAME }/CMakeLists.txt" + pattern: "install.*FILES" + text: "\n ${ blockname }_block_control.hpp" + - insert_after: + file: "lib/CMakeLists.txt" + pattern: "APPEND *rfnoc_${ MODULE_NAME }_sources" + text: "\n ${ blockname }_block_control.cpp" + # RFNoC block Python bindings + - write_template: + template: template_block_control_python.hpp.mako + dest: "python/${ blockname }_block_control_python.hpp" + - insert_after: + file: "python/pyrfnoc-${ MODULE_NAME}.cpp" + pattern: "PYBIND11_MODULE[^}]*" + text: " export_${ blockname }_block_control(m);\n" + - insert_before: + file: "python/pyrfnoc-${ MODULE_NAME}.cpp" + pattern: "\nPYBIND11_MODULE" + text: '#include "${ blockname }_block_control_python.hpp"\n' diff --git a/host/python/uhd/rfnoc_utils/modtool_commands/create.yml b/host/python/uhd/rfnoc_utils/modtool_commands/create.yml new file mode 100644 index 000000000..4716fb708 --- /dev/null +++ b/host/python/uhd/rfnoc_utils/modtool_commands/create.yml @@ -0,0 +1,45 @@ +# +# Copyright 2024 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +# Because this is for creating a new module, we skip the step of finding a valid +# module to operate in. This requires us to go switch directories further down. +skip_identify_module: true + +help: | + Create a new RFNoC OOT module + +# These get turned into command line arguments for this command. May not contain +# variable references (they get evaluated later). +args: + module_name: + name_or_flags: module_name + type: str + help: Name of the new RFNoC OOT module, without the leading "rfnoc-" + +# These variables may be used in the steps below +variables: + module_name: "${ args.module_name }" + module_name_full: "${ args.module_name if args.module_name.startswith('rfnoc-') else 'rfnoc-' + args.module_name }" + +steps: + - copy_dir: + src: "${ RFNOC_PKG_DIR }/rfnoc-newmod" + dst: "${ CWD }/${ module_name_full }" + - chdir: + dir: "${ CWD }/${ module_name_full }" + - search_and_replace: + glob: "**" + pattern: 'newmod' + repl: "${ module_name }" + quiet: true + - search_and_replace: + glob: README.md + pattern: "${ module_name } example that ships with" + repl: "gain example that ships with" + - multi_rename: + glob: "**/*newmod*" + pattern: 'newmod' + repl: "${ module_name }" diff --git a/host/python/uhd/rfnoc_utils/rfnoc_modtool.py b/host/python/uhd/rfnoc_utils/rfnoc_modtool.py new file mode 100644 index 000000000..4eb39f485 --- /dev/null +++ b/host/python/uhd/rfnoc_utils/rfnoc_modtool.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""RFNoC Modtool: The tool to create and manipulate RFNoC OOT modules.""" +# +# Copyright 2024 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import argparse +import glob +import os +import sys + +from ruamel.yaml import YAML + +from .log import init_logging +from .step_executor import StepExecutor +from .utils import resolve + + +def get_command_repo_dir(): + """Return the path to the command repository directory (where the command YAMLS are stored).""" + this_dir = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(this_dir, "modtool_commands") + + +def collect_commands(): + """Load all available commands. + + The return value is a dictionary where the keys are the command names and + the values are raw contents of the YAML as Python structures. + """ + yamls = glob.glob(os.path.join(get_command_repo_dir(), "*.yml")) + yaml = YAML() + commands = {} + for y in yamls: + cmd_name = os.path.basename(y).replace(".yml", "") + with open(y, "r", encoding="utf-8") as f: + commands[cmd_name] = yaml.load(f) + return commands + + +def parse_args(cmds): + """Parse args. + + This will dynamically populate main- and subparsers from the YAML configs + given in cmds. + """ + arg_parser = argparse.ArgumentParser(description=__doc__) + arg_parser.add_argument( + "-C", "--directory", help="Change to this directory before running the command" + ) + arg_parser.add_argument( + "-l", "--log-level", default="INFO", help="Set the log level (default: INFO)" + ) + subparsers = arg_parser.add_subparsers(dest="command") + parser_list = [] + for cmd in cmds: + new_parser = subparsers.add_parser(cmd, help=cmds[cmd]["help"]) + for arg_name, arg_info in cmds[cmd]["args"].items(): + name_or_flags = ( + f"{arg_name}" if "name_or_flags" not in arg_info else arg_info.pop("name_or_flags") + ) + if not isinstance(name_or_flags, list): + name_or_flags = [name_or_flags] + # We don't want to use eval(), but for most types we need to do + # just that, so manually map + if "type" in arg_info: + try: + arg_info["type"] = { + "str": str, + "int": int, + "float": float, + }[arg_info["type"]] + except KeyError: + pass + new_parser.add_argument(*name_or_flags, **arg_info) + parser_list.append(new_parser) + return arg_parser.parse_args() + + +def get_global_vars(pkg_data_dir): + """Create a dictionary with global variables for the key resolution.""" + return { + "RFNOC_PKG_DIR": pkg_data_dir, + "CWD": os.getcwd(), + } + + +def resolve_vars(cmd, global_vars, args): + """Resolve all variables in the command.""" + for var_name, var_val in cmd.get("variables", {}).items(): + cmd["variables"][var_name] = resolve( + var_val, args=args, **global_vars, **cmd.get("variables", {}) + ) + return cmd + + +def check_valid_oot_dir(oot_dir): + """Check if the given directory contains a valid OOT module.""" + return os.path.isfile(os.path.join(oot_dir, "CMakeLists.txt")) and os.path.isdir( + os.path.join(oot_dir, "rfnoc") + ) + + +def main(pkg_data_dir): + """Run main rfnoc_modtool function.""" + cmds = collect_commands() + args = parse_args(cmds) + init_logging(log_level=args.log_level) + if args.directory: + os.chdir(args.directory) + cmd = cmds[args.command] + global_vars = get_global_vars(pkg_data_dir) + cmd = resolve_vars(cmd, global_vars, args) + if not cmd.get("skip_identify_module", False): + if not check_valid_oot_dir(os.getcwd()): + print("Error: Not a valid OOT module directory") + return 1 + oot_dir_name = os.path.split(os.getcwd())[-1] + module_name = oot_dir_name.replace("rfnoc-", "") + global_vars["MODULE_NAME"] = module_name + global_vars["MODULE_NAME_FULL"] = oot_dir_name + executor = StepExecutor(global_vars, args, cmd) + executor.run(cmd["steps"]) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/host/python/uhd/rfnoc_utils/step_executor.py b/host/python/uhd/rfnoc_utils/step_executor.py new file mode 100644 index 000000000..00c7f6043 --- /dev/null +++ b/host/python/uhd/rfnoc_utils/step_executor.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""RFNoC Modtool: Step Executor. + +Contains the class that can run individual steps of a command script. +""" +# +# Copyright 2024 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import datetime +import fnmatch +import glob +import logging +import os +import re +import shutil +import sys +from pathlib import Path + +import mako.lookup +import mako.template +from ruamel.yaml import YAML + +from .utils import resolve + + +def get_file_list(**kwargs): + """Extract a list of filenames from kwargs. + + The following rules apply: + - If "glob" is present, use it to generate a list of filenames. + - If "files" is present, use it as a list of filenames. + - If "file" is present, use it as a single filename. + """ + file_list = [] + if "glob" in kwargs: + glob_pattern = kwargs["glob"] + # FIXME: Maybe the user wants to glob somewhere else + glob_pattern = os.path.join(os.getcwd(), glob_pattern) + file_list = [ + f + for f in glob.glob(kwargs["glob"], recursive=kwargs.get("glob_recursive", True)) + if Path(f).is_file() + ] + file_list += kwargs.get("files", []) + if "file" in kwargs: + file_list.append(kwargs["file"]) + return file_list + + +class StepExecutor: + """Main engine for executing steps in a command script.""" + + def __init__(self, global_vars, args, cmd): + """Initialize the executor.""" + self.cmd = cmd + self.args = args + self.global_vars = global_vars + self.template_base = os.path.join(os.path.dirname(__file__), "templates") + self.log = logging.getLogger(__name__) + + def _resolve(self, value): + """Shorthand for resolving a value.""" + return resolve(value, args=self.args, **self.global_vars, **self.cmd.get("variables", {})) + + def run(self, steps): + """Run all the steps in the command.""" + for step in steps: + for step_type, step_args in step.items(): + getattr(self, step_type)(**{k: self._resolve(v) for k, v in step_args.items()}) + + def run_if(self, condition, steps): + """Run a block of steps if a condition is true.""" + if self._resolve(condition): + self.run(steps) + + def copy_dir(self, src, dst, **kwargs): + """Copy a directory from src to dest, recursively.""" + self.log.debug("Copying directory %s to %s", src, dst) + ignore = None + if "ignore_globs" in kwargs: + + def ignore_patterns(path, names): + relpath = os.path.relpath(path, os.getcwd()) + return [ + name + for name in names + if any( + fnmatch.fnmatch(os.path.normpath(os.path.join(relpath, name)), pattern) + for pattern in kwargs["ignore_globs"] + ) + ] + + ignore = ignore_patterns + copytree_kwargs = {"ignore": ignore} + if sys.version_info >= (3, 8): + copytree_kwargs["dirs_exist_ok"] = True + shutil.copytree(src, dst, **copytree_kwargs) + + def search_and_replace(self, **kwargs): + """Search and replace text in a file.""" + file_list = get_file_list(**kwargs) + for file in file_list: + self.log.debug( + "Editing file %s (replacing `%s' with `%s')", + file, + kwargs["pattern"], + kwargs["repl"], + ) + with open(file, "r", encoding="utf-8") as f: + contents = f.read() + count = kwargs.get("count", 0) + contents, sub_count = re.subn( + kwargs["pattern"], + kwargs["repl"], + contents, + count=count, + flags=re.MULTILINE | re.DOTALL, + ) + if sub_count == 0 and not kwargs.get("quiet", False): + self.log.warning("Pattern not found in file %s", os.path.abspath(file)) + with open(file, "w", encoding="utf-8") as f: + f.write(contents) + + def chdir(self, dir): + """Change the working directory.""" + os.chdir(os.path.normpath(dir)) + self.global_vars["CWD"] = os.getcwd() + self.log.debug("Changed working directory to %s", os.getcwd()) + + def multi_rename(self, pattern, repl, **kwargs): + """Rename multiple files with a regex pattern.""" + paths_to_rename = glob.glob(kwargs["glob"], recursive=kwargs.get("glob_recursive", True)) + for path in paths_to_rename: + head, tail = os.path.split(path) + new_path = os.path.join(head, re.sub(pattern, repl, tail)) + if new_path != path: + self.log.debug("Renaming %s to %s", path, new_path) + shutil.move(path, new_path) + + def parse_descriptor(self, source, var="config", **kwargs): + """Load a block descriptor file.""" + yaml = YAML(typ="safe", pure=True) + self.log.debug("Loading descriptor file %s into variable %s", source, var) + with open(source, "r", encoding="utf-8") as f: + self.global_vars[var] = yaml.load(f) + + def write_template(self, template, dest, **kwargs): + """Write a template file.""" + template_dir = os.path.join( + self.template_base, kwargs.get("namespace", self.cmd.get("template_namespace", "")) + ) + lookup = mako.lookup.TemplateLookup(directories=[template_dir]) + Path(dest).parent.mkdir(parents=True, exist_ok=True) + tpl = mako.template.Template( + filename=os.path.join(template_dir, template), lookup=lookup, strict_undefined=True + ) + vars = self.cmd.get("variables", {}).copy() + # Make sure standard template variables are available + if "year" in kwargs: + vars["year"] = kwargs["year"] + elif "year" in vars: + pass + else: + vars["year"] = datetime.datetime.now().year + self.log.debug("Writing template %s to %s", template, dest) + with open(dest, "w", encoding="utf-8") as f: + f.write(tpl.render(**self.global_vars, **vars)) + + def insert_after(self, pattern, text, **kwargs): + """Insert text after a pattern in a file.""" + file_list = get_file_list(**kwargs) + for file in file_list: + self.log.debug("Editing file %s (inserting `%s')", file, text.strip()) + with open(file, "r", encoding="utf-8") as f: + contents = f.read() + count = kwargs.get("count", 1) + contents, sub_count = re.subn( + pattern, r"\g<0>" + text, contents, count=count, flags=re.MULTILINE | re.DOTALL + ) + if sub_count == 0: + self.log.warning("Pattern not found in file %s", file) + with open(file, "w", encoding="utf-8") as f: + f.write(contents) + + def insert_before(self, pattern, text, **kwargs): + """Insert text after a pattern in a file.""" + file_list = get_file_list(**kwargs) + for file in file_list: + self.log.debug("Editing file %s (inserting `%s')", file, text.strip()) + with open(file, "r", encoding="utf-8") as f: + contents = f.read() + count = kwargs.get("count", 1) + contents, sub_count = re.subn( + pattern, text + r"\g<0>", contents, count=count, flags=re.MULTILINE | re.DOTALL + ) + if sub_count == 0: + self.log.warning("Pattern not found in file %s", file) + with open(file, "w", encoding="utf-8") as f: + f.write(contents) + + def comment_out(self, character="#", **kwargs): + """Modify all lines in range to prepend character. + + Note that the line range starts at 1, as it would with sed or in an + editor. + """ + line_range = kwargs.get("range", [1, -1]) + file_list = get_file_list(**kwargs) + for file in file_list: + self.log.debug("Commenting out lines in %s", file) + with open(file, "r", encoding="utf-8") as f: + contents = f.readlines() + for line_no_minus_one, line in enumerate(contents): + if line_no_minus_one + 1 >= line_range[0] and ( + line_no_minus_one + 1 <= line_range[1] or line_range[1] == -1 + ): + contents[line_no_minus_one] = character + line + with open(file, "w", encoding="utf-8") as f: + f.writelines(contents) + + def rmtree(self, **kwargs): + """Remove a directory tree.""" + dir_to_remove = kwargs["dir"] + self.log.debug("Removing directory %s", dir_to_remove) + if os.path.exists(dir_to_remove) and os.path.isdir(dir_to_remove): + shutil.rmtree(dir_to_remove) diff --git a/host/utils/rfnoc_blocktool/templates/Makefile b/host/python/uhd/rfnoc_utils/templates/blocktool/Makefile.mako similarity index 95% rename from host/utils/rfnoc_blocktool/templates/Makefile rename to host/python/uhd/rfnoc_utils/templates/blocktool/Makefile.mako index 55b02790e..2cb5eabfc 100644 --- a/host/utils/rfnoc_blocktool/templates/Makefile +++ b/host/python/uhd/rfnoc_utils/templates/blocktool/Makefile.mako @@ -26,9 +26,10 @@ include $(BASE_DIR)/../lib/rfnoc/utils/Makefile.srcs include Makefile.srcs DESIGN_SRCS += $(abspath ${"\\"} +$(RFNOC_SRCS) ${"\\"} $(RFNOC_CORE_SRCS) ${"\\"} $(RFNOC_UTIL_SRCS) ${"\\"} -$(RFNOC_OOT_SRCS) ${"\\"} +$(${ blockname_full.upper() }_SRCS) ${"\\"} ) #------------------------------------------------- diff --git a/host/utils/rfnoc_blocktool/templates/Makefile.srcs b/host/python/uhd/rfnoc_utils/templates/blocktool/Makefile.srcs.mako similarity index 89% rename from host/utils/rfnoc_blocktool/templates/Makefile.srcs rename to host/python/uhd/rfnoc_utils/templates/blocktool/Makefile.srcs.mako index 9a86c0c23..e632fe166 100644 --- a/host/utils/rfnoc_blocktool/templates/Makefile.srcs +++ b/host/python/uhd/rfnoc_utils/templates/blocktool/Makefile.srcs.mako @@ -16,7 +16,7 @@ ${"##################################################"} # calling this file. RFNOC_OOT_SRCS needs to be a simply expanded variable # (not a recursively expanded variable), and we take care of that in the build # infrastructure. -RFNOC_OOT_SRCS += $(addprefix $(dir $(abspath $(lastword $(MAKEFILE_LIST)))), ${"\\"} +${ blockname_full.upper() }_SRCS += $(addprefix $(dir $(abspath $(lastword $(MAKEFILE_LIST)))), ${"\\"} rfnoc_block_${config['module_name']}.v ${"\\"} noc_shell_${config['module_name']}.v ${"\\"} ) diff --git a/host/utils/rfnoc_blocktool/templates/functions.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/functions.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/functions.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/functions.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_chdr_connect_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_chdr_connect_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_chdr_connect_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_chdr_connect_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_chdr_modules_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_chdr_modules_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_chdr_modules_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_chdr_modules_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_chdr_wires_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_chdr_wires_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_chdr_wires_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_chdr_wires_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_ctrl_connect_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_ctrl_connect_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_ctrl_connect_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_ctrl_connect_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_ctrl_modules_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_ctrl_modules_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_ctrl_modules_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_ctrl_modules_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_ctrl_wires_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_ctrl_wires_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_ctrl_wires_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_ctrl_wires_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_data_connect_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_data_connect_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_data_connect_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_data_connect_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_data_modules_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_data_modules_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_data_modules_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_data_modules_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_data_wires_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_data_wires_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_data_wires_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_data_wires_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_pyld_ctxt_connect_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_pyld_ctxt_connect_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_pyld_ctxt_connect_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_pyld_ctxt_connect_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_pyld_ctxt_modules_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_pyld_ctxt_modules_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_pyld_ctxt_modules_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_pyld_ctxt_modules_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/axis_pyld_ctxt_wires_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_pyld_ctxt_wires_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/axis_pyld_ctxt_wires_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/axis_pyld_ctxt_wires_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/ctrlport_connect_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/ctrlport_connect_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/ctrlport_connect_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/ctrlport_connect_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/ctrlport_modules_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/ctrlport_modules_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/ctrlport_modules_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/ctrlport_modules_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/modules/ctrlport_wires_template.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/modules/ctrlport_wires_template.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/modules/ctrlport_wires_template.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/modules/ctrlport_wires_template.mako diff --git a/host/utils/rfnoc_blocktool/templates/noc_shell_template.v.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/noc_shell_template.sv.mako similarity index 100% rename from host/utils/rfnoc_blocktool/templates/noc_shell_template.v.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/noc_shell_template.sv.mako diff --git a/host/utils/rfnoc_blocktool/templates/rfnoc_block_template.v.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/rfnoc_block_template.sv.mako similarity index 98% rename from host/utils/rfnoc_blocktool/templates/rfnoc_block_template.v.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/rfnoc_block_template.sv.mako index fff1c70dd..1522f1dee 100644 --- a/host/utils/rfnoc_blocktool/templates/rfnoc_block_template.v.mako +++ b/host/python/uhd/rfnoc_utils/templates/blocktool/rfnoc_block_template.sv.mako @@ -3,9 +3,9 @@ import math %>\ <%namespace name="func" file="/functions.mako"/>\ // -// Copyright ${year} Ettus Research, a National Instruments Brand +// Copyright ${year} ${copyright_holder} // -// SPDX-License-Identifier: LGPL-3.0-or-later +// ${license} // // Module: rfnoc_block_${config['module_name']} // diff --git a/host/utils/rfnoc_blocktool/templates/rfnoc_block_template_tb.sv.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/rfnoc_block_template_tb.sv.mako similarity index 98% rename from host/utils/rfnoc_blocktool/templates/rfnoc_block_template_tb.sv.mako rename to host/python/uhd/rfnoc_utils/templates/blocktool/rfnoc_block_template_tb.sv.mako index 1034d3439..7da16d4b0 100644 --- a/host/utils/rfnoc_blocktool/templates/rfnoc_block_template_tb.sv.mako +++ b/host/python/uhd/rfnoc_utils/templates/blocktool/rfnoc_block_template_tb.sv.mako @@ -1,8 +1,8 @@ <%namespace name="func" file="/functions.mako"/>\ // -// Copyright ${year} Ettus Research, a National Instruments Brand +// Copyright ${year} ${copyright_holder} // -// SPDX-License-Identifier: LGPL-3.0-or-later +// ${license} // // Module: rfnoc_block_${config['module_name']}_tb // diff --git a/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control.cpp.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control.cpp.mako new file mode 100644 index 000000000..7716eff3b --- /dev/null +++ b/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control.cpp.mako @@ -0,0 +1,30 @@ +// +// Copyright ${year} ${copyright_holder} +// +// ${license} +// + +// Include our own header: +#include + +// These two includes are the minimum required to implement a block: +#include +#include + +using namespace rfnoc::${MODULE_NAME}; +using namespace uhd::rfnoc; + +// Define register addresses here: +//const uint32_t ${blockname}_block_control::REG_NAME = 0x1234; + +class ${blockname}_block_control_impl : public ${blockname}_block_control +{ +public: + RFNOC_BLOCK_CONSTRUCTOR(${blockname}_block_control) {} + + +private: +}; + +UHD_RFNOC_BLOCK_REGISTER_DIRECT( + ${blockname}_block_control, ${config["noc_id"]}, "Gain", CLOCK_KEY_GRAPH, "bus_clk") diff --git a/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control.hpp.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control.hpp.mako new file mode 100644 index 000000000..703de09d4 --- /dev/null +++ b/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control.hpp.mako @@ -0,0 +1,29 @@ +// +// Copyright ${year} ${copyright_holder} +// +// ${license} +// + +#pragma once + +#include +#include + +#include + +namespace rfnoc { namespace ${MODULE_NAME} { + +/*! Block controller: Describe me! + */ +class UHD_API ${blockname}_block_control : public uhd::rfnoc::noc_block_base +{ +public: + RFNOC_DECLARE_BLOCK(${blockname}_block_control) + + // List all registers here if you need to know their address in the block controller: + ////! The register address of the gain value + //static const uint32_t REG_NAME; + +}; + +}} // namespace rfnoc::gain diff --git a/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control_python.hpp.mako b/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control_python.hpp.mako new file mode 100644 index 000000000..d9dbf0fe4 --- /dev/null +++ b/host/python/uhd/rfnoc_utils/templates/blocktool/template_block_control_python.hpp.mako @@ -0,0 +1,21 @@ +// +// Copyright ${year} ${copyright_holder} +// +// ${license} +// + +#pragma once + +#include +#include + +using namespace rfnoc::gain; + +void export_${blockname}_block_control(py::module& m) +{ + py::class_<${blockname}_block_control, ${blockname}_block_control::sptr>(m, "${blockname}_block_control") + .def(py::init( + &uhd::rfnoc::block_controller_factory<${blockname}_block_control>::make_from)) + + ; +} diff --git a/host/utils/CMakeLists.txt b/host/utils/CMakeLists.txt index 088c3b505..a9a4ef415 100644 --- a/host/utils/CMakeLists.txt +++ b/host/utils/CMakeLists.txt @@ -23,6 +23,47 @@ if(ENABLE_PYMOD_UTILS OR ENABLE_UTILS) COMPONENT utilities ) + ### RFNoC modtool. Note this also requires the RFNoC gain OOT. + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/rfnoc_modtool.py" + "${CMAKE_CURRENT_BINARY_DIR}/rfnoc_modtool" + ) + UHD_INSTALL(PROGRAMS + ${CMAKE_CURRENT_BINARY_DIR}/rfnoc_modtool + RENAME rfnoc_modtool + DESTINATION ${RUNTIME_DIR} + COMPONENT utilities + ) + + file(GLOB_RECURSE RFNOC_GAIN_OOT_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/apps/CMakeLists.txt + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/cmake/* + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/CMakeLists.txt + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/examples/CMakeLists.txt + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/fpga/gain/CMakeLists.txt + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/include/rfnoc/gain/CMakeLists.txt + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/lib/CMakeLists.txt + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/python/* + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/README.md + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/rfnoc/CMakeLists.txt + ${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain/rfnoc/blocks/CMakeLists.txt + ) + + add_custom_target( + rfnoc_newmod ALL + DEPENDS ${RFNOC_GAIN_OOT_FILES} + COMMAND + "${PYTHON_EXECUTABLE}" "${UHD_SOURCE_DIR}/cmake/Modules/copy_rfnoc_newmod.py" + "--source" "${CMAKE_CURRENT_SOURCE_DIR}/../examples/rfnoc-gain" + "--dest" "${CMAKE_CURRENT_BINARY_DIR}/rfnoc-newmod" + "--module-dir" "${CMAKE_CURRENT_SOURCE_DIR}/../python/uhd" + COMMENT "Generating rfnoc-newmod" + ) + install(DIRECTORY + ${CMAKE_CURRENT_BINARY_DIR}/rfnoc-newmod + DESTINATION ${PKG_DATA_DIR} + COMPONENT utilities) + ### UHD Images downloader # Configure the scripts file(READ ${CMAKE_CURRENT_SOURCE_DIR}/../../images/manifest.txt CMAKE_MANIFEST_CONTENTS) diff --git a/host/utils/copy_python_module.cmake b/host/utils/copy_python_module.cmake deleted file mode 100644 index 379fcf13a..000000000 --- a/host/utils/copy_python_module.cmake +++ /dev/null @@ -1,6 +0,0 @@ -set(BINARY_DIR "" CACHE STRING "") -set(SOURCE_DIR "" CACHE STRING "") -file(COPY "${SOURCE_DIR}/rfnoc/" DESTINATION ${BINARY_DIR}/rfnoc - FILES_MATCHING PATTERN *.py) -file(COPY "${SOURCE_DIR}/rfnoc/" DESTINATION ${BINARY_DIR}/rfnoc - FILES_MATCHING PATTERN *.v.mako) diff --git a/host/utils/rfnoc_blocktool/rfnoc_create_verilog.py b/host/utils/rfnoc_blocktool/rfnoc_create_verilog.py deleted file mode 100755 index cc5add95d..000000000 --- a/host/utils/rfnoc_blocktool/rfnoc_create_verilog.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright 2013-2015 Ettus Research LLC -# Copyright 2018 Ettus Research, a National Instruments Brand -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -""" -Create a task for each file to generate from templates. A task is based on -a named tuple to ease future extensions. -Each task generates on result file by utilizing the BlockGenerator class. -""" - -import argparse -import datetime -import os -import re -import sys -from collections import namedtuple -import mako.template -import mako.lookup -from mako import exceptions -from ruamel import yaml - - -class BlockGenerator: - """ - Generates a new file from a template utilizing a YAML configuration passed - as argument. - - Each BlockGenerator generate one file out of one template. - All substitution parameter used in the template must be given in the YAML - configuration. Exceptions: year (generated on the fly) and destination - (given as argument). The root object parsed from the YAML configuration is - config. All configuration items are represented as object members of config - (YAML dictionaries are resolved recursively). - """ - - def __init__(self, template_file): - """ - Initializes a new generator based on template_file - :param template_file: file used as root template during rendering, - template file is assumed to be located in folder - 'modules' relative to this Python file. - """ - self.template_file = template_file - self.year = datetime.datetime.now().year - self.parser = None - self.config = None - self.destination = None - - def setup_parser(self): - """ - Setup argument parser. - - ArgumentParser is able to receive destination path and a file location - of the YAML configuration. - """ - self.parser = argparse.ArgumentParser( - description="Generate RFNoC module out of yml description") - self.parser.add_argument("-c", "--config", required=True, - help="Path to yml configuration file") - self.parser.add_argument("-d", "--destination", required=True, - help="Destination path to write output files") - - def setup(self): - """ - Prepare generator for template substitution. - - Results of setup are save in member variables and are accessible for - further template processing. - self.year current year (for copyright headers) - self.destination root path where template generation results are placed - self.config everything that was passed as YAML format - configuration - """ - args = self.parser.parse_args() - self.year = datetime.datetime.now().year - self.destination = args.destination - os.makedirs(self.destination, exist_ok=True) - with open(args.config) as stream: - self.config = yaml.safe_load(stream) - - def run(self): - """ - Do template substitution for destination template using YAML - configuration passed by arguments. - - Template substitution is done in memory. The result is written to a file - where the destination folder is given by the argument parser and the - final filename is derived from the template file by substitute template - by the module name from the YAML configuration. - """ - # Create absolute paths for templates so run location doesn't matter - template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), - "templates")) - lookup = mako.lookup.TemplateLookup(directories=[template_dir]) - filename = os.path.join(template_dir, self.template_file) - tpl = mako.template.Template(filename=filename, lookup=lookup, - strict_undefined=True) - # Render and return - try: - block = tpl.render(**{"config": self.config, - "year": self.year, - "destination": self.destination,}) - except: - print(exceptions.text_error_template().render()) - sys.exit(1) - - filename = self.template_file - # pylint: disable=no-member - filename = re.sub(r"template(.*)\.mako", - r"%s\1" % self.config['module_name'], - filename) - fullpath = os.path.join(self.destination, filename) - - with open(fullpath, "w") as stream: - stream.write(block) - - -if __name__ == "__main__": - Task = namedtuple("Task", "name") - TASKS = [Task(name="noc_shell_template.v.mako"), - Task(name="rfnoc_block_template.v.mako"), - Task(name="rfnoc_block_template_tb.sv.mako"), - Task(name="Makefile"), - Task(name="Makefile.srcs")] - for task in TASKS: - generator = BlockGenerator(task.name) - generator.setup_parser() - generator.setup() - generator.run() diff --git a/host/utils/rfnoc_image_builder.py b/host/utils/rfnoc_image_builder.py index 07a1d6ef5..87ca5c722 100755 --- a/host/utils/rfnoc_image_builder.py +++ b/host/utils/rfnoc_image_builder.py @@ -1,20 +1,7 @@ #!/usr/bin/env python3 """RFNoC Image Builder: Create RFNoC FPGA bitfiles from YAML input. -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 . +SPDX-License-Identifier: GPL-3.0-or-later """ import argparse @@ -24,63 +11,7 @@ import os import re import sys -import yaml -from uhd.rfnoc_utils import grc, image_builder, yaml_utils - - -class ColorFormatter(logging.Formatter): - """Logging Formatter to add colors and icons.""" - - RESET = 0 - BOLD = 1 - DIM = 2 - GREY = "38;20" - BLACK = "30;20" - YELLOW = "33;20" - RED = "31;20" - BRIGHTRED = 91 - BRIGHT = 99 - - def c(colors): # noqa -- should be staticmethod but that requires Python 3.10 - """Format escape sequence from list of colors.""" - return f"\x1b[{';'.join(str(c) for c in colors)}m" - - debug_color = c([DIM]) - info_color = c([BRIGHT, BOLD]) - warning_color = c([YELLOW]) - error_color = c([BRIGHTRED]) - crit_color = c([RED, BOLD]) - reset = c([RESET]) - - FORMATS = { - logging.DEBUG: debug_color + "[debug] %(message)s" + reset, - logging.INFO: info_color + "%(message)s" + reset, - logging.WARNING: warning_color + "⚠ %(message)s" + reset, - logging.ERROR: error_color + "⛔ %(message)s" + reset, - logging.CRITICAL: crit_color + "⛔ %(message)s" + reset, - } - - def format(self, record): - """Format a record the way we like it.""" - log_fmt = self.FORMATS.get(record.levelno) - return logging.Formatter(log_fmt).format(record) - -class SimpleFormatter(logging.Formatter): - """Logging Formatter for non-interactive shells.""" - - FORMATS = { - logging.DEBUG: "[debug] %(message)s", - logging.INFO: "%(message)s", - logging.WARNING: "[warning] %(message)s", - logging.ERROR: "[error] %(message)s", - logging.CRITICAL: "[critical] %(message)s", - } - - def format(self, record): - """Format a record the way we like it.""" - log_fmt = self.FORMATS.get(record.levelno) - return logging.Formatter(log_fmt).format(record) - +from uhd.rfnoc_utils import grc, image_builder, log as rfnoc_log, yaml_utils def setup_parser(): @@ -238,7 +169,8 @@ def setup_parser(): choices=("never", "auto", "always"), default="auto", help="Enable colorful output. When set to 'auto' will only show color " - "output in TTY environments (e.g., interactive shells)") + "output in TTY environments (e.g., interactive shells)", + ) return parser @@ -262,6 +194,9 @@ def image_config(args): if image_core_name is None: image_core_name = os.path.splitext(os.path.basename(args.yaml_config))[0] return config, args.yaml_config, device, image_core_name, target + # FIXME replace with ruamel.yaml + import yaml + with open(args.grc_config, encoding="utf-8") as grc_file: config = yaml.load(grc_file) logging.info("Converting GNU Radio Companion file to image builder format") @@ -342,16 +277,7 @@ def main(): :return: exit code """ args = setup_parser().parse_args() - use_color = (args.color == "always") or \ - (args.color == "auto" and sys.__stdout__.isatty() and sys.__stderr__.isatty()) - handler = logging.StreamHandler() - handler.setFormatter(ColorFormatter() if use_color else SimpleFormatter()) - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - logger.addHandler(handler) - - if args.log_level is not None: - logging.root.setLevel(args.log_level.upper()) + rfnoc_log.init_logging(args.color, args.log_level) config, source, device, image_core_name, target = image_config(args) source_hash = hashlib.sha256() diff --git a/host/utils/rfnoc_modtool.py b/host/utils/rfnoc_modtool.py new file mode 100644 index 000000000..ee0bf2dac --- /dev/null +++ b/host/utils/rfnoc_modtool.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""RFNoC Modtool: The tool to create and manipulate RFNoC OOT modules. + +Copyright 2024 Ettus Research, A National Instrument Brand + +SPDX-License-Identifier: GPL-3.0-or-later +""" + +import os +import sys + +from uhd.rfnoc_utils import rfnoc_modtool + + +def get_pkg_dir(): + """Return package data directory. + + This is the main UHD installation path for package data (e.g., /usr/share/uhd). + """ + return os.path.normpath("@CONFIG_PATH@") + + +if __name__ == "__main__": + sys.exit(rfnoc_modtool.main(get_pkg_dir()))