VUnit / vunit

VUnit is a unit testing framework for VHDL/SystemVerilog
http://vunit.github.io/
Other
722 stars 261 forks source link

GUI Wave Tcl Script #615

Open nfrancque opened 4 years ago

nfrancque commented 4 years ago

I would like to improve the wave.do loading for vunit. This is all from a modelsim background, please let me know if there's something that would be different in another simulator.

1)

Right now I have one run.py with many testbenches, some of which are not at the same hierarchy and therefore need different wave files. I would prefer to keep this master run.py from being edited by individual block designers

So it would be nice to automatically search the folder containing the source file of the next testbench to run and load up wave.do if it exists - this is what modelsim does anyways, except only in current directory rather than directory of testbench. This also makes vunit more backwards compatible with standard vsim interface.

I would expect this to be a default overridden by the init_file of the python interface, but hopefully its usage would decrease quite a bit with this default.

2)

In combination with #455, it could be nice to generate a tcl proc like vunit_wave [path/to/hier/level] [-depth depth] [-save]

Which automatically add and group all modules at the specified hierarchy, recursively with the specified depth. Likely default to top level with depth 1, so it adds a group for each block in the top level. Depth of 0 would recurse completely.

-save should save the wave.do to the folder containing the source file of the currently option testbench.

I could see these both being combined so that the first time we load up the gui we run vunit_wave -save to save wave.do in the corresponding folder.

It would also be much better if the generated .do files had wildcards rather than explicitly referencing all. This is how modelsim generates them by default so if a signal changes you have to manually modify.

3) It would also be very nice if when running without the GUI it added these signals the log and generated a wlf in the output directory. I have frequently found myself loading up the GUI only to get the signals I want for a long sim. Would prefer to run on command line overnight and see a wlf of interesting signals in the morning.

Perhaps the generated wave.do could create a list of what is to be added and then if in GUI mode add to wave, else log.

kraigher commented 4 years ago

Sure sounds good. We try to keep the same feature set for both modelsim and riviera so it would be good if it can share implementation for both.

smenzel commented 4 years ago

I wrote a wave_init.do script that does something similar to your request: It doesn't load a wave from the location of the testbench but from a subfolder waves in the vunit folder. Here I keep the waves of all my testbenches and name them {testbench_name}_wave.do.

# get a lot of info about the loaded design
set data [ write report -tcl ]

# extract the name of the testbench
regexp {([A-Z_a-z0-9]*_tb).vhd} $data -> design_unit
puts "Found Design Unit: $design_unit"

# try to source wave file with appropriate name from waves folder
set wavename [pwd]/../../waves/${design_unit}
if { [file exists ${wavename}.do] } {
    source ${wavename}.do
} elseif { [file exists ${wavename}_wave.do] } {
    source ${wavename}_wave.do
} elseif { [file exists [pwd]/../../wave.do] } {
    # fallback: generic wave.do
    source [pwd]/../../wave.do
} else {
    puts "No Wave 'do' file found. If you save a wave as '${wavename}_wave.do' I will load it for you next time for this testbench."
}
nfrancque commented 4 years ago

Thanks for the the example.

I believe that feature is as simple as this:

https://github.com/VUnit/vunit/blob/b06474f7758580e2a40e936017975ca0a1b26073/vunit/sim_if/vsim_simulator_mixin.py#L204

to

        default_wave = join(config.tb_path, 'wave.do')
        init_files = config.sim_options.get(opt_name, [default_wave])

to support regular wave.do's as they are now. Modelsim just ignores add wave commands from a wave.do when run on the command line.

To get the logging part done, maybe we can set a tcl variable in _create_gui_script / _run_batch_file to indicate gui or not to the wave.do?

nfrancque commented 4 years ago

Just so it doesn't look like this is being abandoned - found out someone at my company already made something like this. Finding out if it can be partially released, otherwise will write from scratch.

nfrancque commented 4 years ago

@eine added you as a reviewer to #622 First pull request here so feel free to berate me if things are not as they should be :)

GlenNicholls commented 4 years ago

