Verilator C++ testbench
Description
This example shows how to create a C++ testbench and instantiate a Verilated RTL IP library.
In this case we will have an IP library called adder
, that is just a simple combinatorial adder with only 1 file adder.v
.
We will also have a C++ testbench main.cpp
Example
Directory structure
Lets take a look at the directory structure of the example first.
.
├── adder
│ ├── adder.v
│ └── CMakeLists.txt
├── CMakeLists.txt
├── deps
│ ├── CPM.cmake
│ └── deps.cmake
└── main.cpp
We have a directory adder/
that contains the adder
IP block, it as its own CMakeLists.txt
to make it easier to reuse in a larger project.
For a design this simple it is not really necessary to have a separate CMakeLists.txt
, but it is a good practice anyways.
adder/adder.v
Adder verilog file is just a simple two 8bit inputs, and a 9bit output module.
module adder (
input [7:0] a,
input [7:0] b,
output [8:0] o
);
assign o = a + b;
endmodule
adder/CMakeLists.txt
There is nothing new in this file from previous examples.
We are making a library under the full name cern::ip::adder::0.0.1
.
cmake_minimum_required(VERSION 3.25)
project(adder NONE)
add_ip(cern::ip::adder::0.0.1
DESCRIPTION "Just a simple adder"
)
ip_sources(adder VERILOG
${PROJECT_SOURCE_DIR}/adder.v
)
main.cpp
This is a standard C++ testbench using a Verilated model. Refer to Verilator documentation for more information.
#include <cstdlib>
#include <iostream>
#include <verilated.h>
#include <verilated_vcd_c.h>
#include "Vadder.h"
int main (int argc, char *argv[]) {
Verilated::traceEverOn(true);
Vadder dut;
VerilatedVcdC* m_trace;
m_trace = new VerilatedVcdC;
dut.trace(m_trace, 99);
m_trace->open("trace.vcd");
dut.eval();
for(int i =0; i<30; i++){
dut.a = i;
dut.b = i+10;
dut.eval();
m_trace->dump(10 * i + 10/2);
std::cout << (uint32_t)dut.a <<
" + " << (uint32_t)dut.b <<
" = " << (uint32_t)dut.o << "\n";
if(dut.o != 2*i+10){
std::cerr << "Mismatch\n DUT: " << dut.o << "\n REF: " << 2*i+10 << "\n";
std::exit(EXIT_FAILURE);
}
}
m_trace->close();
return 0;
}
CMakeLists.txt
And finally we need a top CMakeLists.txt
that will assemble the full design and create simulation targets.
cmake_minimum_required(VERSION 3.25)
project(example CXX C)
include("deps/deps.cmake")
add_subdirectory(adder)
verilator(cern::ip::adder::0.0.1 TRACE)
add_executable(testbench main.cpp)
target_link_libraries(testbench cern::ip::adder::0.0.1::vlt)
help()
We can add the adder
IP as a subdirectory with add_subdirectory()
CMake function.
Then we are creating the target to Verilate the IP.
Executing the cern__ip__adder__0.0.1_verilate
target will compile a static library of the IP block.
This time we are not asking Verilator to create a main.cpp
file as we will write it ourselves.
We are also passing TRACE
argument to verilate()
, indicating that we want to enable VCD
generation in Verilated model.
After that we create an executable
with add_executable()
CMake function. The first argument is the name of the compiled executable, and then we pass a list of sources, in this case only main.cpp
Finally we use target_link_libraries()
, to link the Verilated static library into our testbench executable, and also add the include paths for headers of the Verilated model, and Verilator headers.
Notice in the line 11, we are linking to the static library created with Verilator.
It is a bit annoying to have to change the version number manually, in order to avoid it it is a better idea to set a variable in adder/CMakeLists.txt
that will hold the full name of the IP.
...
if(NOT PROJECT_IS_TOP_LEVEL) # Avoid warning if project is top level
set(ADDER_LIB_NAME ${IP} PARENT_SCOPE)
endif()
And then in CMakeLists.txt
:
...
target_link_libraries(testbench ${ADDER_LIB_NAME}__vlt)
Build graph
Take a look at the build graph for this example.
We can see that the testbench
executable depends on Verilated models and Verilator targets.
Executing the testbench
targets, the dependencies will be built first.
Running the simulation
Simulation can be run the same way as always:
mkdir build
cd build
cmake ../ # Configure project
make testbench -j$(nproc) # Build testbench
./testbench # Execute testbench
Use bash-completion to autocomplete make
target names.
After typing make
press Tab ↹
twice.