Rapidvpi
Blazingly fast, modern C++ API using coroutines for efficient RTL verification and co-simulation via the VPI interface
Install / Use
/learn @ArcSpecter/RapidvpiREADME

RapidVPI
Blazingly fast, modern C++ API using coroutines for efficient RTL verification and co-simulation via the VPI interface.
Table of Contents
- Introduction
- Why RapidVPI?
- Prerequisites
- Installation (Linux)
- Quick start
- Writing RapidVPI test code
- RapidVPI Internal Architecture
- Organization of RapidVPI Test Project
- CMake Custom Commands for rtl_template
- RapidVPI API coroutines
- User coroutines
- Usage of RapidVPI
Introduction
The RapidVPI API allows you to write modern C++ code for verification and co-simulation of digital RTL HDL designs using any simulator which supports VPI interface. Currently, as of now this library was tested with Iverilog. The API abstracts many of the VPI related boilerplate and tedious mechanisms and implements all the necessary signal driving and reading via the convenient coroutine mechanism native to C++ since version 20. User just creates the coroutine awaitable object, adds operations to it such as read, write and suspends it with co_await statement.
Why RapidVPI?
The motivation for the development of this library was the ability to use all the flexible and advanced features of modern C++ during the RTL verification or co-simulation since verification itself is a software task and should use the proper software design tool such as C++; SystemVerilog is not a tool which is as powerful as C++ when it comes to modeling some advanced complex system. Additionally, SystemVerilog running with all of its features requires a very expensive license for the EDA tools, such a basic feature as randomization of variables in a class for example is not supported even in a lower tier paid simulators. The VPI interface on the other hand is supported by most lowest tier packaged commercial simulators, and of course it is supported by Iverilog which is a free and fast tool for Verilog simulation.
Prerequisites
ninja, gcc >14.2, cmake >3.1, iverilog (tested with), gtkwave (optional)
Installation (Linux)
The following environment variable must be set: VPI_INCLUDE_DIR - pointing to the location of vpi_user.h (For example, ~/eda/quartus25_1/questa_fse/include/ where QuestaSim installed)
Get the repository, compile and install:
git clone https://github.com/ArcSpecter/rapidvpi.git
cd ./rapidvpi/main
mkdir -p cmake-build-release && cd cmake-build-release
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --target rapidvpi.vpi -j$(nproc) && cd ..
sudo cmake --install ./cmake-build-release
Above will install the library and its .so files and necessary headers in appropriate locations (/usr/local/src, /usrc/local/lib, /usr/local/include) in a system, you should see output like this:
-- Install configuration: "Release"
-- Up-to-date: /usr/local/include/rapidvpi/core
-- Installing: /usr/local/include/rapidvpi/core/core.hpp
-- Up-to-date: /usr/local/include/rapidvpi/scheduler
-- Installing: /usr/local/include/rapidvpi/scheduler/scheduler.hpp
-- Up-to-date: /usr/local/include/rapidvpi/testmanager
-- Installing: /usr/local/include/rapidvpi/testmanager/testmanager.hpp
-- Up-to-date: /usr/local/include/rapidvpi/testbase
-- Installing: /usr/local/include/rapidvpi/testbase/testbase.hpp
-- Up-to-date: /usr/local/src/rapidvpi/core
-- Installing: /usr/local/src/rapidvpi/core/core.cpp
-- Up-to-date: /usr/local/src/rapidvpi/scheduler
-- Installing: /usr/local/src/rapidvpi/scheduler/scheduler.cpp
-- Up-to-date: /usr/local/src/rapidvpi/testmanager
-- Installing: /usr/local/src/rapidvpi/testmanager/testmanager.cpp
-- Up-to-date: /usr/local/src/rapidvpi/testbase
-- Installing: /usr/local/src/rapidvpi/testbase/awaitchange.cpp
-- Installing: /usr/local/src/rapidvpi/testbase/awaitread.cpp
-- Installing: /usr/local/src/rapidvpi/testbase/awaitwrite.cpp
-- Installing: /usr/local/src/rapidvpi/testbase/testbase.cpp
-- Installing: /usr/local/src/rapidvpi/testbase/utility.cpp
-- Installing: /usr/local/src/rapidvpi/entry.cpp
-- Installing: /usr/local/lib/rapidvpi/librapidvpi.vpi.so
-- Up-to-date: /usr/local/lib/cmake/rapidvpi/rapidvpiTargets.cmake
-- Up-to-date: /usr/local/lib/cmake/rapidvpi/rapidvpiTargets-release.cmake
-- Up-to-date: /usr/local/lib/cmake/rapidvpi/rapidvpiConfig.cmake
-- Up-to-date: /usr/local/lib/cmake/rapidvpi/rapidvpiConfigVersion.cmake
Quick start
Build the vip_template shared co-simulation .so file:
cd ./vip_template
mkdir -p cmake-build-debug && cd cmake-build-debug
cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ..
cmake --build . --target vip_template -j$(nproc)
Above will produce libvip_template.so file
Next we navigate to rtl_template, compile the RTL and run the co-simulation with .so file built previously under vip_template:
cd ../../rtl_template
mkdir build
cd ./build
cmake .. && cd ..
cmake --build ./build/ --target sim_compile
cmake --build ./build/ --target sim_run
And get the output of the simulation as well as recorded test.vcd inside ./build/sim folder under ./rtl_template:
Top level DUT: dut_top
VCD info: dumpfile ./sim/test.vcd opened for output.
Awaited Numeric value for the 'c' is : c000000007
Awaited Hex String value for the 'c' is : C000000007
Awaited Bin String value for the 'c' is : 0000000000000000000000001100000000000000000000000000000000000111
numeric value of 'c' is: c000000007
hex string is: C000000007
bin string is: 0000000000000000000000001100000000000000000000000000000000000111
some_func() called
value received during test creation: 42
[100%] Built target sim_run
Alternatively you can just run automated script located within ./rapidvpi/templates folder:
chmod +x ./quick_start.sh
./quick_start.sh
Assumption is that everyone has his own CMake plugin in IDEs like VSCode and one would just run automated commands from GUI mode. Also, some people might just use IDEs like CLion for working on C++ portion of verification like vip_template. I personally use CLion for it.
Writing RapidVPI test code
For example, in a below code fragment we are looking at scenario of some module's simulation start. What we do is, set all inputs to zero by default, then after 10 ns we release the reset. Then we show forcing port "c" of DUT to some value and releasing that force 2 ns after. In order to get that job done we have a run() coroutine declared and its implementation shown in the code.
In this current example we are writing values to DUT ports. In order to write a value one first creates a writer awaitable object using the getCoWrite(), the argument to getCoWrite is basically a delay (by default in nanoseconds) from the current timeline to when the write of all the signals should occur. Once that event occurs, the code proceeds below the line co_await awaiter, after that all the scheduled write events get automatically cleared for the awaiter object (which by the way can have any name).
Then, since awaiter was already declared we can just keep reusing it, for example modify its delay with awater.setDelay(10) line, now the delay is 10 nanoseconds from the current timeline (from the timeline which is completed as per previous request, when the delay was 0 ns). Next, we add new write operation, this time port "rst" has to be written with value 1. Then we again schedule the write and further code execution blocked until write happens.
Force and release operations also belong to the write group, thus also carried out in a similar fashion as shown in snippet below.
The run() coroutine shown below is just example of some single user defined RunTask coroutine, however; user can have as many as he wants such coroutines executing concurrently.
TestImpl2::RunTask TestImpl2::run() {
auto awaiter = test.getCoWrite(0); // Create awaitable object for write operations with given delay
awaiter.write("clk", 0); // Start adding the write operations to any signals of DUT
awaiter.write("a", 0); // Currently setting ports clk,rst,a,b of DUT to 0
awaiter.write("b", 0);
awaiter.write("rst", 0);
co_await awaiter; // Schedule events and suspend coroutine
awaiter.setDelay(10); // Modify now delay for the previously created awaiter object
awaiter.write("rst", 1); // Schedule another write of value 1 to rst port of DUT
co_await awaiter;
auto await_force = test.getCoWrite(12); // Create new awaitable object for writing
await_force.force("c", 0xabcd); // This time force signal to certain value 12 ns from current time
co_await await_force;
auto await_release = test.getCoWrite(2); // Create a new awaitable object
await_release.release("c"); // Release the forcing for that signal 2 ns after
co_await await_release;
co_return;
}
Every time you obtain an awaitable object using getCoWrite(timedelay), the timedelay is in nanoseconds by default, however; if different time unit required one can use templated arguments and do it like:
auto awaiter = test.getCoWrite<ms>(0); // valid units are: ms, us, ns, ps
Another example now for reading is below. We want to read value of "c" port of DUT at current value (without delay, thus 0). We create awaitable object obtained from getCoRead, we schedule read operation for port "c", we suspend coroutine with co_await. Once the read is done, the value is internally stored in awRd and we can obtain it in several ways, a hex string, a binary string o