@nfrancque Attached are scripts for gtk and modelsim. Have you ever tried to find signals in activeHDL or RivieraPRO with tcl? I'm trying to translate these scripts to activeHDL and the problem I'm running into is that the following command only prints the signals, it does not return them like modelsim does:

set groups [ find -type signal -rec * ]
puts "all signals: $groups"

I then see:

Signals:
# /tb_FrequencyCounter/clk
# /tb_FrequencyCounter/freqCnt
# /tb_FrequencyCounter/numRandWords
# /tb_FrequencyCounter/reset
# /tb_FrequencyCounter/test_clk
# /tb_FrequencyCounter/uut/freqCnt
# /tb_FrequencyCounter/uut/freqCntLatch
# /tb_FrequencyCounter/uut/refStrb
# /tb_FrequencyCounter/uut/refStrbCnt
# /tb_FrequencyCounter/uut/gen_NotTestClkOnBUFG/refStrb_d1
# /tb_FrequencyCounter/uut/gen_NotTestClkOnBUFG/test_clk_d1
# all signals: 
# VUnit help: Design already loaded. Use run -all to run the test.

As you can see, the list that should be returned is empty. I've been playing around with lappend as well with no luck :/

A similar command in modelsim using set all_signals [ find signals * -r ] actually returns the list of values to tcl.

scripts.zip

nfrancque commented 4 years ago

I haven't actually used much besides questa. GHDL very passively in my personal time.

GlenNicholls commented 4 years ago

@nfrancque Okay, well I'm not sure if it's possible with activeHDL to search for signals and get them returned as a list to tcl. I opened a case on Aldec's forums, but from my experience, Aldec is absolutely awful with customer support so it might be a couple weeks. They have my case open, but haven't said anything yet :/

GlenNicholls commented 4 years ago

@nfrancque I've been forced to use activeHDL since all our other licenses are currently tied up and what a pinful debugging process it was to mimic my GTK and ModelSim scripts to work properly. For whatever reason, activeHDL doesn't allow you get the output from the find command within a proc. I opened an issue, but Aldec's customer support has always been awful, but even worse now with COVID-19. With that said, I was able to accomplish this task by referencing these commands globally. I have attached an updated folder with all the scripts.

scripts.zip

I remember I wrote a description somewhere about each file, but can't find it atm, so here it goes just so everything is in one place.

addWave.tcl is the "top level" script. I've been putting these in the same dir as my run.py and have VUnit source this when I use the GUI. Basically, I hated having so many copies of this script, hence #455. This file calls the top-level "public" proc's for each <simulator>AddWave.tcl (addWavesByDepth and addWavesByName). To do this, it finds the path to sourceSimulator.tcl by searching for the root of a Git repo. Those commands are commented since I wasn't using a repo when testing for activeHDL. The nice thing about VUnit is the scripts will always go to the same place, so this will not be needed.

sourceSimulator.tcl is just an intermediate step that checks the VUNIT_SIMULATOR env variable. Then, it simply sources the associated file so that all the proc's are within scope of addWave.tcl.

<msim | ghdl | aldec>AddWave.tcl contains all simulator specific commands and formatting to ensure signals and such get added properly. Between the 3 simulators I have support for, there is a lot of common functionality. One of the things I did for the aldecAddWave.tcl was write functions for specifically getting instances, signals, variables, and constants and a formatting function. I think it is definitely possible to re-factor each script to have some commonly named functions so that the shared code between each can avoid doing any funky manipulation to make maintaining so many files much easier. Then, the script with all our top-level functions simply sources the correct script and calls all the functions in a common fashion. The aldecAddWave.tcl should work for RivieraPRO, but I don't have a license to check this.

My understanding of TCL is not the greatest, but I know enough to get by. Are you or do you know anyone that is good with TCL that would be willing to answer questions so I could clean these up?

I should have time in the evenings to help out with developing this. Where are you at on this issue and what do you need help with?

In combination with #455, it could be nice to generate a tcl proc like vunit_wave [path/to/hier/level] [-depth depth] [-save]

