Skip to main content

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.

tip

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.

adder/adder.v
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.

adder/CMakeLists.txt
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.

main.cpp
#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.

CMakeLists.txt
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.

tip

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.

adder/CMakeLists.txt
...
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:

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.

graph
graph

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
tip

Use bash-completion to autocomplete make target names. After typing make press Tab ↹ twice.