From 6e29332290614dafcf6f461785635f26cb33094a Mon Sep 17 00:00:00 2001 From: nielshaandbaek Date: Fri, 14 Jan 2022 20:47:30 +0100 Subject: [PATCH] Improved HDL simulation flow for Questa (#47) * Updated to use vlog, vcom, vopt and vsim directly with dependency management instead of scripts. * Updated to support testcases. * Updated to make the testcase generator and test cases work. * Updated to compile all parameter sets enabling run-time selection and updated to merge coverage * Add VsimFlags option. * Fixed whitespace. Updated to be able to simulate all parameter sets. * Fixing formatting using 'go fmt' and converted tabs to spaces. * Made functions not needed elsewhere private. * Updated how coverage data is generated; added flag to generate HTML coverage database. * Added Vivado xsim support. * Updated formatting. * Added descriptions to variables. * Removed check for valid parameter set as it doesn't work when all targets are evaluated. * Changed name of optimized design to avoid name collisions. * Updated to run final blocks too. * Updated to also add a dependency on the testcase generator if available. * Fixed problem where a compilation error did not make the build process fail. Also made naming of directories a bit more sane. * Updated based on review feedback to accept comma-separated testcases and params. Improved dbt build output to reflect command usage. Outputs coverage database location when enabled. Co-authored-by: Niels --- RULES/hdl/hdl.go | 1 + RULES/hdl/questa.go | 596 ++++++++++++++++++++++++++++++++++++----- RULES/hdl/simulator.go | 173 ++++++++++-- RULES/hdl/utils.go | 19 +- RULES/hdl/xsim.go | 500 ++++++++++++++++++++++++++++++---- 5 files changed, 1144 insertions(+), 145 deletions(-) diff --git a/RULES/hdl/hdl.go b/RULES/hdl/hdl.go index 887a635..2fcc617 100644 --- a/RULES/hdl/hdl.go +++ b/RULES/hdl/hdl.go @@ -25,6 +25,7 @@ type Ip interface { } type Library struct { + Lib string Srcs []core.Path DataFiles []core.Path IpDeps []Ip diff --git a/RULES/hdl/questa.go b/RULES/hdl/questa.go index e4bd64f..74dd23b 100644 --- a/RULES/hdl/questa.go +++ b/RULES/hdl/questa.go @@ -2,75 +2,547 @@ package hdl import ( "fmt" + "log" + "os" + "path" + "strings" "dbt-rules/RULES/core" - "dbt-rules/hdl" ) -type QuestaSimScriptParams struct { - Name string - PartName string - BoardName string - OutDir core.Path - OutScript core.Path - OutSimScript core.Path - IncDir core.Path - Srcs []core.Path - Ips []core.Path - Libs []string - LibDir string - Verbose bool +// VlogFlags enables the user to specify additional flags for the 'vlog' command. +var VlogFlags = core.StringFlag{ + Name: "questa-vlog-flags", + DefaultFn: func() string { + return "" + }, + Description: "Extra flags for the vlog command", +}.Register() + +// VcomFlags enables the user to specify additional flags for the 'vcom' command. +var VcomFlags = core.StringFlag{ + Name: "questa-vcom-flags", + DefaultFn: func() string { + return "" + }, + Description: "Extra flags for the vcom command", +}.Register() + +// VsimFlags enables the user to specify additional flags for the 'vsim' command. +var VsimFlags = core.StringFlag{ + Name: "questa-vsim-flags", + DefaultFn: func() string { + return "" + }, + Description: "Extra flags for the vsim command", +}.Register() + +// Lint enables additional linting information during compilation. +var Lint = core.BoolFlag{ + Name: "questa-lint", + DefaultFn: func() bool { + return false + }, + Description: "Enable additional lint information during compilation", +}.Register() + +// Access enables the user to control the accessibility in the compiled design for +// debugging purposes. +var Access = core.StringFlag{ + Name: "questa-access", + DefaultFn: func() string { + return "rna" + }, + Description: "Control access to simulation objects for debugging purposes", +}.Register() + +// Coverage enables the user to run the simulation with code coverage. +var Coverage = core.BoolFlag{ + Name: "questa-coverage", + DefaultFn: func() bool { + return false + }, + Description: "Enable code-coverage database generation", +}.Register() + +// Target returns the optimization target name defined for this rule. +func (rule Simulation) Target() string { + if Coverage.Value() { + return "vopt_cover" + } else { + return "vopt" + } +} + +// Instance returns the instance name of the rule based on the Top and the DUT +// fields. +func (rule Simulation) Instance() string { + // Defaults + top := "board" + dut := "u_dut" + + // Pick top from rule + if rule.Top != "" { + top = rule.Top + } + + // Pick DUT from rule + if rule.Dut != "" { + dut = rule.Dut + } + + return "/" + top + "/" + dut +} + +// rules holds a map of all defined rules to prevent defining the same rule +// multiple times. +var rules = make(map[string]bool) + +// common_flags holds common flags used for the 'vlog', 'vcom', and 'vopt' commands. +const common_flags = "-nologo -quiet" + +// compileSrcs compiles a list of sources using the specified context ctx, rule, +// dependencies and include paths. It returns the resulting dependencies and include paths +// that result from compiling the source files. +func compileSrcs(ctx core.Context, rule Simulation, + deps []core.Path, incs []core.Path, srcs []core.Path) ([]core.Path, []core.Path) { + for _, src := range srcs { + // We handle header files separately from other source files + if IsHeader(src.String()) { + incs = append(incs, src) + } else if IsRtl(src.String()) { + // log will point to the log file to be generated when compiling the code + log := rule.Path().WithSuffix("/" + src.Relative() + ".log") + + // If we already have a rule for this file, skip it. + if rules[log.String()] { + continue + } + + // Holds common flags for both 'vlog' and 'vcom' commands + cmd := fmt.Sprintf("%s +acc=%s -work %s -l %s", common_flags, Access.Value(), rule.Lib(), log.String()) + + // tool will point to the tool to execute (also used for logging below) + var tool string + if IsVerilog(src.String()) { + tool = "vlog" + cmd = cmd + " " + VlogFlags.Value() + cmd = cmd + " -suppress 2583 -svinputport=net" + cmd = cmd + fmt.Sprintf(" +incdir+%s", core.SourcePath("").String()) + for _, inc := range incs { + cmd = cmd + fmt.Sprintf(" +incdir+%s", path.Dir(inc.Absolute())) + } + } else if IsVhdl(src.String()) { + tool = "vcom" + cmd = cmd + " " + VcomFlags.Value() + } + + if Lint.Value() { + cmd = cmd + " -lint" + } + + // Create plain compilation command + cmd = tool + " " + cmd + " " + src.String() + + // Remove the log file if the command fails to ensure we can recompile it + cmd = cmd + " || { rm " + log.String() + " && exit 1; }" + + // Add the compilation command as a build step with the log file as the + // generated output + ctx.AddBuildStep(core.BuildStep{ + Out: log, + Ins: append(deps, src), + Cmd: cmd, + Descr: fmt.Sprintf("%s: %s", tool, src.Relative()), + }) + + // Add the log file to the dependencies of the next files + deps = append(deps, log) + + // Note down the created rule + rules[log.String()] = true + } + } + + return deps, incs } -type SimulationQuesta struct { - Name string - Srcs []core.Path - Ips []Ip - Libs []string - Verbose bool +// compileIp compiles the IP dependencies and the source files and an IP. +func compileIp(ctx core.Context, rule Simulation, ip Ip, + deps []core.Path, incs []core.Path) ([]core.Path, []core.Path) { + for _, sub_ip := range ip.Ips() { + deps, incs = compileIp(ctx, rule, sub_ip, deps, incs) + } + deps, incs = compileSrcs(ctx, rule, deps, incs, ip.Sources()) + + return deps, incs +} + +// compile compiles the IP dependencies and source files of a simulation rule. +func compile(ctx core.Context, rule Simulation) []core.Path { + incs := []core.Path{} + deps := []core.Path{} + + for _, ip := range rule.Ips { + deps, incs = compileIp(ctx, rule, ip, deps, incs) + } + deps, incs = compileSrcs(ctx, rule, deps, incs, rule.Srcs) + + return deps } -func (rule SimulationQuesta) Build(ctx core.Context) { - outDir := ctx.Cwd().WithSuffix("/" + rule.Name) - outScript := outDir.WithSuffix(".sh") - outSimScript := outDir.WithSuffix(".questa.do") - - ins := []core.Path{} - srcs := []core.Path{} - ips := []core.Path{} - - for _, ip := range FlattenIpGraph(rule.Ips) { - for _, src := range ip.Sources() { - if IsSimulationArchive(src.String()) { - ips = append(ips, src) - } else if IsRtl(src.String()) { - srcs = append(srcs, src) +// optimize creates and optimized version of the design optionally including +// coverage recording functionality. The optimized design unit can then conveniently +// be simulated using 'vsim'. +func optimize(ctx core.Context, rule Simulation, deps []core.Path) { + top := "board" + if rule.Top != "" { + top = rule.Top + } + + cover_flag := "" + log_file_suffix := "vopt.log" + if Coverage.Value() { + cover_flag = "+cover" + log_file_suffix = "vopt_cover.log" + } + + log_files := []core.OutPath{} + targets := []string{} + params := []string{} + if rule.Params != nil { + for key, _ := range rule.Params { + log_files = append(log_files, rule.Path().WithSuffix("/"+key+"_"+log_file_suffix)) + targets = append(targets, key + "_" + rule.Target()) + params = append(params, key) + } + } else { + log_files = append(log_files, rule.Path().WithSuffix("/"+log_file_suffix)) + targets = append(targets, rule.Target()) + params = append(params, "") + } + + for i := range log_files { + log_file := log_files[i] + target := targets[i] + param_set := params[i] + + // Skip if we already have a rule + if rules[log_file.String()] { + return + } + + cmd := fmt.Sprintf("vopt %s %s +acc=%s -l %s -work %s %s -o %s", + common_flags, cover_flag, Access.Value(), + log_file.String(), rule.Lib(), top, target) + + // Set up parameters + if param_set != "" { + // Check that the parameters exist + if params, ok := rule.Params[param_set]; ok { + // Add parameters for all generics + for param, value := range params { + cmd = fmt.Sprintf("%s -G %s=%s", cmd, param, value) + } } - ins = append(ins, src) - } - } - srcs = append(srcs, rule.Srcs...) - ins = append(ins, rule.Srcs...) - - data := QuestaSimScriptParams{ - PartName: PartName.Value(), - BoardName: BoardName.Value(), - Name: rule.Name, - OutDir: outDir, - OutScript: outScript, - OutSimScript: outSimScript, - IncDir: core.SourcePath(""), - Srcs: srcs, - Ips: ips, - Libs: rule.Libs, - LibDir: SimulatorLibDir.Value(), - Verbose: rule.Verbose, - } - - ctx.AddBuildStep(core.BuildStep{ - Outs: []core.OutPath{outDir, outScript, outSimScript}, - Ins: ins, - Script: core.CompileTemplateFile(hdl.QuestaSimScriptTmpl.String(), data), - Descr: fmt.Sprintf("Generating Questa simulation %s", outScript.Relative()), - }) + } + + if rule.TestCaseGenerator != nil { + deps = append(deps, rule.TestCaseGenerator) + } + + // Add the rule to run 'vopt'. + ctx.AddBuildStep(core.BuildStep{ + Out: log_file, + Ins: deps, + Cmd: cmd, + Descr: fmt.Sprintf("vopt: %s %s", rule.Lib()+"."+top, target), + }) + + // Note that we created this rule + rules[log_file.String()] = true + } +} + +// BuildQuesta will compile and optimize the source and IPs associated with the given +// rule. +func BuildQuesta(ctx core.Context, rule Simulation) { + // compile the code + deps := compile(ctx, rule) + + // optimize the code + optimize(ctx, rule, deps) +} + +// verbosityLevelToFlag takes a verbosity level of none, low, medium or high and +// converts it to the corresponding DVM_ level. +func verbosityLevelToFlag(level string) (string, bool) { + var verbosity_flag string + var print_output bool + switch level { + case "none": + verbosity_flag = " +verbosity=DVM_VERB_NONE" + print_output = false + case "low": + verbosity_flag = " +verbosity=DVM_VERB_LOW" + print_output = true + case "medium": + verbosity_flag = " +verbosity=DVM_VERB_MED" + print_output = true + case "high": + verbosity_flag = " +verbosity=DVM_VERB_HIGH" + print_output = true + default: + log.Fatal(fmt.Sprintf("invalid verbosity flag '%s', only 'low', 'medium',"+ + " 'high' or 'none' allowed!", level)) + } + + return verbosity_flag, print_output +} + +// questaCmd will create a command for starting 'vsim' on the compiled and optimized design with flags +// set in accordance with what is specified on the command line. +func questaCmd(rule Simulation, args []string, gui bool, testcase string, params string) string { + // Prefix the vsim command with this + cmd_preamble := "" + + // Default log file + log_file_suffix := "vsim.log" + if testcase != "" { + log_file_suffix = testcase + "_" + log_file_suffix + } + if params != "" { + log_file_suffix = params + "_" + log_file_suffix + } + log_file := rule.Path().WithSuffix("/" + log_file_suffix) + + // Default flag values + vsim_flags := " -onfinish final -l " + log_file.String() + seed_flag := " -sv_seed random" + verbosity_flag := " +verbosity=DVM_VERB_NONE" + mode_flag := " -batch -quiet" + plusargs_flag := "" + + // Default database name for simulation + var target string + if len(params) > 0 { + target = params + "_" + rule.Target() + } else { + target = rule.Target() + } + + // Enable coverage in simulator + coverage_flag := "" + if Coverage.Value() { + coverage_flag = " -coverage" + } + + // Determine the names of the coverage databases, this one will hold merged + // data from multiple testcases + main_coverage_db := rule.Name + + // This will be the name of the database created by the current run + coverage_db := rule.Name + + // Collect do-files here + var do_flags []string + + // Turn off output unless verbosity is activated + print_output := false + + // Parse additional arguments + for _, arg := range args { + if strings.HasPrefix(arg, "-seed=") { + // Define simulator seed + var seed int64 + if _, err := fmt.Sscanf(arg, "-seed=%d", &seed); err == nil { + seed_flag = fmt.Sprintf(" -sv_seed %d", seed) + } else { + log.Fatal("-seed expects an integer argument!") + } + } else if strings.HasPrefix(arg, "-verbosity=") { + // Define verbosity level + var level string + if _, err := fmt.Sscanf(arg, "-verbosity=%s", &level); err == nil { + verbosity_flag, print_output = verbosityLevelToFlag(level) + } else { + log.Fatal("-verbosity expects an argument of 'low', 'medium', 'high' or 'none'!") + } + } else if strings.HasPrefix(arg, "+") { + // All '+' arguments go directly to the simulator + plusargs_flag = plusargs_flag + " " + arg + } + } + + // Create optional command preamble + cmd_preamble, testcase = Preamble(rule, testcase) + + cmd_echo := "" + if rule.Params != nil && params != "" { + // Update coverage database name based on parameters. Since we cannot merge + // different parameter sets, we have to make a dedicated main database + // for this parameter set. + main_coverage_db = main_coverage_db + "_" + params + coverage_db = coverage_db + "_" + params + cmd_echo = "Testcase " + params + + // Update with testcase if specified + if testcase != "" { + coverage_db = coverage_db + "_" + testcase + cmd_echo = cmd_echo + "/" + testcase + ":" + testcase = params + "_" + testcase + } else { + cmd_echo = cmd_echo + ":" + testcase = params + } + } else { + // Update coverage database name with testcase alone, main database stays + // the same + if testcase != "" { + coverage_db = coverage_db + "_" + testcase + cmd_echo = "Testcase " + testcase + ":" + } else { + testcase = "default" + } + } + + cmd_postamble := "" + cmd_pass := "PASS" + if gui { + mode_flag = " -gui" + if rule.WaveformInit != nil { + do_flags = append(do_flags, rule.WaveformInit.String()) + } + } else { + if !print_output { + mode_flag = mode_flag + " -nostdout" + } + do_flags = append(do_flags, "\"run -all\"") + if Coverage.Value() { + do_flags = append(do_flags, fmt.Sprintf("\"coverage save -assert"+ + " -cvg -codeAll -testname %s"+ + " %s.ucdb\"", + testcase, coverage_db)) + do_flags = append(do_flags, + fmt.Sprintf("\"vcover merge -testassociated -out %s.ucdb %s.ucdb %s.ucdb\"", + main_coverage_db, main_coverage_db, coverage_db)) + do_flags = append(do_flags, + fmt.Sprintf("\"vcover report -html -output %s_covhtml -testdetails -details -assert"+ + " -cvg -codeAll %s.ucdb\"", main_coverage_db, main_coverage_db)) + cmd_pass = cmd_pass + fmt.Sprintf(" Coverage:$$(pwd)/%s.ucdb", main_coverage_db) + } + do_flags = append(do_flags, "\"quit -code [coverage attribute -name TESTSTATUS -concise]\"") + cmd_newline := ":" + if cmd_echo != "" { + cmd_newline = "echo" + } + cmd_postamble = fmt.Sprintf("|| { %s; cat %s; exit 1; }", cmd_newline, log_file.String()) + } + + vsim_flags = vsim_flags + mode_flag + seed_flag + coverage_flag + + verbosity_flag + plusargs_flag + VsimFlags.Value() + + for _, do_flag := range do_flags { + vsim_flags = vsim_flags + " -do " + do_flag + } + + cmd := fmt.Sprintf("{ echo -n %s && vsim %s -work %s %s && echo %s; }", cmd_echo, vsim_flags, rule.Lib(), target, cmd_pass) + if cmd_preamble == "" { + cmd = cmd + " " + cmd_postamble + } else { + cmd = "{ { " + cmd_preamble + " } && " + cmd + " } " + cmd_postamble + } + + // Wrap command in another layer of {} to enable chaining + cmd = "{ " + cmd + " }" + + return cmd +} + +// simulateQuesta will create a command to start 'vsim' on the compiled design +// with flags set in accordance with what is specified on the command line. It will +// optionally build a chain of commands in case the rule has parameters, but +// no parameters are specified on the command line +func simulateQuesta(rule Simulation, args []string, gui bool) string { + // Optional testcase goes here + testcases := []string{} + + // Optional parameter set goes here + params := []string{} + + // Parse additional arguments + for _, arg := range args { + if strings.HasPrefix(arg, "-testcases=") && rule.TestCaseGenerator != nil { + var testcases_arg string + if _, err := fmt.Sscanf(arg, "-testcases=%s", &testcases_arg); err != nil { + log.Fatal(fmt.Sprintf("-testcases expects a string argument!")) + } + testcases = append(testcases, strings.Split(testcases_arg, ",")...) + } else if strings.HasPrefix(arg, "-params=") && rule.Params != nil { + var params_arg string + if _, err := fmt.Sscanf(arg, "-params=%s", ¶ms_arg); err != nil { + log.Fatal(fmt.Sprintf("-params expects a string argument!")) + } else { + for _, param := range strings.Split(params_arg, ",") { + if _, ok := rule.Params[param]; ok { + params = append(params, param) + } + } + } + } + } + + // If no parameters have been specified, simulate them all + if rule.Params != nil && len(params) == 0 { + for key := range rule.Params { + params = append(params, key) + } + } else if len(params) == 0 { + params = append(params, "") + } + + // If no testcase has been specified, simulate them all + if rule.TestCaseGenerator != nil && rule.TestCasesDir != nil && len(testcases) == 0 { + // Loop through all defined testcases in directory + if items, err := os.ReadDir(rule.TestCasesDir.String()); err == nil { + for _, item := range items { + testcases = append(testcases, item.Name()) + } + } else { + log.Fatal(err) + } + } else if len(testcases) == 0 { + testcases = append(testcases, "") + } + + // Final command + cmd := "{ :; }" + + // Loop for all parameter sets + for i := range params { + // Loop for all test cases + for j := range testcases { + cmd = cmd + " && " + questaCmd(rule, args, gui, testcases[j], params[i]) + // Only one testcase allowed in GUI mode + if gui { + break + } + } + // Only one parameter set allowed in gui mode + if gui { + break + } + } + + return cmd +} + +// Run will build the design and run a simulation in GUI mode. +func RunQuesta(rule Simulation, args []string) string { + return simulateQuesta(rule, args, true) +} + +// Test will build the design and run a simulation in batch mode. +func TestQuesta(rule Simulation, args []string) string { + return simulateQuesta(rule, args, false) } diff --git a/RULES/hdl/simulator.go b/RULES/hdl/simulator.go index 03f0852..11e2c08 100644 --- a/RULES/hdl/simulator.go +++ b/RULES/hdl/simulator.go @@ -2,49 +2,174 @@ package hdl import ( "dbt-rules/RULES/core" + "fmt" + "log" + "os" + "errors" + "strings" + "path" ) var Simulator = core.StringFlag{ Name: "hdl-simulator", - Description: "HDL simulator to use when generating simulation targets", + Description: "Select HDL simulator", DefaultFn: func() string { - return "xsim" + return "questa" }, AllowedValues: []string{"xsim", "questa"}, }.Register() var SimulatorLibDir = core.StringFlag{ Name: "hdl-simulator-lib-dir", - Description: "Path to the HDL Simulator libraries", + Description: "Path to the HDL simulator libraries", DefaultFn: func() string { return "" }, }.Register() +type ParamMap map[string]map[string]string + type Simulation struct { - Name string - Srcs []core.Path - Ips []Ip - Libs []string - Verbose bool + Name string + Srcs []core.Path + Ips []Ip + Libs []string + Params ParamMap + Top string + Dut string + TestCaseGenerator core.Path + TestCasesDir core.Path + WaveformInit core.Path +} + +// Lib returns the standard library name defined for this rule. +func (rule Simulation) Lib() string { + return rule.Name + "_lib" +} + +// Path returns the default root path for log files defined for this rule. +func (rule Simulation) Path() core.Path { + return core.BuildPath("/" + rule.Name) } func (rule Simulation) Build(ctx core.Context) { - if Simulator.Value() == "xsim" { - SimulationXsim{ - Name: rule.Name, - Srcs: rule.Srcs, - Ips: rule.Ips, - Libs: rule.Libs, - Verbose: rule.Verbose, - }.Build(ctx) - } else { - SimulationQuesta{ - Name: rule.Name, - Srcs: rule.Srcs, - Ips: rule.Ips, - Libs: rule.Libs, - Verbose: rule.Verbose, - }.Build(ctx) + switch Simulator.Value() { + case "xsim": + BuildXsim(ctx, rule) + case "questa": + BuildQuesta(ctx, rule) + default: + log.Fatal(fmt.Sprintf("invalid value '%s' for hdl-simulator flag", Simulator.Value())) + } +} + +func (rule Simulation) Run(args []string) string { + res := "" + + switch Simulator.Value() { + case "xsim": + res = RunXsim(rule, args) + case "questa": + res = RunQuesta(rule, args) + default: + log.Fatal(fmt.Sprintf("'run' target not supported for hdl-simulator flag '%s'", Simulator.Value())) + } + + return res +} + +func (rule Simulation) Test(args []string) string { + res := "" + switch Simulator.Value() { + case "xsim": + res = TestXsim(rule, args) + case "questa": + res = TestQuesta(rule, args) + default: + log.Fatal(fmt.Sprintf("'test' target not supported for hdl-simulator flag '%s'", Simulator.Value())) + } + + return res +} + +func (rule Simulation) Description() string { + // Print the rule name as its needed for parameter selection + description := "" + first := true + for param, _ := range rule.Params { + if first { + description = description + " -params=" + param + first = false + } else { + description = description + "," + param + } + } + + if rule.TestCaseGenerator != nil && rule.TestCasesDir != nil { + description = description + " -testcases=" + + // Loop through all defined testcases in directory + if items, err := os.ReadDir(rule.TestCasesDir.String()); err == nil { + for index, item := range items { + if index > 0 { + description = description + "," + } + description = description + item.Name() + } + } else { + log.Fatal(err) + } + } + + if len(description) > 0 { + description = description + " " + } + + return description +} + +// Preamble creates a preamble for the simulation command for the purpose of generating +// a testcase. +func Preamble(rule Simulation, testcase string) (string, string) { + preamble := "" + + // Create a testcase generation command if necessary + if rule.TestCaseGenerator != nil { + if testcase == "" && rule.TestCasesDir != nil { + // No testcase specified, pick the first one from the directory + if items, err := os.ReadDir(rule.TestCasesDir.String()); err == nil { + if len(items) == 0 { + log.Fatal(fmt.Sprintf("TestCasesDir directory '%s' empty!", rule.TestCasesDir.String())) + } + + // Create path to testcase + testcase = rule.TestCasesDir.Absolute() + "/" + items[0].Name() + } + } else if testcase != "" && rule.TestCasesDir != nil { + // Testcase specified, create path to testcase + testcase = rule.TestCasesDir.Absolute() + "/" + testcase + } + + if testcase == "" { + // Create the preamble for testcase generator without any argument + preamble = fmt.Sprintf("{ %s . ; }", rule.TestCaseGenerator.String()) + testcase = "default" + } else { + // Check that the testcase exists + if _, err := os.Stat(testcase); errors.Is(err, os.ErrNotExist) { + log.Fatal(fmt.Sprintf("Testcase '%s' does not exist!", testcase)) + } + + // Create the preamble for testcase generator with arguments + preamble = fmt.Sprintf("{ %s %s . ; }", rule.TestCaseGenerator.String(), testcase) + } + + // Add information to command + preamble = fmt.Sprintf("{ echo Generating %s; } && ", testcase) + preamble + + // Trim testcase for use in coverage databaes + testcase = strings.TrimSuffix(path.Base(testcase), path.Ext(testcase)) } + + return preamble, testcase } diff --git a/RULES/hdl/utils.go b/RULES/hdl/utils.go index cb4c1d4..cd4405b 100644 --- a/RULES/hdl/utils.go +++ b/RULES/hdl/utils.go @@ -5,7 +5,24 @@ import ( ) func IsRtl(path string) bool { - return strings.HasSuffix(path, ".v") || strings.HasSuffix(path, ".sv") || strings.HasSuffix(path, ".vhd") + return strings.HasSuffix(path, ".v") || + strings.HasSuffix(path, ".sv") || + strings.HasSuffix(path, ".vhdl") || + strings.HasSuffix(path, ".vhd") +} + +func IsVerilog(path string) bool { + return strings.HasSuffix(path, ".v") || + strings.HasSuffix(path, ".sv") +} + +func IsVhdl(path string) bool { + return strings.HasSuffix(path, ".vhdl") || + strings.HasSuffix(path, ".vhd") +} + +func IsHeader(path string) bool { + return strings.HasSuffix(path, ".svh") } func IsConstraint(path string) bool { diff --git a/RULES/hdl/xsim.go b/RULES/hdl/xsim.go index 8dbf8d8..a552c96 100644 --- a/RULES/hdl/xsim.go +++ b/RULES/hdl/xsim.go @@ -2,74 +2,458 @@ package hdl import ( "fmt" + "log" + "os" + "path" "strings" "dbt-rules/RULES/core" - "dbt-rules/hdl" ) -type XSimScriptParams struct { - Name string - PartName string - BoardName string - OutDir core.Path - OutScript core.Path - OutSimScript core.Path - IncDir core.Path - Srcs []core.Path - Ips []core.Path - Libs []string - Verbose bool +// XvlogFlags enables the user to specify additional flags for the 'vlog' command. +var XvlogFlags = core.StringFlag{ + Name: "xsim-xvlog-flags", + DefaultFn: func() string { + return "" + }, + Description: "Extra flags for the xvlog command", +}.Register() + +// XvhdlFlags enables the user to specify additional flags for the 'vcom' command. +var XvhdlFlags = core.StringFlag{ + Name: "xsim-xvhdl-flags", + DefaultFn: func() string { + return "" + }, + Description: "Extra flags for the xvhdl command", +}.Register() + +// XsimFlags enables the user to specify additional flags for the 'vsim' command. +var XsimFlags = core.StringFlag{ + Name: "xsim-xsim-flags", + DefaultFn: func() string { + return "" + }, + Description: "Extra flags for the xsim command", +}.Register() + +// XelabDebug enables the user to control the accessibility in the compiled design for +// debugging purposes. +var XelabDebug = core.StringFlag{ + Name: "xsim-xelab-debug", + DefaultFn: func() string { + return "typical" + }, + Description: "Extra debug flags for the xelab command", + AllowedValues: []string{"line", "wave", "drivers", "readers", "xlibs", "all", "typical", "subprogram", "off"}, +}.Register() + +// xsim_rules holds a map of all defined rules to prevent defining the same rule +// multiple times. +var xsim_rules = make(map[string]bool) + +// xsimCompileSrcs compiles a list of sources using the specified context ctx, rule, +// dependencies and include paths. It returns the resulting dependencies and include paths +// that result from compiling the source files. +func xsimCompileSrcs(ctx core.Context, rule Simulation, + deps []core.Path, incs []core.Path, srcs []core.Path) ([]core.Path, []core.Path) { + for _, src := range srcs { + // We handle header files separately from other source files + if IsHeader(src.String()) { + incs = append(incs, src) + } else if IsRtl(src.String()) { + // log will point to the log file to be generated when compiling the code + log := rule.Path().WithSuffix("/" + src.Relative() + ".log") + + // If we already have a rule for this file, skip it. + if xsim_rules[log.String()] { + continue + } + + // Holds common flags for both 'vlog' and 'vcom' commands + cmd := fmt.Sprintf("-work %s --log %s", strings.ToLower(rule.Lib()), log.String()) + + // tool will point to the tool to execute (also used for logging below) + var tool string + if IsVerilog(src.String()) { + tool = "xvlog" + cmd = cmd + " --sv " + XvlogFlags.Value() + cmd = cmd + fmt.Sprintf(" -i %s", core.SourcePath("").String()) + for _, inc := range incs { + cmd = cmd + fmt.Sprintf(" -i %s", path.Dir(inc.Absolute())) + } + } else if IsVhdl(src.String()) { + tool = "xvhdl" + } + + // Remove the log file if the command fails to ensure we can recompile it + cmd = tool + " " + cmd + " " + src.String() + " > /dev/null" + + " || { cat " + log.String() + "; rm " + log.String() + "; exit 1; }" + + // Add the compilation command as a build step with the log file as the + // generated output + ctx.AddBuildStep(core.BuildStep{ + Out: log, + Ins: append(deps, src), + Cmd: cmd, + Descr: fmt.Sprintf("%s: %s", tool, src.Relative()), + }) + + // Add the log file to the dependencies of the next files + deps = append(deps, log) + + // Note down the created rule + xsim_rules[log.String()] = true + } + } + + return deps, incs } -type SimulationXsim struct { - Name string - Srcs []core.Path - Ips []Ip - Libs []string - Verbose bool +// xsimCompileIp compiles the IP dependencies and the source files and an IP. +func xsimCompileIp(ctx core.Context, rule Simulation, ip Ip, + deps []core.Path, incs []core.Path) ([]core.Path, []core.Path) { + for _, sub_ip := range ip.Ips() { + deps, incs = xsimCompileIp(ctx, rule, sub_ip, deps, incs) + } + deps, incs = xsimCompileSrcs(ctx, rule, deps, incs, ip.Sources()) + + return deps, incs } -func (rule SimulationXsim) Build(ctx core.Context) { - outDir := ctx.Cwd().WithSuffix("/" + rule.Name) - outScript := outDir.WithSuffix(".sh") - outSimScript := outDir.WithSuffix(".xsim.tcl") - - ins := []core.Path{} - srcs := []core.Path{} - ips := []core.Path{} - - for _, ip := range FlattenIpGraph(rule.Ips) { - for _, src := range ip.Sources() { - if IsSimulationArchive(src.String()) { - ips = append(ips, src) - } else if IsRtl(src.String()) { - srcs = append(srcs, src) +// xsimCompile compiles the IP dependencies and source files of a simulation rule. +func xsimCompile(ctx core.Context, rule Simulation) []core.Path { + incs := []core.Path{} + deps := []core.Path{} + + for _, ip := range rule.Ips { + deps, incs = xsimCompileIp(ctx, rule, ip, deps, incs) + } + deps, incs = xsimCompileSrcs(ctx, rule, deps, incs, rule.Srcs) + + return deps +} + +// elaborate creates and optimized version of the design optionally including +// coverage recording functionality. The optimized design unit can then conveniently +// be simulated using 'xsim'. +func elaborate(ctx core.Context, rule Simulation, deps []core.Path) { + top := "board" + if rule.Top != "" { + top = rule.Top + } + + log_file_suffix := "xelab.log" + + log_files := []core.OutPath{} + targets := []string{} + params := []string{} + if rule.Params != nil { + for key, _ := range rule.Params { + log_files = append(log_files, rule.Path().WithSuffix("/"+key+"_"+log_file_suffix)) + targets = append(targets, rule.Name+key) + params = append(params, key) + } + } else { + log_files = append(log_files, rule.Path().WithSuffix("/"+log_file_suffix)) + targets = append(targets, rule.Name) + params = append(params, "") + } + + for i := range log_files { + log_file := log_files[i] + target := targets[i] + param_set := params[i] + + // Skip if we already have a rule + if xsim_rules[log_file.String()] { + return + } + + cmd := fmt.Sprintf("xelab --timescale 1ns/1ps --debug %s --log %s %s.%s -s %s", + XelabDebug.Value(), log_file.String(), strings.ToLower(rule.Lib()), top, target) + + // Set up parameters + if param_set != "" { + // Check that the parameters exist + if params, ok := rule.Params[param_set]; ok { + // Add parameters for all generics + for param, value := range params { + cmd = fmt.Sprintf("%s -generic_top \"%s=%s\"", cmd, param, value) + } + } else { + log.Fatal(fmt.Sprintf("parameter set '%s' not defined for Simulation target '%s'!", + params, rule.Name)) } - ins = append(ins, src) } + + cmd = cmd + " > /dev/null || { cat " + log_file.String() + + "; rm " + log_file.String() + "; exit 1; }" + + // Hack: Add testcase generator as an optional dependency + if rule.TestCaseGenerator != nil { + deps = append(deps, rule.TestCaseGenerator) + } + + // Add the rule to run 'xelab'. + ctx.AddBuildStep(core.BuildStep{ + Out: log_file, + Ins: deps, + Cmd: cmd, + Descr: fmt.Sprintf("xelab: %s %s", rule.Lib()+"."+top, target), + }) + + // Note that we created this rule + xsim_rules[log_file.String()] = true + } +} + +// BuildXsim will compile and elaborate the source and IPs associated with the given +// rule. +func BuildXsim(ctx core.Context, rule Simulation) { + // compile the code + deps := xsimCompile(ctx, rule) + + // elaborate the code + elaborate(ctx, rule, deps) +} + +// xsimVerbosityLevelToFlag takes a verbosity level of none, low, medium or high and +// converts it to the corresponding DVM_ level. +func xsimVerbosityLevelToFlag(level string) (string, bool) { + var verbosity_flag string + var print_output bool + switch level { + case "none": + verbosity_flag = " --testplusarg verbosity=DVM_VERB_NONE" + print_output = false + case "low": + verbosity_flag = " --testplusarg verbosity=DVM_VERB_LOW" + print_output = true + case "medium": + verbosity_flag = " --testplusarg verbosity=DVM_VERB_MED" + print_output = true + case "high": + verbosity_flag = " --testplusarg verbosity=DVM_VERB_HIGH" + print_output = true + default: + log.Fatal(fmt.Sprintf("invalid verbosity flag '%s', only 'low', 'medium',"+ + " 'high' or 'none' allowed!", level)) } - srcs = append(srcs, rule.Srcs...) - ins = append(ins, rule.Srcs...) - - data := XSimScriptParams{ - PartName: PartName.Value(), - BoardName: BoardName.Value(), - Name: strings.ToLower(rule.Name), - OutDir: outDir, - OutScript: outScript, - OutSimScript: outSimScript, - IncDir: core.SourcePath(""), - Srcs: srcs, - Ips: ips, - Libs: rule.Libs, - Verbose: rule.Verbose, - } - - ctx.AddBuildStep(core.BuildStep{ - Outs: []core.OutPath{outDir, outScript, outSimScript}, - Ins: ins, - Script: core.CompileTemplateFile(hdl.XSimScriptTmpl.String(), data), - Descr: fmt.Sprintf("Generating XSim simulation %s", outScript.Relative()), - }) + + return verbosity_flag, print_output +} + +// xsimCmd will create a command for starting 'vsim' on the compiled and optimized design with flags +// set in accordance with what is specified on the command line. +func xsimCmd(rule Simulation, args []string, gui bool, testcase string, params string) string { + // Prefix the vsim command with this + cmd_preamble := "" + + // Default log file + log_file_suffix := "xsim.log" + if testcase != "" { + log_file_suffix = testcase + "_" + log_file_suffix + } + if params != "" { + log_file_suffix = params + "_" + log_file_suffix + } + log_file := rule.Path().WithSuffix("/" + log_file_suffix) + + // Default flag values + vsim_flags := " --onfinish quit --log " + log_file.String() + seed_flag := " --sv_seed 1" + verbosity_flag := " --testplusarg verbosity=DVM_VERB_NONE" + mode_flag := "" + plusargs_flag := "" + + // Collect do-files here + var do_flags []string + + // Turn off output unless verbosity is activated + print_output := false + + // Parse additional arguments + for _, arg := range args { + if strings.HasPrefix(arg, "-seed=") { + // Define simulator seed + var seed int64 + if _, err := fmt.Sscanf(arg, "-seed=%d", &seed); err == nil { + seed_flag = fmt.Sprintf(" --sv_seed %d", seed) + } else { + log.Fatal("-seed expects an integer argument!") + } + } else if strings.HasPrefix(arg, "-verbosity=") { + // Define verbosity level + var level string + if _, err := fmt.Sscanf(arg, "-verbosity=%s", &level); err == nil { + verbosity_flag, print_output = xsimVerbosityLevelToFlag(level) + } else { + log.Fatal("-verbosity expects an argument of 'low', 'medium', 'high' or 'none'!") + } + } else if strings.HasPrefix(arg, "+") { + // All '+' arguments go directly to the simulator + plusargs_flag = plusargs_flag + " --testplusarg " + strings.TrimPrefix(arg, "+") + } + } + + // Create optional command preamble + cmd_preamble, testcase = Preamble(rule, testcase) + + cmd_echo := "" + if rule.Params != nil && params != "" { + // Update coverage database name based on parameters. We cannot merge + // different parameter sets, do we have to make a dedicated main database + // for this parameter set. + cmd_echo = "Testcase " + params + + // Update with testcase if specified + if testcase != "" { + cmd_echo = cmd_echo + "/" + testcase + ":" + testcase = params + "_" + testcase + } else { + cmd_echo = cmd_echo + ":" + testcase = params + } + } else { + // Update coverage database name with testcase alone, main database stays + // the same + if testcase != "" { + cmd_echo = "Testcase " + testcase + ":" + } else { + testcase = "default" + } + } + + cmd_postamble := "" + if gui { + mode_flag = " --gui" + if rule.WaveformInit != nil { + do_flags = append(do_flags, rule.WaveformInit.String()) + } + } else { + mode_flag = " --runall" + cmd_newline := ":" + if cmd_echo != "" { + cmd_newline = "echo" + } + + cmd_postamble = fmt.Sprintf("|| { %s; cat %s; exit 1; }", cmd_newline, log_file.String()) + } + + vsim_flags = vsim_flags + mode_flag + seed_flag + + verbosity_flag + plusargs_flag + XsimFlags.Value() + + for _, do_flag := range do_flags { + vsim_flags = vsim_flags + " --tclbatch " + do_flag + } + + // Using this part of the command we send the stdout into a black hole to + // keep the output clean + cmd_devnull := "" + if !print_output { + cmd_devnull = "> /dev/null" + } + + cmd := fmt.Sprintf("{ echo -n %s && xsim %s %s %s && "+ + "{ { ! grep -q FAILURE %s; } && echo PASS; } }", + cmd_echo, vsim_flags, rule.Name+params, cmd_devnull, log_file.String()) + if cmd_preamble == "" { + cmd = cmd + " " + cmd_postamble + } else { + cmd = "{ { " + cmd_preamble + " } && " + cmd + " } " + cmd_postamble + } + + // Wrap command in another layer of {} to enable chaining + cmd = "{ " + cmd + " }" + + return cmd +} + +// simulateXsim will create a command to start 'vsim' on the compiled design +// with flags set in accordance with what is specified on the command line. It will +// optionally build a chain of commands in case the rule has parameters, but +// no parameters are specified on the command line +func simulateXsim(rule Simulation, args []string, gui bool) string { + // Optional testcase goes here + testcases := []string{} + + // Optional parameter set goes here + params := []string{} + + // Parse additional arguments + for _, arg := range args { + if strings.HasPrefix(arg, "-testcases=") && rule.TestCaseGenerator != nil { + var testcases_arg string + if _, err := fmt.Sscanf(arg, "-testcases=%s", &testcases_arg); err != nil { + log.Fatal(fmt.Sprintf("-testcases expects a string argument!")) + } + testcases = append(testcases, strings.Split(testcases_arg, ",")...) + } else if strings.HasPrefix(arg, "-params=") && rule.Params != nil { + var params_arg string + if _, err := fmt.Sscanf(arg, "-params=%s", ¶ms_arg); err != nil { + log.Fatal(fmt.Sprintf("-params expects a string argument!")) + } else { + for _, param := range strings.Split(params_arg, ",") { + if _, ok := rule.Params[param]; ok { + params = append(params, param) + } + } + } + } + } + + // If no parameters have been specified, simulate them all + if rule.Params != nil && len(params) == 0 { + for key := range rule.Params { + params = append(params, key) + } + } else if len(params) == 0 { + params = append(params, "") + } + + // If no testcase has been specified, simulate them all + if rule.TestCaseGenerator != nil && rule.TestCasesDir != nil && len(testcases) == 0 { + // Loop through all defined testcases in directory + if items, err := os.ReadDir(rule.TestCasesDir.String()); err == nil { + for _, item := range items { + testcases = append(testcases, item.Name()) + } + } else { + log.Fatal(err) + } + } else if len(testcases) == 0 { + testcases = append(testcases, "") + } + + // Final command + cmd := "{ :; }" + + // Loop for all parameter sets + for i := range params { + // Loop for all test cases + for j := range testcases { + cmd = cmd + " && " + xsimCmd(rule, args, gui, testcases[j], params[i]) + // Only one testcase allowed in GUI mode + if gui { + break + } + } + // Only one parameter set allowed in gui mode + if gui { + break + } + } + + return cmd +} + +// Run will build the design and run a simulation in GUI mode. +func RunXsim(rule Simulation, args []string) string { + return simulateXsim(rule, args, true) +} + +// Test will build the design and run a simulation in batch mode. +func TestXsim(rule Simulation, args []string) string { + return simulateXsim(rule, args, false) }