Creating a TCL proc works for ModelSim and I think RivieraPRO, but I know that for some reason it does not work in activeHDL but I don't remember why. @LarsAsplund or @kraigher told me a while ago, I just don't remember what the specific reason was. My guess is it's because the activeHDL command window is not TCL, but their built-in MACRO commands, thus you can't source a TCL script and call a procedure from the command window. From my exposure to developing this script, I think they have a TCL interpreter that only runs when you call runscript -tcl <file> or do <file> if it recognizes the .tcl extension.

My vision for the issue I opened was to have the ability to add waves to a certain depth for specific tests within the run.py, but I'm not sure what youre thoughts are on this. I like the idea of doing this in the GUI, but in activeHDL it has to be a command line argument if it's not added to the run.py

tasgomes commented 4 years ago

Hi everyone,

I will share my approach to automatically add a waveform to a simulation in GUI mode using only VUnit (4.2.0) features. I use modelsim simulator.

In my setup, I have a subfolder called "scripts" where all the waveform files are stored. The name of each file has a convention, e.g. _wave.do. Then in the run.py script, I have the following command: # Add waveform automatically when running in GUI mode. for tb in lib.get_test_benches(): tb.set_sim_option("modelsim.init_file.gui","scripts/" + tb.name +"_wave.do")

Every time I run a simulation in GUI mode, the waveform will be automatically loaded for that particular testbench. If no waveform files exists, no error is thrown so you are free to create a new waveform file and store it under "scripts" folder with the proper name.

GlenNicholls commented 4 years ago

@tasgomes So this is an interesting approach. Do you have a different run.py for each tb or do you set the tb.name based on which testbench you're using? From the development I've done, I have a single run.py for an entire package/library, so I'm not quite seeing how this works if you have multiple testbenches being controlled from a single run.py script.

tasgomes commented 4 years ago

@GlenNicholls I am not sure if I got your question. I have a single run.py per project which contains several entities and each entity has its own testbench. So I have a single run.py that can run several different testbenches like you have I guess.

The name of each testbench has a convention, e.g. _tb.vhd. This way each waveform file will have a single name as well. With the commands above, if you run any of the testbenches in GUI mode then the correct waveform will be loaded. Sometimes if I have a lot of testcases in a testbench, I separated the testcases in two different files so I called them _1_tb.vhd and _2_tb.vhd. In this case, I have then two waveform files with the corresponding names.

nfrancque commented 4 years ago

That seems like a great idea for smaller projects. However,to me it seems necessary that the wave script sit with the testbench. From an IP perspective that scripts directory will be lost to time. It is a goal to keep information as close to the source code as possible.

nfrancque commented 4 years ago

@GlenNicholls thank you for providing scripts for the other simulators. I unfortunately have written a good amount of tcl and can look into that.

Can you describe what you'd like to see out of the run.py? Would that be in addition to GUI commands or separate?

tasgomes commented 4 years ago

@nfrancque the "scripts" folder is local to the "tb" folder where all the testbenches are so the waveform files are always together with their testbenches.

That is my setup:

--tb.......................................................Testbenches directory --------coverage..............................Simulation coverage output files --------models..................................Simulation models directory --------scripts...................................Simulation waveforms directory --------vunit_out..............................VUnit output directory

All the testbench files and the run.py file is in "tb" folder.

nfrancque commented 4 years ago

@tasgomes thanks for explaining, think I am still not clear though. You said above there is a project level run.py, but also that the run.py is stored in the tb folder. Is this tb folder global to the whole project?

tasgomes commented 4 years ago

@nfrancque What I meant was that a project has only a tb folder, thus a single run.py. Either the project is implementing an IP core or any other generic design it does not matter. In the "tb" folder I always have that folder structure, therefore for each testbench I have a specific waveform. I think this is also your case. If you add the command below to the "run.py" script then you always load the correct waveform if the waveform have the same name of the testbench plus "_wave.do".

# Add waveform automatically when running in GUI mode.
for tb in lib.get_test_benches():
tb.set_sim_option("modelsim.init_file.gui","scripts/" + tb.name +"_wave.do")

If you store it in the "tb/scripts" folder or in "tb" folder is up to you and you can adapt this in the command itself. You can also set the name of the waveform with the exact same name of the testbench and place it in the same folder:

