diff --git a/examples/vhdl/three_step_flow/run.py b/examples/vhdl/three_step_flow/run.py index d9f391f06..580d38abf 100644 --- a/examples/vhdl/three_step_flow/run.py +++ b/examples/vhdl/three_step_flow/run.py @@ -4,19 +4,28 @@ # # Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com +from argparse import Namespace from pathlib import Path -from vunit import VUnit +from vunit import VUnit, VUnitCLI +from os import environ + +cli = VUnitCLI() + +environ["VUNIT_SIMULATOR"] = "modelsim" vu = VUnit.from_argv() vu.add_vhdl_builtins() -lib = vu.add_library("lib") -lib.add_source_files(Path(__file__).parent / "*.vhd") +lib1 = vu.add_library("lib1") +lib1.add_source_files(Path(__file__).parent / "sub_module" / "*.vhd") + +lib2 = vu.add_library("lib2") +lib2.add_source_files(Path(__file__).parent / "*.vhd") -tb = lib.test_bench("tb_example") -test = tb.test("test 2") +tb = lib2.test_bench("tb_example") +test = tb.test("test") -for value in range(3): +for value in range(5): test.add_config(name=f"{value}", generics=dict(value=value)) vu.set_sim_option("modelsim.three_step_flow", True) diff --git a/examples/vhdl/three_step_flow/sub_module/tb_a.vhd b/examples/vhdl/three_step_flow/sub_module/tb_a.vhd new file mode 100644 index 000000000..b4f0748c8 --- /dev/null +++ b/examples/vhdl/three_step_flow/sub_module/tb_a.vhd @@ -0,0 +1,35 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com + +library vunit_lib; +context vunit_lib.vunit_context; + +entity tb_a is + generic ( + runner_cfg : string); +end entity; + +architecture tb of tb_a is +begin + main : process + function recurse(value : integer) return integer is + begin + if value <= 0 then + return 0; + elsif value mod 2 = 0 then + return 1 + recurse(value - 1); + else + return recurse(value - 1); + end if; + end; + begin + test_runner_setup(runner, runner_cfg); + + info("Running tb_a: " & to_string(recurse(17))); + + test_runner_cleanup(runner); + end process; +end architecture; diff --git a/examples/vhdl/three_step_flow/tb_b.vhd b/examples/vhdl/three_step_flow/tb_b.vhd new file mode 100644 index 000000000..61bd84976 --- /dev/null +++ b/examples/vhdl/three_step_flow/tb_b.vhd @@ -0,0 +1,35 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this file, +-- You can obtain one at http://mozilla.org/MPL/2.0/. +-- +-- Copyright (c) 2014-2024, Lars Asplund lars.anders.asplund@gmail.com + +library vunit_lib; +context vunit_lib.vunit_context; + +entity tb_b is + generic ( + runner_cfg : string); +end entity; + +architecture tb of tb_b is +begin + main : process + function recurse(value : integer) return integer is + begin + if value <= 0 then + return 0; + elsif value mod 2 = 0 then + return 1 + recurse(value - 1); + else + return recurse(value - 1); + end if; + end; + begin + test_runner_setup(runner, runner_cfg); + + info("Running tb_b: " & to_string(recurse(17))); + + test_runner_cleanup(runner); + end process; +end architecture; diff --git a/examples/vhdl/three_step_flow/tb_example.vhd b/examples/vhdl/three_step_flow/tb_example.vhd index 3a54f1149..eda9eae57 100644 --- a/examples/vhdl/three_step_flow/tb_example.vhd +++ b/examples/vhdl/three_step_flow/tb_example.vhd @@ -16,15 +16,23 @@ end entity; architecture tb of tb_example is begin main : process + function recurse(value : integer) return integer is + begin + if value <= 0 then + return 0; + else + return 1 + recurse(value - 1); + end if; + end; begin test_runner_setup(runner, runner_cfg); while test_suite loop - if run("test 1") then - elsif run("test 2") then + if run("test") then end if; info("Running " & running_test_case & " with generic value = " & to_string(value)); + info("Recurse = " & to_string(recurse(value))); end loop; test_runner_cleanup(runner); diff --git a/vunit/persistent_tcl_shell.py b/vunit/persistent_tcl_shell.py index 1a05beacc..14a395585 100644 --- a/vunit/persistent_tcl_shell.py +++ b/vunit/persistent_tcl_shell.py @@ -73,7 +73,7 @@ def read_var(self, varname): return consumer.var def read_bool(self, varname): - result = self.read_var(varname) + result = self.read_var(varname).lower() assert result in ("true", "false") return result == "true" diff --git a/vunit/sim_if/modelsim.py b/vunit/sim_if/modelsim.py index 4a80c8951..d40b24c8a 100644 --- a/vunit/sim_if/modelsim.py +++ b/vunit/sim_if/modelsim.py @@ -13,10 +13,11 @@ import logging from configparser import RawConfigParser from ..exceptions import CompileError -from ..ostools import Process, file_exists +from ..ostools import write_file, Process, file_exists from ..vhdl_standard import VHDL from . import SimulatorInterface, ListOfStringOption, StringOption, BooleanOption from .vsim_simulator_mixin import VsimSimulatorMixin, fix_path +from threading import Lock, Event LOGGER = logging.getLogger(__name__) @@ -100,6 +101,9 @@ def __init__(self, prefix, output_path, persistent=False, gui=False): self._coverage_files = set() assert not (persistent and gui) self._create_modelsim_ini() + self._optimized_designs = dict() + self._optimized_libraries = dict() + self._vopt_lock = Lock() def _create_modelsim_ini(self): """ @@ -211,7 +215,7 @@ def create_library(self, library_name, path, mapped_libraries=None): os.makedirs(apath) if not file_exists(path): - proc = Process([str(Path(self._prefix) / "vlib"), "-unix", path], env=self.get_env()) + proc = Process([str(Path(self._prefix) / "vlib"), "-unix", "-type", "directory", path], env=self.get_env()) proc.consume_output(callback=None) if library_name in mapped_libraries and mapped_libraries[library_name] == path: @@ -231,49 +235,57 @@ def _get_mapped_libraries(self): del libraries["others"] return libraries - def _create_load_function(self, test_suite_name, config, output_path): - """ - Create the vunit_load TCL function that runs the vsim command and loads the design - """ - - set_generic_str = " ".join( - ( - f"-g/{config.entity_name!s}/{name!s}={encode_generic_value(value)!s}" - for name, value in config.generics.items() - ) - ) - pli_str = " ".join(f"-pli {{{fix_path(name)!s}}}" for name in config.sim_options.get("pli", [])) + def _optimize(self, config, script_path): + three_step_flow = config.sim_options.get("modelsim.three_step_flow", False) if config.architecture_name is None: architecture_suffix = "" else: architecture_suffix = f"({config.architecture_name!s})" - if config.sim_options.get("enable_coverage", False): - coverage_file = str(Path(output_path) / "coverage.ucdb") - self._coverage_files.add(coverage_file) - coverage_save_cmd = ( - f"coverage save -onexit -testname {{{test_suite_name!s}}} -assert -directive " - f"-cvg -codeAll {{{fix_path(coverage_file)!s}}}" - ) - coverage_args = "-coverage" - else: - coverage_save_cmd = "" - coverage_args = "" - - three_step_flow = config.sim_options.get("modelsim.three_step_flow", False) - design_to_optimize = ( config.library_name + "." + config.entity_name + architecture_suffix if config.vhdl_configuration_name is None else config.library_name + "." + config.vhdl_configuration_name ) - if three_step_flow: + if not three_step_flow: + return design_to_optimize + + optimize = False + has_library_lock = False + with self._vopt_lock: + if not self._optimized_designs.get(design_to_optimize, None): + self._optimized_designs[design_to_optimize] = dict(simulation_target=None, vopt_event=Event()) + optimize = True + + if not self._optimized_libraries.get(config.library_name, None): + library_lock = Lock() + library_lock.acquire() + LOGGER.debug(f"Acquired library lock for {config.library_name} to optimize {design_to_optimize}") + has_library_lock = True + self._optimized_libraries[config.library_name] = library_lock + + simulation_target, vopt_event = self._optimized_designs[design_to_optimize].values() + + if optimize: + if not has_library_lock: + with self._vopt_lock: + library_lock = self._optimized_libraries[config.library_name] + + LOGGER.debug(f"Waiting for library lock for {config.library_name} to optimize {design_to_optimize}") + library_lock.acquire() + LOGGER.debug(f"Acquired library lock for {config.library_name} to optimize {design_to_optimize}") + + LOGGER.debug(f"Optimizing {design_to_optimize}") + # vopt has limitations on how the optimized design can be named. Simply removing # non-alphanumeric characters is a simple solution to that simulation_target = "opt_" + "".join(ch for ch in design_to_optimize if ch.isalnum()) + with self._vopt_lock: + self._optimized_designs[design_to_optimize]["simulation_target"] = simulation_target + vopt_flags = [ f"{design_to_optimize}", f"-work {{{config.library_name}}}", @@ -282,8 +294,128 @@ def _create_load_function(self, test_suite_name, config, output_path): f"-o {{{simulation_target}}}", self._vopt_extra_args(config), ] + + # There is a known bug in modelsim that prevents the -modelsimini flag from accepting + # a space in the path even with escaping, see issue #36 + if " " not in self._sim_cfg_file_name: + modelsimini_option = f"-modelsimini {fix_path(self._sim_cfg_file_name)!s}" + vopt_flags.insert(0, modelsimini_option) + + library_option = [] + for library in self._libraries: + library_option += ["-L", library.name] + + vopt_flags += library_option + + tcl = """ +proc vunit_optimize {{vopt_extra_args ""}} {""" + tcl += """ +set vopt_failed [catch {{ + eval vopt ${{vopt_extra_args}} {{{vopt_flags}}} +}}] + +if {{${{vopt_failed}}}} {{ + echo Command 'vopt ${{vopt_extra_args}} {vopt_flags}' failed + echo Bad flag from vopt_extra_args? + return true +}} + +return False +}} +""".format( + vopt_flags=" ".join(vopt_flags) + ) + + optimize_file_name = script_path / "optimize.do" + write_file(str(optimize_file_name), tcl) + + if self._persistent_shell is not None: + try: + self._persistent_shell.execute(f'source "{fix_path(str(optimize_file_name))!s}"') + self._persistent_shell.execute("set failed [vunit_optimize]") + if self._persistent_shell.read_bool("failed"): + status = False + else: + status = True + + except Process.NonZeroExitCode: + status = False + else: + tcl = f"""\ +onerror {{quit -code 1}} +source "{fix_path(str(optimize_file_name))!s}" +set failed [vunit_optimize] +if {{$failed}} {{quit -code 1}} +quit -code 0 +""" + batch_file_name = script_path / "batch_optimize.do" + write_file(str(batch_file_name), tcl) + + try: + args = [ + str(Path(self._prefix) / "vsim"), + "-c", + "-l", + str(script_path / "transcript"), + "-do", + f'source "{fix_path(str(batch_file_name))!s}"', + ] + + proc = Process(args, cwd=str(Path(self._sim_cfg_file_name).parent)) + proc.consume_output() + status = True + except Process.NonZeroExitCode: + status = False + + with self._vopt_lock: + if not status: + LOGGER.debug(f"Failed to optimize {design_to_optimize}.") + else: + LOGGER.debug(f"{design_to_optimize} optimization completed.") + self._optimized_designs[design_to_optimize]["vopt_event"].set() + self._optimized_libraries[config.library_name].release() + + if not status: + return False + + elif not simulation_target: + LOGGER.debug(f"Waiting for {design_to_optimize} to be optimized.") + # Do not completely block to allow for Ctrl+C + while not vopt_event.wait(0.05): + pass + LOGGER.debug(f"Done waiting for {design_to_optimize} to be optimized.") + with self._vopt_lock: + simulation_target = self._optimized_designs[design_to_optimize]["simulation_target"] + else: - simulation_target = design_to_optimize + LOGGER.debug(f"Reusing optimized {design_to_optimize}.") + + return simulation_target + + def _create_load_function(self, test_suite_name, config, output_path, simulation_target): + """ + Create the vunit_load TCL function that runs the vsim command and loads the design + """ + + set_generic_str = " ".join( + ( + f"-g/{config.entity_name!s}/{name!s}={encode_generic_value(value)!s}" + for name, value in config.generics.items() + ) + ) + pli_str = " ".join(f"-pli {{{fix_path(name)!s}}}" for name in config.sim_options.get("pli", [])) + + if config.sim_options.get("enable_coverage", False): + coverage_file = str(Path(output_path) / "coverage.ucdb") + self._coverage_files.add(coverage_file) + coverage_save_cmd = ( + f"coverage save -onexit -testname {{{test_suite_name!s}}} -assert -directive " + f"-cvg -codeAll {{{fix_path(coverage_file)!s}}}" + ) + coverage_args = "-coverage" + else: + coverage_save_cmd = "" + coverage_args = "" vsim_flags = [ f"-wlf {{{fix_path(str(Path(output_path) / 'vsim.wlf'))!s}}}", @@ -305,37 +437,17 @@ def _create_load_function(self, test_suite_name, config, output_path): modelsimini_option = f"-modelsimini {fix_path(self._sim_cfg_file_name)!s}" vsim_flags.insert(0, modelsimini_option) - if three_step_flow: - vopt_flags.insert(0, modelsimini_option) - library_option = [] for library in self._libraries: library_option += ["-L", library.name] vsim_flags += library_option - if three_step_flow: - vopt_flags += library_option - vhdl_assert_stop_level_mapping = {"warning": 1, "error": 2, "failure": 3} tcl = """ proc vunit_load {{vsim_extra_args ""} {vopt_extra_args ""}} {""" - if three_step_flow: - tcl += """ - set vopt_failed [catch {{ - eval vopt ${{vopt_extra_args}} {{{vopt_flags}}} - }}] - - if {{${{vopt_failed}}}} {{ - echo Command 'vopt ${{vopt_extra_args}} {vopt_flags}' failed - echo Bad flag from vopt_extra_args? - return true - }}""".format( - vopt_flags=" ".join(vopt_flags) - ) - tcl += """ set vsim_failed [catch {{ eval vsim ${{vsim_extra_args}} {{{vsim_flags}}} diff --git a/vunit/sim_if/vsim_simulator_mixin.py b/vunit/sim_if/vsim_simulator_mixin.py index 1ff8bb59c..ef6b549d8 100644 --- a/vunit/sim_if/vsim_simulator_mixin.py +++ b/vunit/sim_if/vsim_simulator_mixin.py @@ -121,7 +121,7 @@ def _create_restart_function(): }} """ - def _create_common_script(self, test_suite_name, config, script_path, output_path): + def _create_common_script(self, test_suite_name, config, script_path, output_path, simulation_target): """ Create tcl script with functions common to interactive and batch modes """ @@ -164,7 +164,7 @@ def _create_common_script(self, test_suite_name, config, script_path, output_pat """ tcl += self._create_init_files_after_load(config) tcl += self._create_init_files_before_run(config) - tcl += self._create_load_function(test_suite_name, config, script_path) + tcl += self._create_load_function(test_suite_name, config, script_path, simulation_target) tcl += get_is_test_suite_done_tcl(get_result_file_name(output_path)) tcl += self._create_run_function() tcl += self._create_restart_function() @@ -307,6 +307,12 @@ def _run_persistent(self, common_file_name, load_only=False): except Process.NonZeroExitCode: return False + def _optimize(self, config, script_path): + """ + Return simulation target or False if optimization failed or None the optimization step isn't supported. + """ + return None + def simulate(self, output_path, test_suite_name, config, elaborate_only): """ Run a test bench @@ -317,9 +323,13 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): gui_file_name = script_path / "gui.do" batch_file_name = script_path / "batch.do" + simulation_target = self._optimize(config, script_path) + if simulation_target == False: + return False + write_file( str(common_file_name), - self._create_common_script(test_suite_name, config, script_path, output_path), + self._create_common_script(test_suite_name, config, script_path, output_path, simulation_target), ) write_file(str(gui_file_name), self._create_gui_script(str(common_file_name), config)) write_file(