diff --git a/src/main/scala/chisel3/simulator/parametric/ParametricSimulator.scala b/src/main/scala/chisel3/simulator/parametric/ParametricSimulator.scala new file mode 100644 index 00000000000..84c313d10e8 --- /dev/null +++ b/src/main/scala/chisel3/simulator/parametric/ParametricSimulator.scala @@ -0,0 +1,226 @@ +package chisel3.simulator.parametric + +import svsim._ +import chisel3.RawModule +import chisel3.simulator.{PeekPokeAPI, SingleBackendSimulator} + +import java.nio.file.Files +import java.time.{format, LocalDateTime} + +/** + * The interface object to the simulation. + * + * It uses [[svsim]] and [[PeekPokeAPI]] to run a simulation. + */ +object ParametricSimulator extends PeekPokeAPI { + + // private var simulator = new ParametricSimulator + + /** If true, the simulator will be reset before running each simulation */ + private var _resetSimulationBeforeRun = false + + /** Use this method to run a simulations */ + def simulate[T <: RawModule]( + module: => T, + settings: Seq[SimulatorSettings] = Seq(), + simName: String = "defaultSimulation" + )(body: T => Unit + ): Unit = { + // if (_resetSimulationBeforeRun) + // reset() + // simulator.simulate(module, settings, simName)(body) + + // TODO: check if the simulation should be reset for every run + (new ParametricSimulator).simulate(module, settings, simName)(body) + } + + /** + * Use this method to manually reset the simulator and run multiple + * independent simulations + */ + // def reset(): Unit = + // simulator = new ParametricSimulator + + def resetBeforeEachRun(): Unit = + _resetSimulationBeforeRun = true +} + +/** + * A simulator that can be customized by passing some parameters through a list + * of [[SimulatorSettings]]. It offers the possibility to: + * - output a trace vcd file + * - store the files generated during the simulation + * + * More advanced simulators can be implemented starting from this. + */ +class ParametricSimulator { + + // Settings variable of the simulator + private var _backendCompileSettings = verilator.Backend.CompilationSettings() + private val _testRunDir = "test_run_dir" + private var _moduleDutName = "" + private val _simulatorName = getClass.getSimpleName.stripSuffix("$") + private var _simName = "defaultSimulationName" + + // Working directory and workspace + private var _workdirFinalFile: Option[String] = None + private var _workdir: Option[String] = None // Initialized when makeSimulator is called + protected def wantedWorkspacePath: String = + if (_workdirFinalFile.isDefined) + Seq(_testRunDir, _moduleDutName, _simulatorName, _simName, _workdirFinalFile.get).mkString("/") + else + Seq(_testRunDir, _moduleDutName, _simulatorName, _simName).mkString("/") + + // Traces + private var _traceExtension: Option[String] = None + private var _traceWantedName: String = "trace" + private var _finalTracePath: Option[String] = None + + def finalTracePath: Option[String] = _finalTracePath + private def _emittedTraceRelPath: Option[String] = + if (_workdir.isDefined && _traceExtension.isDefined) + Some(Seq(_workdir.get, s"trace.${_traceExtension.get}").mkString("/")) + else None + + private var _firtoolArgs: Seq[String] = Seq() + + /** Launch and execute a simulation given a list of [[SimulatorSettings]]. */ + def simulate[T <: RawModule]( + module: => T, + settings: Seq[SimulatorSettings] = Seq(), + simName: String + )(body: T => Unit + ): Unit = { + + // Set the initial settings before the simulation: i.e. backend compile settings + setInitialSettings(settings) + + // Create a new simulator + val simulator = makeSimulator + simulator + .simulate(module) { simulatedModule => + // Set the controller settings: i.e. enable trace + setControllerSettings(simulatedModule.controller, settings) + + // Update the wanted workspace path + _moduleDutName = simulatedModule.wrapped.name + _simName = simName + + // Launch the actual simulation and return the result + body(simulatedModule.wrapped) + } + .result + + // Cleanup the simulation after the execution + simulator.cleanup() + } + + /** + * Set settings before the simulation starts. It sets the initial settings + * such as the backend compile settings, trace style, etc. + */ + private def setInitialSettings(settings: Seq[SimulatorSettings]): Unit = { + settings.foreach { + case t: TraceSetting => + _backendCompileSettings = verilator.Backend.CompilationSettings( + traceStyle = Some(t.traceStyle), + outputSplit = None, + outputSplitCFuncs = None, + disabledWarnings = Seq(), + disableFatalExitOnWarnings = false + ) + // Set also the extension + _traceExtension = Some(t.extension) + + case TraceName(name) => + if ( + !settings.exists { + case _: TraceSetting => true + case _ => false + } + ) throw new Exception("TraceName must be used with TraceSetting") + if (settings.count(_.isInstanceOf[TraceName]) > 1) + throw new Exception("TraceName must be used only once") + _traceWantedName = name + + case SaveWorkspace(name) => + if (settings.count(_.isInstanceOf[SaveWorkspace]) > 1) + throw new Exception("SaveWorkspace must be used only once") + if (name == "") // If no name is specified -> Current date time as default (gg-mm-ddThh:mm) no millis + _workdirFinalFile = + Some(LocalDateTime.now().format(format.DateTimeFormatter.ofPattern("dd_MM_yyyy:HH-mm-ss"))) + else + _workdirFinalFile = Some(name) + + case FirtoolArgs(args) => _firtoolArgs = args + case _ => + } + + // If it contains underscore, add it to the trace name + if (settings.contains(simulatorSettings.VcdTraceWithUnderscore)) + _traceWantedName = s"${_traceWantedName}_underscore" + + } + + /** + * Set the controller settings. It sets settings such as the trace output + * enable. + */ + private def setControllerSettings(controller: Simulation.Controller, settings: Seq[SimulatorSettings]): Unit = + settings.foreach { + case _: TraceSetting => controller.setTraceEnabled(true) + case _ => // Ignore other settings + } + + /** Default ParametricSimulator */ + protected class DefaultParametricSimulator(val workspacePath: String, val tag: String) + extends SingleBackendSimulator[verilator.Backend] { + + val backend: verilator.Backend = verilator.Backend.initializeFromProcessEnvironment() + override val firtoolArgs: Seq[String] = _firtoolArgs + + val backendSpecificCompilationSettings: verilator.Backend.CompilationSettings = _backendCompileSettings + val commonCompilationSettings: CommonCompilationSettings = + CommonCompilationSettings( + optimizationStyle = CommonCompilationSettings.OptimizationStyle.OptimizeForCompilationSpeed, + availableParallelism = CommonCompilationSettings.AvailableParallelism.UpTo(4), + defaultTimescale = Some(CommonCompilationSettings.Timescale.FromString("1ms/1ms")) + ) + + /** + * Cleanup the simulation and move the simulation workspace to the wanted + * workspace path. + */ + def cleanup(): Unit = { + + val workDirOld = os.Path(workspacePath) + val workDir = os.pwd / os.RelPath(wantedWorkspacePath) + + // Check if the workspace must be saved or not + if (_workdirFinalFile.isDefined) + os.copy(workDirOld, workDir, replaceExisting = true, createFolders = true, mergeFolders = true) + + // Rename the wanted trace + if (_traceExtension.isDefined) { + val tracePath = workDirOld / os.RelPath(_emittedTraceRelPath.get) + val wantedTracePath = + os.pwd / os.RelPath(Seq(wantedWorkspacePath, s"${_traceWantedName}.${_traceExtension.get}").mkString("/")) + os.copy(tracePath, wantedTracePath, replaceExisting = true, createFolders = true) + _finalTracePath = Some(wantedTracePath.toString) + } + + } + } + + /** Create a new simulator */ + protected def makeSimulator: DefaultParametricSimulator = { // def allows to create a new simulator each tim + val className = _simulatorName + val id = java.lang.management.ManagementFactory.getRuntimeMXBean.getName + + val tmpWorkspacePath = Files.createTempDirectory(s"${className}_${id}_").toString + val tag = "default" + val defaultSimulator = new DefaultParametricSimulator(tmpWorkspacePath, tag = tag) + _workdir = Some(s"${defaultSimulator.workingDirectoryPrefix}-$tag") + defaultSimulator + } +} diff --git a/src/main/scala/chisel3/simulator/parametric/SimulatorSettings.scala b/src/main/scala/chisel3/simulator/parametric/SimulatorSettings.scala new file mode 100644 index 00000000000..ef766946078 --- /dev/null +++ b/src/main/scala/chisel3/simulator/parametric/SimulatorSettings.scala @@ -0,0 +1,57 @@ +package chisel3.simulator.parametric + +import svsim.verilator +import verilator.Backend.CompilationSettings.TraceStyle + +/** Trait to represent the simulator settings */ +trait SimulatorSettings + +protected sealed trait TraceSetting extends SimulatorSettings { + val traceStyle: TraceStyle + val extension: String +} + +/** + * [[TraceVcd]] is an abstracted representation of the + * [[verilator.Backend.CompilationSettings.TraceStyle.Vcd]] that can be used by + * the user of the simulator. + */ +private[simulator] case class TraceVcd(traceUnderscore: Boolean) extends TraceSetting with SimulatorSettings { + val traceStyle: TraceStyle = TraceStyle.Vcd(traceUnderscore) + val extension: String = "vcd" +} + +/** + * [[TraceName]] stores the name of the final trace file. + */ +private[simulator] case class TraceName(name: String) extends SimulatorSettings + +/** + * [[SaveWorkspace]] tells to save the workspace of the simulation and stores + * the final name. + */ +private[simulator] case class SaveWorkspace(name: String) extends SimulatorSettings + +/** + * [[FirtoolArgs]] stores the arguments to pass to the firtool command. + */ +private[simulator] case class FirtoolArgs(args: Seq[String]) extends SimulatorSettings + +/** + * Package object to expose the simulator settings to the user. The following + * settings can be used by a simulator to allow users to configure the + * simulation. + */ +package object simulatorSettings { + // Interface to the simulation + val VcdTrace: TraceVcd = TraceVcd(false) + val VcdTraceWithUnderscore: TraceVcd = TraceVcd(true) + + val SaveWorkdirFile: String => SaveWorkspace = (name: String) => SaveWorkspace(name) + val SaveWorkdir: SaveWorkspace = SaveWorkspace("") + + val NameTrace: String => TraceName = (name: String) => TraceName(name) + + val WithFirtoolArgs: Seq[String] => FirtoolArgs = (args: Seq[String]) => FirtoolArgs(args) + +} diff --git a/src/test/scala/chiselTests/simulator/ParametricSimulatorSpec.scala b/src/test/scala/chiselTests/simulator/ParametricSimulatorSpec.scala new file mode 100644 index 00000000000..50125fb3c80 --- /dev/null +++ b/src/test/scala/chiselTests/simulator/ParametricSimulatorSpec.scala @@ -0,0 +1,128 @@ +package chiselTests.simulator + +import chisel3._ + +// Import the simulator and the settings +import chisel3.simulator.parametric.ParametricSimulator._ +import chisel3.simulator.parametric.simulatorSettings._ + +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.must.Matchers + +import java.nio.file.{Files, Paths} + +class ParametricSimulatorSpec extends AnyFunSpec with Matchers { + + // The testbench function used in this test + private def gcdTb(gcd: => GCD): Unit = { + gcd.io.a.poke(24.U) + gcd.io.b.poke(36.U) + gcd.io.loadValues.poke(1.B) + gcd.clock.step() + gcd.io.loadValues.poke(0.B) + gcd.clock.stepUntil(sentinelPort = gcd.io.resultIsValid, sentinelValue = 1, maxCycles = 10) + gcd.io.resultIsValid.expect(true.B) + gcd.io.result.expect(12) + } + + describe("ParametricSimulator runs") { + + it("runs GCD correctly without settings") { + simulate(new GCD())(gcd => gcdTb(gcd)) + } + } + + describe("ParametricSimulator: VCD Trace") { + it("runs GCD with VCD trace file") { + simulate(new GCD(), Seq(VcdTrace))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/trace.vcd"))) + } + + it("runs GCD with VCD underscore trace") { + simulate(new GCD(), Seq(VcdTraceWithUnderscore))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/trace_underscore.vcd"))) + } + + it("runs GCD with VCD trace file with name") { + simulate(new GCD(), Seq(VcdTrace, NameTrace("gcdTbTraceName")))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/gcdTbTraceName.vcd"))) + } + + it("runs underscore when VcdTrace and VcdTraceWithUnderscore are used") { + simulate(new GCD(), Seq(VcdTrace, VcdTraceWithUnderscore))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/trace_underscore.vcd"))) + + simulate(new GCD(), Seq(VcdTraceWithUnderscore, VcdTrace))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/trace_underscore.vcd"))) + } + + it("runs GCD with VCD trace file with name and VCD underscore trace") { + simulate(new GCD(), Seq(VcdTrace, VcdTraceWithUnderscore, NameTrace("gcdTb1")))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/gcdTb1_underscore.vcd"))) + + simulate(new GCD(), Seq(VcdTraceWithUnderscore, VcdTrace, NameTrace("gcdTb2")))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/gcdTb2_underscore.vcd"))) + + simulate(new GCD(), Seq(NameTrace("gcdTb3"), VcdTraceWithUnderscore, VcdTrace))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/gcdTb3_underscore.vcd"))) + } + } + + describe("ParametricSimulator: SaveWorkdir") { + + it("uses a name for the simulation") { + simulate(new GCD(), Seq(VcdTrace, NameTrace("gcdTb1")), simName = "use_a_name_for_the_simulation")(gcd => + gcdTb(gcd) + ) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/use_a_name_for_the_simulation/gcdTb1.vcd"))) + } + + it("save the workdir with a name") { + simulate(new GCD(), Seq(VcdTrace, SaveWorkdirFile("myWorkdir")))(gcd => gcdTb(gcd)) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/myWorkdir"))) + } + } + + describe("ParametricSimulator: Custom Firtool compilation") { + it("uses firtool args") { + simulate(new GCD(), Seq(WithFirtoolArgs(Seq("-g", "--emit-hgldd")), SaveWorkdirFile("myWorkdir2")))(gcd => + gcdTb(gcd) + ) + assert(Files.exists(Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/myWorkdir2"))) + assert( + Files.exists( + Paths.get("test_run_dir/GCD/ParametricSimulator/defaultSimulation/myWorkdir2/support-artifacts/GCD.dd") + ) + ) + } + + } + + describe("ParametricSimulator Exceptions") { + + it("throws an exception when NameTrace is used without VcdTrace or VcdTraceWithUnderscore") { + intercept[Exception] { + simulate(new GCD(), Seq(NameTrace("")))(gcd => gcdTb(gcd)) + } + } + + it("throws an exception with two or more NameTrace") { + intercept[Exception] { + simulate(new GCD(), Seq(VcdTrace, NameTrace("a"), NameTrace("b")))(gcd => gcdTb(gcd)) + } + } + + it("throws an exception with two or more SaveWorkdir") { + intercept[Exception] { + simulate(new GCD(), Seq(SaveWorkdir, SaveWorkdir))(gcd => gcdTb(gcd)) + } + } + + it("throws an exception with two or more SaveWorkdirFile") { + intercept[Exception] { + simulate(new GCD(), Seq(SaveWorkdirFile("a"), SaveWorkdirFile("b")))(gcd => gcdTb(gcd)) + } + } + + } +}