Add rfnoc-modtool

This replaces rfnoc_modtool that was previously shipped with gr-ettus.
It is fully RFNoC/UHD4.x compatible. There is no shared code between
this version and the old one; this is a clean rewrite.

As of now, it lacks many of the features that were available with the
previous version, most importantly, the ability to generate GNU Radio
bindings.
This commit is contained in:
Martin Braun 2024-07-18 16:42:24 +02:00 committed by joergho
parent 1d8026bcfe
commit 1fb022a35f
42 changed files with 1026 additions and 260 deletions

View file

@ -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())

View file

@ -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"

View file

@ -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

View file

@ -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)

69
host/docs/rfnoc_modtool.1 Normal file
View file

@ -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.

View file

@ -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)

View file

@ -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")

View file

@ -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",
]
},
)

View file

@ -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())

View file

@ -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/<blockname>.yml.
If this file does not exist, the command will fail with an error.
copyright_holder:
name_or_flags: "--copyright-holder"
default: "<author>"
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'

View file

@ -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 }"

View file

@ -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())

View file

@ -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)

View file

@ -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) ${"\\"}
)
#-------------------------------------------------

View file

@ -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 ${"\\"}
)

View file

@ -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']}
//

View file

@ -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
//

View file

@ -0,0 +1,30 @@
//
// Copyright ${year} ${copyright_holder}
//
// ${license}
//
// Include our own header:
#include <rfnoc/${MODULE_NAME}/${blockname}_block_control.hpp>
// These two includes are the minimum required to implement a block:
#include <uhd/rfnoc/defaults.hpp>
#include <uhd/rfnoc/registry.hpp>
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")

View file

@ -0,0 +1,29 @@
//
// Copyright ${year} ${copyright_holder}
//
// ${license}
//
#pragma once
#include <uhd/config.hpp>
#include <uhd/rfnoc/noc_block_base.hpp>
#include <cstdint>
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

View file

@ -0,0 +1,21 @@
//
// Copyright ${year} ${copyright_holder}
//
// ${license}
//
#pragma once
#include <uhd/rfnoc/block_controller_factory_python.hpp>
#include <rfnoc/${MODULE_NAME}/${blockname}_block_control.hpp>
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))
;
}

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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 <http://www.gnu.org/licenses/>.
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()

View file

@ -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()))