# Add waveform automatically when running in GUI mode.
for tb in lib.get_test_benches():
tb.set_sim_option("modelsim.init_file.gui", tb.name +".do")

The property "tb.name" is a property returned by the function "get_test_benches()" from VUnit and it returns the name of the testbench that you declare it in the VHDL code, example:

entity dummy_tb is
    generic (
        runner_cfg : string
    );
end entity dummy_tb ;

Then you would name the waveform file for this testbench as "dummy_tb.do".

GlenNicholls commented 4 years ago

@tasgomes

thanks for explaining, think I am still not clear though. You said above there is a project level run.py, but also that the run.py is stored in the tb folder. Is this tb folder global to the whole project?

What @nfrancque said here is also what confused me, but your description is making a lot more sense to me now that I see we have gone about project structure similarly. My question was of the understanding that you had an entire project with a single run.py like so:

.project
--/top_level
----/ip_1
----/ip_2
--/tb
----/coverage
----/<other folders>
----run.py

From your description, I now see that you actually have this

.project
--/ip_1
----/src
----/tb
------/coverage
------/<other stuff>
------run.py
--/ip_2
----/src
----/tb
------/coverage
------/<other stuff>
------run.py

where ip_<1 | 2> are repos/libraries/other building blocks and project encapsulates the entire design for the FPGA/ASIC.

Now that we're on the same page about how your project looks, I see exactly what you're talking about. I guess a simplification is it might be cool if this was adopted and allow the user to instead write the following:

# Add waveform automatically when running in GUI mode.
for tb in lib.get_test_benches():
    tb.set_sim_option("modelsim.init_file.gui")

Where VUnit would automagically look for a <tb.name>.<do | tcl> in the same directory the running testbench is located. In this case set_sim_option could allow the name to be defaulted as "" and issue a warning to the user if it cannot find a file matching the same convention is uses to discover testbenches tb_<entityname>.<do | tcl> or <entityname>_tb.<do | tcl>. If the user has another convention, it obviously would allow the user to add the path directly like you have shown. I do agree with @nfrancque , I think that these waveform files should be in the same directory as the testbench. For things like formal verification, I can understand making more directories in large projects, but I would keep the do or TCL scripts in the same directory as the TBs if I was in charge of the project, but that's just me.

GlenNicholls commented 4 years ago

@nfrancque

Can you describe what you'd like to see out of the run.py? Would that be in addition to GUI commands or separate?

Yes, what I envisioned when I wrote #455 boils down to the following. Originally I just wanted a way to add arguments to set_sim_option("modelsim.init_file.gui", <file>.<do|tcl>, tcl_args=None) where tcl_args would be a list of arguments to pass to the TCL script when VUnit sources <file>. When you opened this issue, I realized the following would be possible since VUnit could take care of everything instead:

# this would be the definitions of the waveform adding functions
def add_waves_by_hierarchy_depth(depth=1)
    # code to source TCL scripts I attached to this issue
    # code to call TCL procedure addWavesByDepth with argument depth

def add_waves_from_list(waves=[])
    # could make waves a dict to allow user to specify options for adding waves and
    # VUnit could take care of adding the correct simulator specific add wave argument
    # code to source TCL scripts I attached to this issue
    # code to call TCL procedure addWavesByName with argument waves

Then from the run.py, you might have something like this:

for test in lib.test_bench("tb_ent1"):
    test.add_waves_by_hierarchy_depth(3)

for test in lib.test_bench("tb_ent2"):
    test.add_waves_by_hierarchy_depth(2)

for test in lib.test_bench("tb_ent3"):
    name = 'tb_ent3'
    signal_names = [
        join(name, 'clk'), # adds tb_ent3/clk
        join(name, 'uut', 'valid'), # adds tb_ent3/uut/valid
        join(name, 'uut', 'signal_*'), # adds tb_ent3/uut/signal_*
        join('**', '*_counter') # adds all *_counter's in the design
    ]
    test.add_waves_from_list(signal_names)

