chiseltest

repository·main·Indexed 42 days ago

https://github.com/ucb-bar/chiseltest

A testing and formal verification library for Chisel-based RTL designs. It provides a 'batteries-included' environment for writing lightweight, readable, and composable tests using ScalaTest integration. Features include support for multiple simulator backends (treadle, verilator, iverilog, and vcs), concurrency abstractions via fork and join, and various waveform output formats such as VCD, FST, LXT, VPD, and FSDB.

Tokens
4.6K
Snippets
15
Records
21
Agent score
53%

What's inside chiseltest

  1. Use fork and join for concurrent test threads

    main

    ChiselTest provides a concurrency abstraction for writing testbenches using fork and join. This allows you to spawn threads that run within the simulation cycle without real parallelism.

    • fork: Spawns a thread. Child threads run immediately, then return to the parent. On subsequent cycles, child threads run before their parent in the order they were spawned.
    • join: Blocks (waits) on a thread.
    • Safety: The infrastructure checks for collisions or unexpected behavior when pokes/peeks/expects are performed on wires from different threads.
    • Synchronization: You can use fork.withRegion(...) to associate a region with a thread, acting as a synchronization barrier within simulator time steps (useful for monitors).
  2. Install chiseltest via sbt

    main

    To use chiseltest as a managed dependency in your Scala project, add it to your build.sbt file.

    Important: When using chisel5, ensure you pick a matching major version to avoid linking errors. For older versions, ensure your chisel3 and chiseltest versions match to prevent linking issues.

    libraryDependencies += "edu.berkeley.cs" %% "chiseltest" % "5.0-SNAPSHOT"
  3. Migrate from chisel-testers / iotesters

    main

    If migrating from the older chisel-testers or iotesters libraries:

    1. Invert Syntax: In chisel-testers, you used tester.poke(wire, value). In chiseltest, use wire.poke(value) where value is a Chisel literal.
    2. Test Helpers: Since chiseltest does not require a tester context, you can define test helper functions as standard libraries outside of a test class.
    3. PeekPokeTester Compatibility: Use the compatibility layer in chiseltest.iotesters to reuse old tests. Note that while the test logic remains similar, the old Driver is removed and replaced by the new test syntax.
    4. Hardware Testers: Synthesizable tests (extending BasicTester) can be run using the runUntilStop function.
  4. Write a basic ChiselTest unit test

    main

    ChiselTest integrates with the ScalaTest framework. To write a test, follow these steps:

    1. Imports: Import chisel3._, chiseltest._, and org.scalatest.flatspec.AnyFlatSpec.
    2. Test Class: Extend AnyFlatSpec with ChiselScalatestTester.
    3. Test Case: Use the it should "..." in { ... } syntax.
    4. Module Under Test: Use the test(new MyModule) { c => ... } function. The argument c is a handle to the module.
    5. Stimulus: Use poke, step, and expect to drive and verify signals.

    ChiselScalatestTester provides the testdriver functionality and signal value assertions. AnyFlatSpec is the recommended ScalaTest style.

    import chisel3._
    import chiseltest._
    import org.scalatest.flatspec.AnyFlatSpec
    
    class MyModule extends Module {
      val io = IO(new Bundle {
        val in = Input(UInt(16.W))
        val out = Output(UInt(16.W))
      })
      io.out := RegNext(io.in)
    }
    
    class BasicTest extends AnyFlatSpec with ChiselScalatestTester {
      behavior of "MyModule"
    
      it should "do something" in {
        test(new MyModule) {
          c.io.in.poke(0.U)
          c.clock.step()
          c.io.out.expect(0.U)
          c.io.in.poke(42.U)
          c.clock.step()
          c.io.out.expect(42.U)
          println("Last output value :" + c.io.out.peek().litValue)
        }
      }
    }
  5. Handle port dependencies in ScalaBlackBox

    main

    When implementing a ScalaBlackBox, you can define dependencies between ports using getDependencies.

    Note on Internal State: If your black box has internal state, you may need to specify that inputs depend on outputs to ensure the simulator achieves the correct topological sort during the single-pass assignment of wires. This prevents issues where the simulator attempts to assign a wire before the black box has updated its internal state based on the current inputs.

  6. Use VCS waveform dumping

    main

    The VCS simulator supports several waveform formats. To enable waveform dumping, include the corresponding annotation in your circuit state.

    Note for FSDB (Verdi): If you use WriteFsdbAnnotation, you must have the VERDI_HOME environment variable set in your system for the simulator to correctly link the Verdi PLI libraries.

    // Supported waveform annotations:
    // WriteVcdAnnotation
    // WriteVpdAnnotation
    // WriteFsdbAnnotation
  7. Handle simulation interrupts with StepResult

    main

    When calling step(n), the simulator returns a StepResult to indicate whether the simulation proceeded normally or was interrupted by a hardware assertion or stop statement.

    • StepOk: The simulation completed the requested number of steps without interruption.
    • StepInterrupted(after, isFailure, sources): An interrupt occurred.
      • after: The number of steps completed before the interrupt (always $> 0$ and $\le n$).
      • isFailure: true if the interrupt was caused by an assertion/assumption violation or a stop with a non-zero return code; false otherwise.
      • sources: A sequence of hierarchical names identifying which stop/assert/assume statements were triggered.
  8. Enable full stacktraces in tests

    main

    To remove Chisel3's stacktrace suppression (the at ... () lines) and see the full trace, use the PrintFullStackTraceAnnotation via .withChiselAnnotations.

    If you are using ScalaTest via SBT, you must also pass the -oF flag to ScalaTest to prevent it from suppressing the trace. Use -- in SBT to pass arguments directly to ScalaTest.

    import chisel3.stage.PrintFullStackTraceAnnotation
    
    test(new MyModule).withChiselAnnotations(Seq(PrintFullStackTraceAnnotation)) {
      c => // test body
    }
    # Running via SBT
    $ sbt
    > testOnly <spec name> -- -oF
  9. Configure simulator backends

    main

    ChiselTest supports multiple simulator backends. You can switch between them to balance execution speed and waveform quality. Use the appropriate BackendAnnotation to select a simulator.

    BackendAnnotationNotes
    treadle(Default)Fast startup, slow execution for large circuits, supports VCD
    verilatorVerilatorBackendAnnotationSlow startup, fast execution, supports VCD and FST
    iverilogIcarusBackendAnnotationSupports VCD, FST, and LXT
    vcsVcsBackendAnnotationCommercial, supports VCD and FSDB
  10. Configure VCS simulator options

    main

    When using the VCS simulator backend in chiseltest, you can provide specific configuration via annotations. These annotations allow you to pass custom flags to different stages of the VCS compilation and simulation process.

    Available VCS options:

    • VcsFlags(flags: Seq[String]): Adds flags to the initial vcs invocation (compilation stage).
    • VcsCFlags(flags: Seq[String]): Adds flags to the C++ compiler used in the Makefile generated by VCS.
    • VcsSimFlags(flags: Seq[String]): Adds flags to the final simulation binary execution.
    • VcsBackendAnnotation: Used to explicitly select the VcsSimulator backend.
    // Example of using VCS specific flags
    val myAnnotations = Seq(
      VcsBackendAnnotation,
      VcsFlags(Seq("-debug_all")),
      VcsCFlags(Seq("-O3")),
      VcsSimFlags(Seq("+extra_sim_arg"))
    )
  11. Use SimulatorContext to control simulation execution

    main

    The SimulatorContext is the primary interface for interacting with a running simulation. It allows you to drive inputs, read outputs, and advance the simulation clock.

    Key Methods

    • step(n: Int): Advances the main clock n times. Returns StepOk if the simulation continues normally, or StepInterrupted if a stop, assertion, or assumption was triggered.
    • poke(signal: String, value: BigInt): Sets the value of a top-level input port.
    • peek(signal: String): Reads the current value of a top-level input or output port.
    • peekMemory(signal: String, index: Long) and pokeMemory(...): Accesses specific memory locations (supported by select backends).
    • getCoverage(): Returns a list of (String, Long) representing coverage counters.
    • finish(): Finalizes the simulation. Note: You must call this to ensure waveform files are properly written to disk. After calling finish, you can only call sim or getCoverage (if supported).
    // Example usage pattern
    val context = simulator.createContext(state)
    context.poke("my_input", 1)
    val result = context.step(10)
    
    result match {
      case StepOk => println("Simulation stepped successfully")
      case StepInterrupted(after, isFailure, sources) => 
        println(s"Interrupted after $after steps. Failure: $isFailure")
    }
    
    val outputVal = context.peek("my_output")
    context.finish()