From 8ac93759b1dd3938134b4145520d96384e314880 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Fri, 8 Mar 2024 13:18:32 +0100 Subject: [PATCH] Support arbitrary waveform viewer --- docs/cli.rst | 6 +-- docs/news.d/1002.feature.rst | 11 +++++ docs/py/opts.rst | 24 ++++++++++- vunit/sim_if/ghdl.py | 82 +++++++++++++++++++++++++++--------- vunit/sim_if/nvc.py | 77 +++++++++++++++++++++++++++------ 5 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 docs/news.d/1002.feature.rst diff --git a/docs/cli.rst b/docs/cli.rst index 6e94f1c3c..ecaa351d8 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -166,9 +166,9 @@ VUnit automatically detects which simulators are available on the ``PATH`` environment variable and by default selects the first one found. For people who have multiple simulators installed the ``VUNIT_SIMULATOR`` environment variable can be set to one of -``activehdl``, ``rivierapro``, ``ghdl`` or ``modelsim`` to specify -which simulator to use. ``modelsim`` is used for both ModelSim and -Questa as VUnit handles these simulators identically. +``activehdl``, ``rivierapro``, ``ghdl``, ``nvc```, or ``modelsim`` to +specify which simulator to use. ``modelsim`` is used for both ModelSim +and Questa as VUnit handles these simulators identically. In addition to VUnit scanning the ``PATH`` the simulator executable path can be explicitly configured by setting a diff --git a/docs/news.d/1002.feature.rst b/docs/news.d/1002.feature.rst new file mode 100644 index 000000000..17579d2a8 --- /dev/null +++ b/docs/news.d/1002.feature.rst @@ -0,0 +1,11 @@ +[GHDL/NVC] Arbitrary waveform viewers are now supported by passing the `--viewer` +command line arugment. As a consequence, ``ghdl.gtkwave_script.gui`` and +``nvc.gtkwave_script.gui`` are deprecated in favour of ``ghdl.viewer_script.gui`` +and ``nvc.viewer_script.gui``, respectively. The ``--gtkwave-args`` and +``--gtkwave-fmt`` command line argument is deprecated in favour of ``--viewer-args`` +and ``--viewer-fmt``, respectively. ``ghdl.viewer.gui`` and ``nvc.viewer.gui`` can +be used to set the preferred viewer from the run-file. Finally, if the requested +viewer does not exists, ``gtkwave`` or ``surfer`` is used, in that order. This +also means that VUnit uses ``surfer`` if ``gtkwave`` is not installed. + +[NVC] It is possible to get VCD waveform files by passing ``--viewer-fmt=vcd``. \ No newline at end of file diff --git a/docs/py/opts.rst b/docs/py/opts.rst index c782ab69d..192a05075 100644 --- a/docs/py/opts.rst +++ b/docs/py/opts.rst @@ -201,9 +201,15 @@ The following simulation options are known. With ``--elaborate``, execute ``ghdl -e`` instead of ``ghdl --elab-run --no-run``. Must be a boolean. -``ghdl.gtkwave_script.gui`` - A user defined TCL-file that is sourced after the design has been loaded in the GUI. +``ghdl.viewer.gui`` + Name of waveform viewer to use. The command line argument ``--viewer`` will have + precedence if provided. If neither is provided, ``gtkwave`` or ``surfer`` will be + used. + +``ghdl.viewer_script.gui`` + A user defined file that is sourced after the design has been loaded in the GUI. For example this can be used to configure the waveform viewer. Must be a string. + There are currently limitations in the HEAD revision of GTKWave that prevent the user from sourcing a list of scripts directly. The following is the current work around to sourcing multiple user TCL-files: @@ -225,3 +231,17 @@ The following simulation options are known. ``nvc.sim_flags`` Extra simulation flags passed to ``nvc -r``. Must be a list of strings. + +``nvc.viewer.gui`` + Name of waveform viewer to use. The command line argument ``--viewer`` will have + precedence if provided. If neither is provided, ``gtkwave`` or ``surfer`` will be + used. + +``nvc.viewer_script.gui`` + A user defined file that is sourced after the design has been loaded in the GUI. + For example this can be used to configure the waveform viewer. Must be a string. + + There are currently limitations in the HEAD revision of GTKWave that prevent the + user from sourcing a list of scripts directly. The following is the current work + around to sourcing multiple user TCL-files: + ``source `` diff --git a/vunit/sim_if/ghdl.py b/vunit/sim_if/ghdl.py index 6fd33dee0..a9909ead2 100644 --- a/vunit/sim_if/ghdl.py +++ b/vunit/sim_if/ghdl.py @@ -15,6 +15,7 @@ import shlex import re import shutil +import warnings from json import dump from sys import stdout # To avoid output catched in non-verbose mode from ..exceptions import CompileError @@ -43,7 +44,9 @@ class GHDLInterface(SimulatorInterface): # pylint: disable=too-many-instance-at sim_options = [ ListOfStringOption("ghdl.sim_flags"), ListOfStringOption("ghdl.elab_flags"), - StringOption("ghdl.gtkwave_script.gui"), + StringOption("ghdl.gtkwave_script.gui"), # Deprecated in v5.1.0 + StringOption("ghdl.viewer_script.gui"), + StringOption("ghdl.viewer.gui"), BooleanOption("ghdl.elab_e"), ] @@ -52,14 +55,16 @@ def add_arguments(parser): """ Add command line arguments """ - group = parser.add_argument_group("ghdl", description="GHDL specific flags") + group = parser.add_argument_group("ghdl/nvc", description="GHDL/NVC specific flags") group.add_argument( + "--viewer-fmt", "--gtkwave-fmt", choices=["vcd", "fst", "ghw"], default=None, - help="Save .vcd, .fst, or .ghw to open in gtkwave", + help="Save .vcd, .fst, or .ghw to open in waveform viewer. NVC does not support ghw.", ) - group.add_argument("--gtkwave-args", default="", help="Arguments to pass to gtkwave") + group.add_argument("--viewer-args", "--gtkwave-args", default="", help="Arguments to pass to waveform viewer") + group.add_argument("--viewer", default=None, help="Waveform viewer to use") @classmethod def from_args(cls, args, output_path, **kwargs): @@ -71,8 +76,9 @@ def from_args(cls, args, output_path, **kwargs): output_path=output_path, prefix=prefix, gui=args.gui, - gtkwave_fmt=args.gtkwave_fmt, - gtkwave_args=args.gtkwave_args, + viewer_fmt=args.viewer_fmt, + viewer_args=args.viewer_args, + viewer=args.viewer, backend=cls.determine_backend(prefix), ) @@ -88,20 +94,24 @@ def __init__( # pylint: disable=too-many-arguments output_path, prefix, gui=False, - gtkwave_fmt=None, - gtkwave_args="", + viewer_fmt=None, + viewer_args="", + viewer=None, backend="llvm", ): SimulatorInterface.__init__(self, output_path, gui) self._prefix = prefix self._project = None - if gui and (not self.find_executable("gtkwave")): - raise RuntimeError("Cannot find the gtkwave executable in the PATH environment variable. GUI not possible") - self._gui = gui - self._gtkwave_fmt = "ghw" if gui and gtkwave_fmt is None else gtkwave_fmt - self._gtkwave_args = gtkwave_args + self._viewer_fmt = "ghw" if gui and viewer_fmt is None else viewer_fmt + self._viewer_args = viewer_args + self._viewer = viewer + if gui: + self._gtkwave_available = self.find_executable("gtkwave") + self._surfer_available = self.find_executable("surfer") + self._viewer_available = False + self._backend = backend self._vhdl_standard = None self._coverage_test_dirs = set() # For gcov @@ -315,11 +325,11 @@ def _get_command( sim += ["--ieee-asserts=disable"] if wave_file: - if self._gtkwave_fmt == "ghw": + if self._viewer_fmt == "ghw": sim += [f"--wave={wave_file!s}"] - elif self._gtkwave_fmt == "vcd": + elif self._viewer_fmt == "vcd": sim += [f"--vcd={wave_file!s}"] - elif self._gtkwave_fmt == "fst": + elif self._viewer_fmt == "fst": sim += [f"--fst={wave_file!s}"] if not ghdl_e: @@ -355,8 +365,8 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl ghdl_e = elaborate_only and config.sim_options.get("ghdl.elab_e", False) - if self._gtkwave_fmt is not None: - data_file_name = str(Path(script_path) / f"wave.{self._gtkwave_fmt!s}") + if self._viewer_fmt is not None: + data_file_name = str(Path(script_path) / f"wave.{self._viewer_fmt!s}") if Path(data_file_name).exists(): remove(data_file_name) else: @@ -382,10 +392,16 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl except Process.NonZeroExitCode: status = False + if config.sim_options.get(self.name + ".gtkwave_script.gui", None): + warnings.warn(f"{self.name}.gtkwave_script.gui is deprecated and will be removed " + f"in a future version, use {self.name}.viewer_script.gui instead") + if self._gui and not elaborate_only: - cmd = ["gtkwave"] + shlex.split(self._gtkwave_args) + [data_file_name] + cmd = [self._get_viewer(config)] + shlex.split(self._viewer_args) + [data_file_name] - init_file = config.sim_options.get(self.name + ".gtkwave_script.gui", None) + init_file = config.sim_options.get( + self.name + ".viewer_script.gui", + config.sim_options.get(self.name + ".gtkwave_script.gui", None)) if init_file is not None: cmd += ["--script", str(Path(init_file).resolve())] @@ -394,6 +410,32 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl return status + def _get_viewer(self, config): + """ + Determine the waveform viewer to use. + + Falls back to gtkwave or surfer, in that order, if none is provided or the provided + cannot be found. + """ + viewer = self._viewer or config.sim_options.get(self.name + ".viewer.gui", None) + + if viewer is None and not self._gtkwave_available and not self._surfer_available: + raise RuntimeError("No viewer found. GUI not possible. Install GTKWave or Surfer.") + if not self._viewer_available and not self.find_executable(viewer): + if not self._gtkwave_available or not self._surfer_available: + raise RuntimeError( + f"Cannot find the {viewer} executable in the PATH environment variable. GUI not possible" + ) + if self._gtkwave_available: + warnings.warn(f"Cannot find {viewer}, using GTKWave instead.") + viewer = "gtkwave" + elif self._surfer_available: + warnings.warn(f"Cannot find {viewer}, using Surfer instead.") + viewer = "surfer" + else: + self._viewer_available = True + return viewer + def _compile_source_file(self, source_file, printer): """ Runs parent command for compilation, and moves any .gcno files to the compilation output diff --git a/vunit/sim_if/nvc.py b/vunit/sim_if/nvc.py index 8f64ecae3..7cb781d4e 100644 --- a/vunit/sim_if/nvc.py +++ b/vunit/sim_if/nvc.py @@ -14,6 +14,7 @@ import subprocess import shlex import re +import warnings from sys import stdout # To avoid output catched in non-verbose mode from ..exceptions import CompileError from ..ostools import Process @@ -44,7 +45,8 @@ class NVCInterface(SimulatorInterface): # pylint: disable=too-many-instance-att ListOfStringOption("nvc.sim_flags"), ListOfStringOption("nvc.elab_flags"), StringOption("nvc.heap_size"), - StringOption("nvc.gtkwave_script.gui"), + StringOption("nvc.viewer_script.gui"), + StringOption("nvc.viewer.gui"), ] @classmethod @@ -58,6 +60,9 @@ def from_args(cls, args, output_path, **kwargs): prefix=prefix, gui=args.gui, num_threads=args.num_threads, + viewer_fmt=args.viewer_fmt, + viewer_args=args.viewer_args, + viewer=args.viewer ) @classmethod @@ -73,20 +78,28 @@ def __init__( # pylint: disable=too-many-arguments prefix, num_threads, gui=False, - gtkwave_args="", + viewer_fmt=None, + viewer_args="", + viewer=None ): SimulatorInterface.__init__(self, output_path, gui) self._prefix = prefix self._project = None - if gui and (not self.find_executable("gtkwave")): - raise RuntimeError("Cannot find the gtkwave executable in the PATH environment variable. GUI not possible") - self._gui = gui - self._gtkwave_args = gtkwave_args + if viewer_fmt == "ghw": + warnings.warn("NVC does not support ghw, defaulting to fst") + viewer_fmt = None # Defaults to FST later + self._viewer_fmt = viewer_fmt + self._viewer_args = viewer_args + self._viewer = viewer + if gui: + self._gtkwave_available = self.find_executable("gtkwave") + self._surfer_available = self.find_executable("surfer") + self._viewer_available = False + self._vhdl_standard = None self._coverage_test_dirs = set() - (major, minor) = self.determine_version(prefix) self._supports_jit = major > 1 or (major == 1 and minor >= 9) @@ -241,7 +254,7 @@ def compile_vhdl_file_command(self, source_file): cmd += [source_file.name] return cmd - def simulate(self, output_path, test_suite_name, config, elaborate_only): # pylint: disable=too-many-branches + def simulate(self, output_path, test_suite_name, config, elaborate_only): # pylint: disable=too-many-branches, disable=too-many-statements """ Simulate with entity as top level using generics """ @@ -251,16 +264,17 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl if not script_path.exists(): makedirs(script_path) + libdir = self._project.get_library(config.library_name).directory + cmd = self._get_command(self._vhdl_standard, config.library_name, libdir) + if self._gui: - wave_file = script_path / (f"{config.entity_name}.fst") + fmt = self._viewer_fmt or "fst" + wave_file = script_path / (f"{config.entity_name}.{fmt}") if wave_file.exists(): remove(wave_file) else: wave_file = None - libdir = self._project.get_library(config.library_name).directory - cmd = self._get_command(self._vhdl_standard, config.library_name, libdir) - cmd += ["-H", config.sim_options.get("nvc.heap_size", "64m")] cmd += config.sim_options.get("nvc.global_flags", []) @@ -289,6 +303,9 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl if wave_file: cmd += [f"--wave={wave_file}"] + if self._viewer_fmt: + cmd += [f"--format={self._viewer_fmt}"] + print(" ".join([f"'{word}'" if " " in word else word for word in cmd])) status = True @@ -299,10 +316,16 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl except Process.NonZeroExitCode: status = False + if config.sim_options.get(self.name + ".gtkwave_script.gui", None): + warnings.warn(f"{self.name}.gtkwave_script.gui is deprecated and will be removed " + f"in a future version, use {self.name}.viewer_script.gui instead") + if self._gui and not elaborate_only: - cmd = ["gtkwave"] + shlex.split(self._gtkwave_args) + [str(wave_file)] + cmd = [self._get_viewer(config)] + shlex.split(self._viewer_args) + [str(wave_file)] - init_file = config.sim_options.get(self.name + ".gtkwave_script.gui", None) + init_file = config.sim_options.get( + self.name + ".viewer_script.gui", + config.sim_options.get(self.name + ".gtkwave_script.gui", None)) if init_file is not None: cmd += ["--script", str(Path(init_file).resolve())] @@ -310,3 +333,29 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl subprocess.call(cmd) return status + + def _get_viewer(self, config): + """ + Determine the waveform viewer to use. + + Falls back to gtkwave or surfer, in that order, if none is provided or the provided + cannot be found. + """ + viewer = self._viewer or config.sim_options.get(self.name + ".viewer.gui", None) + + if viewer is None and not self._gtkwave_available and not self._surfer_available: + raise RuntimeError("No viewer found. GUI not possible. Install GTKWave or Surfer.") + if not self._viewer_available and not self.find_executable(viewer): + if not self._gtkwave_available or not self._surfer_available: + raise RuntimeError( + f"Cannot find the {viewer} executable in the PATH environment variable. GUI not possible" + ) + if self._gtkwave_available: + warnings.warn(f"Cannot find {viewer}, using GTKWave instead.") + viewer = "gtkwave" + elif self._surfer_available: + warnings.warn(f"Cannot find {viewer}, using Surfer instead.") + viewer = "surfer" + else: + self._viewer_available = True + return viewer