Introduction

Since I’ve started using SpinalHDL for my hobby projects, I’ve been running into many issues. A few of those where minor bugs of SpinalHDL itself, but most of them are because Scala is a completely new language for me.

Initially, I’ve been using SpinalHDL as a pure RTL generator, a replacement of Verilog that requires less typing. In that case, you can get by with a bare minimum of commands and Scala. But SpinalHDL can do much more: complex generators, testbench integration etc. And when you start to get into that, lack of knowledge about Scala and the Scala development environment becomes a limitation quickly.

This blog post serves as a collection of development road blocks and methods on how to navitage past these road blocks.

It’s a living document to which I’ll keep adding more stuff as I get better at SpinalHDL.

Split Testbench into Functional Sections

In general, the best practise seems to be to create a separate class per functional block. A good example of different tests is here.

You can either compile the DUT as part of a single test, or you can compile the dut once, and then use the compiled version for all the tests. That’s definitely more efficient.

Here’s an example of doing a single DUT compile, and then having multiple tests using it:

To kick of all the tests of a Tester class, do the following:

sbt "test-only <scala path to tester class>"

Like this:

sbt "test-only math.FpxxTester"

To run only 1 of the tests:

sbt "test-only math.FpxxTester -- -z FpxxAdd"

Note: running 1 test only works if you compile the DUT as part of the test itself. If you have a separate compile test, then it will not work.

Optional Pipeline Stage

When pipeline is True, then a pipeline stage (with optional enable input) is instantiated, and the input signal is flopped. When False, it’s a straight wire.

Definition:

object OptPipe {
    def apply[T <: Data](that : T, ena: Bool, pipeline : Boolean) : T = if (pipeline) RegNextWhen(that, ena) else that
    def apply[T <: Data](that : T, pipeline : Boolean) : T = apply(that, True, pipeline)
}

Usage:

    // Only insert pipeline stage when configuration parameter asks for it
    val p4_pipe_ena     = pipeStages >= 2

    // When using an enable, it needs to ripple through the pipeline as well...
    val p4_vld          = OptPipe(p3_vld, p4_pipe_ena)

    val sign_p4         = OptPipe(sign_p3,       p3_vld, p4_pipe_ena)
    val x_mul_yhyl_p4   = OptPipe(x_mul_yhyl_p3, p3_vld, p4_pipe_ena)
    val recip_yh2_p4    = OptPipe(recip_yh2_p3,  p3_vld, p4_pipe_ena)
    val exp_full_p4     = OptPipe(exp_full_p3,   p3_vld, p4_pipe_ena)

Leading Zeros Calculator

Implements the method of this StackExchange question in a recursive and a generic way that works for any sized vector input: full code.

Initialize a 32-bit wide Mem with the contents of binary file

  • First pad the binary file to make it the desired size of the Mem.

    I have this in my Makefile:

progmem8k.bin: progmem.bin
        cp $< $@
        dd if=/dev/zero of=$@ bs=1 count=0 seek=8192
  • Then load it through the initialContent parameter when instantiating the Mem:
        import java.nio.file.{Files, Paths}

        val byteArray = Files.readAllBytes(Paths.get("sw/progmem8k.bin"))
        val cpuRamContent = for(i <- 0 until ramSize/4) yield {
                B( (byteArray(4*i).toLong & 0xff) + ((byteArray(4*i+1).toLong & 0xff)<<8) + ((byteArray(4*i+2).toLong & 0xff)<<16) + ((byteArray(4*i+3).toLong & 0xff)<<24), 32 bits)
        }

        val cpu_ram = Mem(Bits(32 bits), initialContent = cpuRamContent)

I have filed a SpinalHDL GitHub Issue to provide a better way to do this.

Using a Local SpinalHDL Version

Often, you only need to make minor changes to your build.sbt file to switch to the latest SpinalHDL released version or to point to a local SpinalHDL version (e.g. to test some kind of patch.)

Here’s such a typical build.sbt file update:

  • The Scala version may need to be updated to a later version
  • You comment out the released SpinalHDL version and add lazy val statements that point to the local SpinalHDL version.

However, often, you need to update a bunch of other files as well:

  • project/build.properties needs to point to the correct sbt release
  • project/plugins.sbt might need to be updated to the latest plugins as well.

If you don’t know what values need to be used for a particular SpinalHDL release, I always look at these files in latest VexRiscv release. That one is almost always in sync with the latest SpinalHDL release.

VexRiscv

Run DhystoneBench for multiple CPU cores:

sbt "testOnly vexriscv.DhrystoneBench"

To Delete Absolutely Everything

sbt has various places where it stores cached content. Sometimes, when, for example upgrading to a new version of SpinalHDL, things don’t work well.

The best way to proceed, then, is to remove all old Scala traces from your system.

Executed from within your project, this should do it:

sbt clean
rm -fr ~/.ivy2
rm -fr ~/.sbt

Running Something from the SpinalHDL tree itself

This doesn’t work:

cd projects/SpinalHDL
sbt "runMain  spinal.lib.com.i2c.Apb3I2cCtrl"

SpinalHDL is itself a multi-module project. So you need to do:

cd projects/SpinalHDL
sbt "lib/runMain  spinal.lib.com.i2c.Apb3I2cCtrl"

Verilog/VHDL Generation Options

Code generation options are specified through the SpinalConfig.

Some useful options:

  • netlistFileName

    Specify the name of the generated Verilog or VHDL file.

    Super useful, because my simulation and synthesis files are often different. (E.g. I use a PLL in the synthesis file but not in the simulation file.)

  • anonymSignalUniqueness

    When true, intermediate signals are unique across the all modules.

    For example. instead of creating a signals _zz_10 in different modules, it will create _zz_MyModule1_10 and _zz_MyModule2_10. This makes it much easier to find all instances in a file where a particular signals is used.

    Default is false, but I set it to true for all my designs.

  • anonymSignalPrefix

    By default, intermediate generated signals start with _zz.

    With this configuration option, you can change it to something else.

  • globalPrefix

    Add a prefix to all module names.

    For example, setting it to gp_ will create a module with gp_MyModule11 instead of `MyModule1.

    This is useful if, for example, you want to include the Verilog of 2 separate SpinalHDL designs into an overall Verilog toplevel and avoid naming collisions.

  • targetDirectory

    By default, all generated files (Verilog, VDHL, ROM binary files) are created in the ./spinal directory.

    You can change that directory with this option.

  • oneFilePerComponent

    When true, writes out only the Verilog or VHDL for the toplevel component, not the instances below it.

  • inlineRom

    When true, initialize memories in the design with an initial statement that assigns a value to each location of the array instead of loading the contents from a separate file with $readmemb.

  • mergeAsyncProcess

    When false (the default), there’s a separate always @(*) block for each combinatorial signal assigment. When true, multiple signals can assigned in the same always @(*) block. This can be easier to match the generated Verilog with the original SpinalHDL code.