# frozen_string_literal: true require_relative "rvvm/version" require_relative "rvvm/utils" require_relative "rvvm/templates" require "optparse" require "fileutils" require "json" # Top level module of the rvvm cli meta tool. # # Handles argument parsing, project creation, template file # generation and all interaction with Xilinx Vivado tools. # # Argument parsing runs on `require`, rest is run using run # # @example # require "rvvm" # # Rvvm.run # # @since 0.1.0 module Rvvm class Error < StandardError; end # Module instance variables to hold tempaltes and project config @templates = Templates.load @config_path = nil @config = nil @formatted_time = nil # Instance variables to hold parsed args and required arg symbol list @args = {} @required_args = %i[ version new module pkg itf svfile comp elab run all runsv dpi gui gencov covreport ] # Script argument parsing block. # Runs on loading the module using require. OptionParser.new do |args| current_time = Time.now @formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S") Crayons.init args.on("-h", "--help", "Shows this help") do puts "\nRVvM - Ruby Vivado Manager\n\n" puts "\tRVvM is a Ruby based tool to manage, compile, elaborate and simulate SystemVerilog and UVM based projects" puts "\tusing Xilinx Vivado xvlog, xelab, xrun and xsc.\n\n" puts args req_args = String.new @required_args.each do |arg| req_args << "--#{arg} " end puts "\nRequired args (at least one): #{req_args}\n\n" exit(0) end args.on("-v", "--version", "Displays RVvM gem version") do puts "\nRVvM version: #{VERSION}\n\n" exit(0) end args.on("-n", "--new ", "Creates a new project wit the prowided name under pwd") args.on("-m", "--module ", "Creates a SystemVerilog module template (default path: /design/src)") args.on("-p", "--pkg ", "Creates a SystemVerilog package template (default path: /design/pkg)") args.on("-i", "--itf ", "Creates a SystemVerilog interface template (default path: /design/itf)") args.on("-s", "--svfile ", "Creates a generic SystemVerilog file template (default path: /design/src)") args.on("--path ", "Specifies a path relative to when creating a file template") args.on("--here", "Specifies pwd as the path for template file creation.") args.on("-c", "--comp", "Compile SystemVerilog sources") args.on("-e", "--elab", "Elaborates project") args.on("-r", "--run", "Runs UVM simulation") args.on("-a", "--all", "Runs --comp, --dpi if dpi dpilib configured in rvvmconf, --elab, --run") args.on("--runsv", "Runs pure SystemVerilog/Verilog simulation (Ignores UVM related arguments and config options)") args.on("-d", "--dpi", "Compiles C/C++ sources to a shared lib to link into an elaborated snaphost using DPI-C") args.on("-u", "--gui", "Opens a dumped waveform in Xilinx Vivado GUI") args.on("-g", "--gencov", "Generates a functional coverage report") args.on("--covreport", "Opens the latest generated functional coverage report in Xilinx coverage dashboard") args.on("--complog ", "Specifies compilation log filename (overrides config)") args.on("--complist ", "Specifies compile list for compilation (overrides config)") args.on("--elablog ", "Specifies elaboration log filename (overrides config)") args.on("--timescale ", "Specifies timescale for testbench snapshot to be elaborated (overrides config)") args.on("--tbtop ", "Specifie testbench top module (overwrites config)") args.on("--tb ", "Specifies tesbench snapshot to be elaborated (overwrites config)") args.on("--customdpilib ", "Specifies a custom DPI-C library to link into a snapshot to be elaborated (overwrites config)") args.on("--simlog ", "Specifies simulation log name (overwrites config)") args.on("--test ", "Specifies UVM test to be run by --run (overwrites config)") args.on("-b", "--batch", "Run a batch of UVM tests from a test list (overwrites config)") args.on("--testlist \"\"", Array, "Specifies test list as a comma separated string for a batch of test run (overwrites config)") args.on("--verb ", "Specifies UVM verbosity (overwrites config)") args.on("--simtb ", "Specifies testbench snapshot for simulation (overwrites config)") args.on("--dpilist ", "Specifies source file list for DPI-C compilation (overwrites config)") args.on("-w", "--wave", "Activates waveform trace dump for elaboration and simulation (overwrites config)") args.on("--wavefile ", "Specifies waveform dump file to be openned in Vivado GUI") args.on("--prjconf ", "Specifies RVvM project config file if not using the one provided in the project") args.on("-l", "--dpilib", "Specifies a DPI-C shared library to be linked with a snapshot during elaboration") args.on("--ignore-errors", "Ignores Vivado tool and shell command errors and continues execution") do @args[:ignore_errors] = true end args.on("--debug", "Script debug") end.parse!(into: @args) # Simple argument check. # # @return [void] # # @since 0.8.0 def self.check_args if @args[:all] @args[:comp] = 1 @args[:elab] = 1 @args[:run] = 1 end # Check if a required arg is provided return unless Utils.all_nil?(@args, @required_args) puts "" Crayons.log_error("No required options provided!") puts "For more info use -h or --help\n\n" exit(1) end # Argument collision handling and additional settings for # batch simulation and dpi compilation. # # @return [void] # # @since 0.8.0 def self.handle_args @args[:dpi] = 1 if @args[:all] && @config[:dpi][:dpilib] == 1 if @args[:batch] || @config[:simulation][:batch] == 1 if @args[:test] puts "UVM test provided even though running in batch mode - option will be ignored..." @args[:test] = nil end elsif @args[:testList] puts "UVM test list provided without running in batch mode - option will be ignored..." @args[:testlist] = nil end @config[:elaboration][:customdpilib] = @args[:customdpilib] if @args[:customdpilib] end # Shell command with debug printout. # # If --debug arg provided instead of calling `system` # prints the input command. # # @param command [String] command to be called/printed # # @return [void] # # @since 0.8.0 def self.execute(tag, command) if @args[:debug] Crayons.spinner_start(tag) sleep(1) Crayons.spinner_log(command) Crayons.spinner_stop(nil, true) else Crayons.command(tag, command, ignore_errors: @args[:ignore_errors]) end end # Creates a new RVvM project template in pwd with a given name. # # @param name [String] project name # # @return [void] # # @since 0.8.0 def self.create_new_project(name) if File.exist?(name) print "\nDirectory #{name} already exists. Do you want to overwrite it? [y/n] " input = gets.chomp puts "" exit(0) unless input == "y" FileUtils.rm_rf(name) else puts "" end Crayons.spinner_start("Generating new project: #{name}...") FileUtils.mkdir(name) Dir.chdir(name) FileUtils.mkdir("rvvm") FileUtils.chdir("rvvm") FileUtils.mkdir("logs") Dir.chdir("logs") FileUtils.mkdir("comp") FileUtils.mkdir("elab") FileUtils.mkdir("sim") FileUtils.mkdir("dpi") Dir.chdir("../..") temp_conf = @templates[:rvvmconf][:conf] temp_conf[:prjname] = name temp_conf[:prjpath] = Dir.pwd Utils.gen_template(@templates[:rvvmconf], nil, temp_conf) temp_conf = @templates[:compilelist][:conf] temp_conf[:prjname] = name Utils.gen_template(@templates[:compilelist], "#{name}_compile_list.f", temp_conf) Utils.gen_template(@templates[:wfcfg]) Utils.gen_template(@templates[:dpilist]) FileUtils.mkdir("design") Dir.chdir("design") FileUtils.mkdir("pkg") FileUtils.mkdir("itf") FileUtils.mkdir("src") Dir.chdir("..") FileUtils.mkdir("verif") Dir.chdir("verif") FileUtils.mkdir("env") FileUtils.mkdir("env/agents") FileUtils.mkdir("env/top") FileUtils.mkdir("tb") FileUtils.mkdir("tb/src") FileUtils.mkdir("test") FileUtils.mkdir("test/seq") FileUtils.mkdir("test/src") Dir.chdir("..") temp_conf = @templates[:tbtop][:conf] temp_conf[:prjname] = name temp_conf[:PRJNAME] = name.upcase Utils.gen_template(@templates[:tbtop], "#{name}_tb_top.sv", temp_conf) Crayons.spinner_log(" ") Crayons.spinner_pause system("git init") system("git add .") system('git commit -am "Initial commit"') Crayons.spinner_resume Crayons.spinner_log("") Crayons.spinner_stop("Done!", true) Crayons.log_pass("\nRVvM: New project generated. ^^\n\n") exit(0) rescue StandardError => e Crayons.spinner_stop("Error!", false) if Crayons.spinner_running? Crayons.log_error("RVvM failed to create project...\n\n#{e.message}\n") exit(1) end # Loads and parses rvvmconf.json config file from # an RVvM project rvvm directory. # # @return [void] # # @since 0.8.0 def self.load_config puts "" Crayons.spinner_start("Loading RVvM project config...") @config_path = Utils.find_file_dir("rvvmconf.json", ".") json_file = File.read("#{@config_path}/rvvmconf.json") if @config_path unless json_file Crayons.log_error(" Failed to load config file!\n") Crayons.log_error(" Make sure you are inside an RVvM project.\n\n") exit(1) end @config = JSON.parse(json_file) if json_file if @config.instance_of?(Hash) @config = @config.transform_values do |v| v.transform_keys(&:to_sym) if v.instance_of?(Hash) end.transform_keys(&:to_sym) end Crayons.spinner_stop(nil, true) puts "" rescue JSON::ParserError => e Crayons.log_error(" Invalid config json!\n\n#{e.message}") puts"" exit(1) end # Navigates to project top (rvvm directory of the RVvM project). # # @return [void] # # @since 0.8.0 def self.prj_top Dir.chdir(@config_path) end # Compiles project SystemVerilog sources using xvlog. # # @return [void] # # @since 0.8.0 def self.compile cmd_args = @config[:compilation][:args].strip logname = @args[:complog] || File.join([@config[:project][:logDir], @config[:compilation][:logDir], @config[:compilation][:log]]) complist = @args[:compilelist] || @config[:compilation][:list] cmd = "xvlog -sv -f #{complist} -log #{logname} #{cmd_args}" execute("Compiling HDL sources...", cmd) end # Compiles C/C++ sources into a shared library to use during # elaboration and simulation using DPI-C. # # @return [void] # # @since 0.9.0 def self.dpi_c cmd_args = @config[:dpi][:args].strip dpilist = @args[:dpilist] || @config[:dpi][:list] cmd = "xsc -f #{dpilist} #{cmd_args}" execute("Building DPI-C library...", cmd) end # Elaborates project into a testbench snapshot using xelab. # # @return [void] # # @since 0.8.0 def self.elaborate cmd_args = @config[:elaboration][:args] cmd_args << " -sv_lib #{@config[:elaboration][:customdpilib]}" if @config[:elaboration][:customdpilib] != "" unless @config[:elaboration][:customdpilib] != "" cmd_args << " -sv_lib dpi" if @args[:dpi] || @args[:dpilib] || @config[:dpi][:dpilib] == 1 end cmd_args << " -debug wave" if @args[:wave] cmd_args.strip! logname = @args[:elablog] || File.join([@config[:project][:logDir], @config[:elaboration][:logDir], @config[:elaboration][:log]]) tb_top = @args[:tbtop] || @config[:elaboration][:tbTop] tb = @args[:tb] || @config[:elaboration][:tb] timescale = @args[:timescale] || @config[:elaboration][:timescale] cmd = "xelab #{tb_top} -relax -s #{tb} -timescale #{timescale} -log #{logname} #{cmd_args}" execute("Elaborating testbench #{tb}...", cmd) end # Runs UVM test simulation on an elaborated testbench snapshot using xrun. # # @return [void] # # @since 0.8.0 def self.run_sim cmd_args = @config[:simulation][:args] if @args[:wave] cmd_args << " --tclbatch wfcfg.tcl" else cmd_args << " -R" end cmd_args.strip! # Run simulaton on an array of tests. # If in not running in batch mode testlist is an array with a single test test = @args[:test] || @config[:simulation][:defTest] testlist = [test] testlist = @args[:testlist] || @config[:simulation][:testlist] if @args[:batch] || @config[:simulation][:batch] == 1 tb = @args[:simtb] || @args[:tb] || @config[:elaboration][:tb] testlist.each_with_index do |simtest, i| simtest.strip! logname = File.join([@config[:project][:logDir], @config[:simulation][:logDir], @config[:simulation][:log]]) logname = Utils.interpolate(logname, { testname: simtest }) verb = "UVM_#{@args[:verb] || @config[:simulation][:verbosity]}" cmd = "xsim #{tb} -log #{logname} -testplusarg \"UVM_VERBOSITY=#{verb}\" -testplusarg \"UVM_TESTNAME=#{simtest}\" #{cmd_args}" execute("Running UVM test #{i + 1}/#{testlist.size}: #{simtest}...", cmd) end end # Runs a pure SystemVerilog/Verilog simulation using xrun. # # @return [void] # # @since 0.9.0 def self.run_sv cmd_args = @config[:simulation][:args].strip! tb = @args[:simtb] || @args[:tb] || @config[:elaboration][:tb] logname = @args[:simlog] || File.join([@config[:project][:logDir], @config[:simulation][:logDir], "svsim.log"]) cmd = "xsim #{tb} -log #{logname} #{cmd_args}" execute("Running pure SV/V simulation...", cmd) end # Opens last generated waveform trace dump to inspect in Vivado GUI. # # @return [void] # # @since 0.9.0 def self.gui dump = @args[:wavefile] || "#{@config[:elaboration][:tb]}.wdb" cmd = "xsim --gui #{dump}" execute("Running Vivado GUI...", cmd) exit(0) end # Generates UVM test functional coverage report using xcrg. # # @return [void] # # @since 0.9.0 def self.coverage cmd = "xcrg -report_format html -dir xsim.covdb" execute("Generating UVM test functional coverage report...", cmd) end # Opens last generated functional coverage report in a HTML dashboard. # # @return [void] # # @since 0.9.0 def self.cov_report Dir.chdir("xcrg_func_cov_report") execute("Opening coverage dashboard...", "./dashboard.html") exit(0) end # Generates a SystemVerilog module/interface template. # # @param type [String] specifies modudle/itf # @param name [String] specifies module/itf name # # @return [void] # # @since 0.9.0 def self.create_module(name, type) conf = @templates[:module][:conf] conf[:module] = name conf[:date] = @formatted_time unless @args[:here] conf[:prjname] = @config[:project][:name] conf[:company] = @config[:project][:company] end conf[:type] = type @templates[:module][:file][:path] = "design/itf" if type == "itf" if @args[:here] path = "." else path = File.join([@config[:project][:path], @args[:path] || @templates[:module][:file][:path]]) end conf[:username] = Utils.git_userame || " " Crayons.spinner_start("Generating SV #{type} template...") Utils.gen_template(@templates[:module], "#{name}.sv", conf, path) Crayons.spinner_stop(nil, true) puts "" exit(0) end # Generates a SystemVerilog package template. # # @param name [String] specifies package name # # @return [void] # # @since 0.9.0 def self.create_pkg(name) conf = @templates[:package][:conf] conf[:package] = name conf[:PACKAGE] = name.upcase conf[:date] = @formatted_time unless @args[:here] conf[:prjname] = @config[:project][:name] conf[:company] = @config[:project][:company] end conf[:username] = Utils.git_userame || " " if @args[:here] path = "." else path = File.join([@config[:project][:path], @args[:path] || @templates[:package][:file][:path]]) end Crayons.spinner_start("Generating SV pkg template...") Utils.gen_template(@templates[:package], "#{name}.sv", conf, path) Crayons.spinner_stop(nil, true) puts "" exit(0) end # Generates a generic SystemVerilog template file. # # @param name [String] specifies template name # # @return [void] # # @since 0.9.0 def self.create_svfile(name) conf = @templates[:svfile][:conf] conf[:NAME] = name.upcase conf[:date] = @formatted_time unless @args[:here] conf[:prjname] = @config[:project][:name] || " " conf[:company] = @config[:project][:company] || " " end conf[:username] = Utils.git_userame || "" if @args[:here] path = "." else path = File.join([@config[:project][:path], @args[:path] || @templates[:module][:file][:path]]) end Crayons.spinner_start("Generating generic SV file template...") Utils.gen_template(@templates[:svfile], "#{name}.sv", conf, path) Crayons.spinner_stop(nil, true) puts "" exit(0) end # Runs rvvm calling its methods based on provided script args. # # @return [void] # # @since 0.8.0 def self.run create_new_project(@args[:new]) if @args[:new] check_args unless @args[:here] load_config handle_args end create_module(@args[:module], "module") if @args[:module] create_module(@args[:itf], "itf") if @args[:itf] create_pkg(@args[:pkg]) if @args[:pkg] create_svfile(@args[:svfile]) if @args[:svfile] prj_top compile if @args[:comp] dpi_c if @args[:dpi] elaborate if @args[:elab] run_sim if @args[:run] run_sv if @args[:runsv] gui if @args[:gui] coverage if @args[:gencov] cov_report if @args[:covreport] exit(0) end end