Thus, run.py could take care of adding different signal sets for each testbench (or even different test cases) and the TCL scripts I wrote automatically group them by hierarchy. The advantage of doing this in the run.py is you don't need to have any simulator specific scripts, everything lies in python and VUnit takes care of executing the correct commands. My use case is being able to switch simulators easily without needing to have a whole bunch of waveform files for each simulator, thus why I wrote those scripts in the first place.

For the function that adds by name, it could also take a dict which would allow the user to specify different add wave options like the radix, analog, etc. to display specific signals as different data types. Getting something working first would be goal 1, but this could be a neat idea.

GlenNicholls commented 4 years ago

Regarding your ideas for the second objective, I think these are really good as well:

In combination with #455, it could be nice to generate a tcl proc like vunit_wave [path/to/hier/level] [-depth depth] [-save]

I really like this idea as it would allow the user not to put anything in run.py, but execute everything from the GUI command line. However, vunit_wave [path/to/hier/level] does not make sense to me since the GUI is already open. In this case, you are already navigating the wave tree to find the signal you want, so typing it manually doesn't really make sense to me since you can just drag/drop. IMO, the usecase for this in the GUI would be to allow wildcards and let the TCL procedures take care of ensuring the / is correct (modelsim goes tb_name/uut/signal and Aldec does /tb_name/uut/signal.)

As I said about 2 weeks ago, executing your proposed vunit_wave in activeHDL is not possible as their command line in the GUI does not have a TCL interpreter to make a proc that the user can call. I.e. the interface you're used to in ModelSim (vunit_restart and such) do not exist. Because of this, the activeHDL interface would have to allow options from the command line when you run python run.py [args]. I'm not sure if the same limitation applies in RivieraPRO, but this is something to consider.

I'm of the mindset that from the command line, running python run.py -g -depth 4 would make sense, but adding signals by name or as a list does not. For simulators that don't allow the user to call a TCL process from the command line in their tool, the only way to add waves by name/list should be from run.py.

-save should save the wave.do to the folder containing the source file of the currently option testbench.

I like this idea a lot. One suggestion though would be to save it as <tb name>_wave.tcl as you could inadvertently overwrite an important wave.do for an older run in a different testbench. Adding an option to -save to specify the filename seems needed here, but the default should be <tb name>_wave.tcl if no filename is provided.

GlenNicholls commented 4 years ago

I got a version of kind of what I'm looking for hacked together using

def get_waves_file(self, depth=1, filename='addWave.tcl'):
        '''
        Creates a waves file to add signals to the waveform viewer down to a specified
        hierarchy depth.
        '''
        assert depth > 1, f'depth must be > 1, got {depth}'
        assert isinstance(depth, int), f'depth must be of type int, got {type(depth)}'
        add_waves_file = self.__SIM_SCRIPTS[self.__simulator].replace("\\", '/')
        tcl_cmd = [
            f'source {add_waves_file}',
            f'addWavesByDepth {depth}'
        ]
        self.__write_tcl_file(tcl_cmd, filename)
        return filename

Once I'm done working for the day, I'll checkout your progress and take a look at how to integrate this

GlenNicholls commented 4 years ago

@nfrancque I just had another idea for your first feature request. What're your thoughts about abstracting away vendor specific waveform files entirely by using JSON/YAML? For your third point I see the value since this would be nice. But for point 1, one thing I was thinking about was possibly having the capability to use JSON format like so:

{
  "global_settings": [<global settings for each add wave>]
  "groups": [
    "group1": [
      "tb/uut/clk": ["radix": "decimal", <other options>],
      "tb/uut/inst_1/reset": ["radix"...]
    ],
    "group2": [
// this would add tb/uut/<clk, reset, var1> to group2
      "path": "tb/uut"
      "signals": "clk"
      "variables": "var1"
    ],
  ]
}

This way, switching simulators doesn't make the generated .do useless. The supporting code that parses the JSON takes care of generating all the simulator specific commands. If you only use a single simulator then this doesn't provide much value.

I checked out your PR and I'm currently working on integrating my scripts.

GlenNicholls commented 4 years ago

Opened https://github.com/gtkwave/gtkwave/issues/10 to hopefully simplify the GTK interface