SpinalHDL with Verilator
This tutorial walks you through building an 8-bit counter in SpinalHDL, simulating it with Verilator, and exploring the resulting waveform in NovyWave. SpinalHDL is a hardware description language embedded in Scala that lets you write hardware designs using the full power of a modern programming language — type safety, generics, and functional abstractions — while generating clean Verilog or VHDL for synthesis and simulation. Verilator compiles the generated Verilog into optimized C++ for fast simulation.
This tutorial works on Linux and macOS. On Windows, Java and sbt work natively, but Verilator requires MSYS2 with a full MinGW build environment — consider using WSL instead.
Prerequisites
Section titled “Prerequisites”- Java 11+ (SpinalHDL runs on the JVM)
- sbt (Scala Build Tool)
- Verilator (for simulation)
- NovyWave installed (Installation Guide)
Installing Java
Section titled “Installing Java”Ubuntu/Debian:
sudo apt install openjdk-11-jdkmacOS:
brew install openjdk@11Verify with:
java -versionInstalling sbt
Section titled “Installing sbt”Ubuntu/Debian:
echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.listcurl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x2EE0EA64E40A89B84B2DF73499E82A75642AC823" | sudo apt-key addsudo apt updatesudo apt install sbtmacOS:
brew install sbtVerify with:
sbt --versionInstalling Verilator
Section titled “Installing Verilator”Ubuntu/Debian:
sudo apt install verilatormacOS:
brew install verilatorVerify with:
verilator --versionStep 1: Set Up the Project
Section titled “Step 1: Set Up the Project”Create a project directory with the following structure:
counter/├── build.sbt├── project/│ └── build.properties└── src/main/scala/counter/ ├── Counter.scala └── CounterSim.scalaFirst, create build.sbt in the project root. This tells sbt which version of Scala and SpinalHDL to use:
ThisBuild / version := "1.0.0"ThisBuild / scalaVersion := "2.12.18"
lazy val root = (project in file(".")) .settings( name := "counter", libraryDependencies ++= Seq( "com.github.spinalhdl" %% "spinalhdl-core" % "1.10.2a", "com.github.spinalhdl" %% "spinalhdl-lib" % "1.10.2a", compilerPlugin("com.github.spinalhdl" %% "spinalhdl-idsl-plugin" % "1.10.2a") ) )
fork := trueThe spinalhdl-core library provides the base HDL types and operators, spinalhdl-lib adds higher-level components, and the spinalhdl-idsl-plugin is a Scala compiler plugin that enables SpinalHDL’s DSL syntax.
Next, create project/build.properties:
sbt.version=1.9.7Step 2: Write the Counter Design
Section titled “Step 2: Write the Counter Design”Create src/main/scala/counter/Counter.scala:
package counter
import spinal.core._import spinal.lib._
// 8-bit Counter with enable and overflow detectioncase class Counter() extends Component { val io = new Bundle { val enable = in Bool() val count = out UInt(8 bits) val overflow = out Bool() }
// Internal counter register val counterReg = Reg(UInt(8 bits)) init(0)
// Counter logic when(io.enable) { counterReg := counterReg + 1 }
// Overflow detection (counter about to wrap) val overflowReg = Reg(Bool()) init(False) when(io.enable && counterReg === 255) { overflowReg := True } otherwise { overflowReg := False }
// Output assignments io.count := counterReg io.overflow := overflowReg}
// Generate Verilog RTLobject CounterVerilog extends App { SpinalConfig( targetDirectory = "rtl", defaultConfigForClockDomains = ClockDomainConfig(resetKind = SYNC) ).generateVerilog(Counter())}
// Generate VHDL RTLobject CounterVhdl extends App { SpinalConfig( targetDirectory = "rtl", defaultConfigForClockDomains = ClockDomainConfig(resetKind = SYNC) ).generateVhdl(Counter())}Here is what each part does:
case class Counter() extends Component— In SpinalHDL, every hardware module extendsComponent. Using a case class gives you Scala’s auto-generatedapplymethod, which makes instantiation cleaner.iobundle — TheBundlegroups all ports.in Bool()andout UInt(8 bits)declare direction and type. SpinalHDL catches width mismatches at compile time.Reg(UInt(8 bits)) init(0)— Declares an 8-bit register initialized to zero on reset. SpinalHDL automatically connects it to the clock domain.when/otherwise— Conditional logic, equivalent to Verilog’sif/elseinside analwaysblock.counterReg === 255— The triple-equals===is SpinalHDL’s hardware comparison operator (Scala’s==is reserved for software comparison).
The CounterVerilog and CounterVhdl objects at the bottom are optional entry points that generate synthesizable RTL files, showing off SpinalHDL’s ability to target both Verilog and VHDL from the same source.
Step 3: Write the Simulation Testbench
Section titled “Step 3: Write the Simulation Testbench”Create src/main/scala/counter/CounterSim.scala:
package counter
import spinal.core._import spinal.core.sim._
// Simulation that generates VCD waveform fileobject CounterSim extends App { // Configure simulation with VCD output val simConfig = SimConfig .withWave // Enable waveform generation .withConfig(SpinalConfig( defaultConfigForClockDomains = ClockDomainConfig(resetKind = SYNC) ))
simConfig.compile(Counter()).doSim { dut => // Fork a clock generation process val clockPeriod = 10 // 10 time units = 100 MHz dut.clockDomain.forkStimulus(period = clockPeriod)
// Initialize dut.io.enable #= false
// Wait for reset dut.clockDomain.waitSampling(5)
// Test 1: Enable counting println("Test 1: Enable counting") dut.io.enable #= true dut.clockDomain.waitSampling(20)
// Test 2: Disable counting println("Test 2: Disable counting") dut.io.enable #= false dut.clockDomain.waitSampling(5)
// Test 3: Resume counting println("Test 3: Resume counting") dut.io.enable #= true dut.clockDomain.waitSampling(10)
// Test 4: Count to overflow (limited cycles for reasonable VCD size) println("Test 4: Counting to overflow") var cycles = 0 var sawOverflow = false while (!sawOverflow && cycles < 260) { dut.clockDomain.waitSampling() cycles += 1 if (dut.io.overflow.toBoolean) { sawOverflow = true println(s" Overflow detected at cycle $cycles") } }
// Continue a bit after overflow dut.clockDomain.waitSampling(5)
println("Simulation complete!") }}The testbench uses SpinalHDL’s simulation API, which runs on top of Verilator:
SimConfig.withWave— Tells the simulator to record all signal changes to a VCD file.dut.clockDomain.forkStimulus(period = clockPeriod)— Spawns a background thread that toggles the clock. SpinalHDL also handles reset automatically at the start of simulation.#=— The simulation assignment operator. It drives a value onto a signal (likeforcein Verilog).dut.clockDomain.waitSampling(n)— Waits fornrising clock edges, keeping your test sequences cycle-accurate.dut.io.overflow.toBoolean— Reads the current value of a signal from the simulation.
The test exercises four scenarios: enabling the counter, pausing it, resuming, and running until the 8-bit value wraps from 255 back to 0.
Step 4: Build and Run the Simulation
Section titled “Step 4: Build and Run the Simulation”Run the simulation with sbt:
sbt "runMain counter.CounterSim"The first build takes several minutes because sbt downloads Scala, SpinalHDL, and Verilator dependencies. Subsequent runs are much faster.
When the simulation finishes, the VCD file is generated inside simWorkspace/Counter/test/. Copy it and fix the timescale for NovyWave:
cp simWorkspace/Counter/test/wave.vcd counter.vcdsed -i 's/$timescale 1s/$timescale 1ns/' counter.vcdYou should see console output like this:
Test 1: Enable countingTest 2: Disable countingTest 3: Resume countingTest 4: Counting to overflow Overflow detected at cycle 226Simulation complete!If you want to automate these steps, you can use a Makefile:
.PHONY: all sim clean
all: sim
sim: sbt "runMain counter.CounterSim" @if [ -f simWorkspace/Counter/test/wave.vcd ]; then \ cp simWorkspace/Counter/test/wave.vcd counter.vcd; \ sed -i 's/$$timescale 1s/$$timescale 1ns/' counter.vcd; \ echo "Copied to: counter.vcd (timescale fixed to 1ns)"; \ fi
clean: rm -rf target project/target simWorkspace rtl rm -f counter.vcdThen simply run:
makeStep 5: Open the Waveform in NovyWave
Section titled “Step 5: Open the Waveform in NovyWave”Launch NovyWave with the generated VCD file:
novywave counter.vcdOr open NovyWave and load the file through the UI:
- Open NovyWave
- Click Load Files
- Select
counter.vcd - Click Load
The file appears in Files & Scopes with this hierarchy:
counter.vcd └── TOP └── CounterStep 6: Explore the Waveform
Section titled “Step 6: Explore the Waveform”Add signals to the viewer
Section titled “Add signals to the viewer”- Click the checkbox next to TOP or Counter to select the scope
- In the Variables panel, you will see the signals available in the design. Click on these signals to add them to the waveform viewer:
clk— the clock signalreset— the synchronous resetio_enable— the enable inputio_count[7:0]— the 8-bit counter outputio_overflow— the overflow flagcounterReg[7:0]— the internal counter registeroverflowReg— the internal overflow register
Navigate the waveform
Section titled “Navigate the waveform”- Press R to fit the entire simulation into view
- Press W to zoom in around the beginning of the trace. You should see
resetheld high for the first few clock cycles while SpinalHDL’s automatic reset initializes the design - After reset deasserts, look for the point where
io_enablegoes high. From that moment,counterRegincrements by 1 on each rising clock edge - Use Shift+E to jump between transitions on the selected signal
Observe the key behaviors
Section titled “Observe the key behaviors”- Reset phase: During the first 5 clock cycles,
resetis high andcounterRegstays at 0 regardless ofio_enable - Counting: Once
io_enableis high andresetis low, the counter increments every clock cycle. Changeio_countformat to UInt to see decimal values climbing 0, 1, 2, 3, … - Pause and resume: When
io_enabledrops low, the counter holds its current value. Whenio_enablereturns high, counting resumes from where it left off - Overflow: When
counterRegreaches 255 andio_enableis high,overflowRegpulses high for one clock cycle as the counter wraps back to 0
Internal vs. external signals
Section titled “Internal vs. external signals”Because SpinalHDL’s Verilator simulation dumps all internal signals, you can compare counterReg (the internal register) with io_count (the output port). They carry the same value, but seeing both confirms that the output assignment works correctly.
SpinalHDL Tips
Section titled “SpinalHDL Tips”Generating RTL without simulation
Section titled “Generating RTL without simulation”If you just need synthesizable Verilog or VHDL (without running a simulation), use:
# Generate Verilogsbt "runMain counter.CounterVerilog"
# Generate VHDLsbt "runMain counter.CounterVhdl"Generated files appear in the rtl/ directory.
Controlling waveform output
Section titled “Controlling waveform output”SpinalHDL’s simulation API gives you several options:
// VCD output (default with .withWave)SimConfig.withWave
// FST output (smaller files, also supported by NovyWave)SimConfig.withFstWaveFST files are significantly smaller and faster to load in NovyWave for larger designs.
Next Steps
Section titled “Next Steps”- Try modifying the counter to count by 2 (change
counterReg + 1tocounterReg + 2) - Add a configurable width parameter using Scala generics
- Compare multiple simulation runs using the multi-file tutorial
- Explore the complete example project in examples/spinalhdl/counter/
Troubleshooting
Section titled “Troubleshooting”sbt not found
Section titled “sbt not found”Install sbt following the instructions above for your platform. Make sure it is on your PATH.
”java.lang.UnsupportedClassVersionError”
Section titled “”java.lang.UnsupportedClassVersionError””You need Java 11 or later. Check with java -version and install a newer JDK if needed.
verilator not found
Section titled “verilator not found”Install Verilator for simulation support. Without it, you can still generate RTL using the CounterVerilog or CounterVhdl entry points, but you cannot run the simulation.
First build is slow
Section titled “First build is slow”That is expected. SpinalHDL and Scala dependencies take time to resolve on the first run. Subsequent builds use cached dependencies and compile only changed files.
Empty VCD file
Section titled “Empty VCD file”Make sure .withWave is included in your SimConfig. Without it, the simulation runs but does not record signal transitions.
Large VCD files
Section titled “Large VCD files”For bigger designs, switch to FST format with .withFstWave instead of .withWave. FST files are 10-100x smaller and load faster in NovyWave.