VPI deep dive #
Verilog Procedural Interface deep dive in the context of cocotb with a focus on the icarus simulator
Good Resources on VPI #
Leaving make
#
In a previous section, we saw that make
called icarus verilog under the hood, running the final execution command:
vvp -M $(shell cocotb-config --lib-dir) -m $(shell cocotb-config --lib-name vpi icarus) $(SIM_ARGS) $(EXTRA_ARGS) $(SIM_BUILD)/sim.vvp $(PLUSARGS)
expanded, the gist of this command is
vvp -M /.venv/lib/python3.10/site-packages/cocotb/libs -mlibcocotbvpi_icarus
The command is pointing vvp
to cocotb’s common compiled libraries and then telling vvp
about a special module called libcocotbvpi_icarus
which, in short, “loads” cocotb into the simulator where it can then interact with the vpi_
interface provided byx vvp
.
When are Cocotb’s Libraries Built? #
Libraries are built during installation via pip. For more information read this link about how pip installation works.
In short: pip works by finding setup.py
and setup.cfg
in the root project directory. When you installed cocotb via pip it ran the setup.py
which called cocotb_build_libs.py
which built all the shared objects, or in icarus’s case - the .vpl
in the share/libs
folder.
How Does Icarus VPI Work With Cocotb? #
VPI itself (sans icarus) is a standardized set of functions that simulators can expose/implement to talk to the outside world (other programs). There are about 37 C functions in VPI in total.
cocotb wraps these in a C++ version in VpiImpl.cpp
and also wraps them in VpiCbHdl.cpp
. They might exist in a few other places too, but I haven’t found them yet. The following list is all the built in VPI functions called by cocotb and what they do:
function | description |
---|---|
vpi_handle() |
obtains a handle to any object with one-to-one relationship |
vpi_scan() |
Scans the Verilog HDL hierarchy for objects that have a one-to-many relationship |
vpi_iterate() |
Returns an iterator handle that you use to access objects that are in a one-to-many relationship |
vpi_get() |
reads any integer or boolean property |
vpi_get_str() |
reads any string property |
vpi_get_value() |
reads any logic value |
vpi_get_delays() |
reads any delay value |
vpi_get_time() |
Retrieves the current simulation time, using the timescale of the object |
vpi_get_vlog_info() |
not a symbol in the compiled objects, native to cocotb? |
vpi_free_object() |
Frees the memory that is allocated for VPI objects |
vpi_handle_by_name() |
Returns a handle to an object that has a specific name |
vpi_handle_by_index() |
Returns a handle to an object, using the object’s index number within a parent object |
vpi_control() |
Passes information from user code to the simulator |
vpi_put_data() |
Puts data into an implementation’s save/restart location |
vpi_register_cb() |
Registers a simulation-related callback |
vpi_remove_cb() |
Removes a simulation callback that has been registered vpi_register_cb() |
If you look at the icarus definition file you’ll see it contains many more VPI functions that are not available in cocotb (or not yet implemented).
Dialing in on libcocotbvpi_icarus.vpl #
What is libcocotbvpi_icarus.vpl
? Let’s take a look at the symbols in the compiled object using the nm
command
> nm libcocotbvpi_icarus.vpl
0000000000010000 D _vlog_startup_routines
00000000000098a4 T _vlog_startup_routines_bootstrap
U _vpi_chk_error
U _vpi_control
U _vpi_free_object
U _vpi_get
U _vpi_get_str
U _vpi_get_time
U _vpi_get_value
U _vpi_get_vlog_info
U _vpi_handle
U _vpi_handle_by_index
U _vpi_handle_by_name
U _vpi_iterate
U _vpi_put_value
U _vpi_register_cb
U _vpi_remove_cb
U _vpi_scan
The object includes the VPI functions in VpiImpl
and VpiCbHdl
. There are other symbols in this library as well, like the C++ functions and cocotbs gpi functions. Note that the VPI functions all have a U
flag in the nm
output - meaning they are undefined.
These are defined in other the vvp
program istelf. You can track this down by looking into library called out in the cocotb/share/def folder. The .def file for icarus has LIBRARY "vvp.exe"
at the top - the vvp
program itself. So under the hood icarus’s vvp
defines its VPI interface. If this theory is true, then nm
(other option is objDump -D
) on vvp
should show the vp_
definitions:
> nm vvp | grep "vpi_"
0000000100077c34 T _vpi_chk_error
0000000100077c3c T _vpi_compare_objects
0000000100079e24 T _vpi_control
0000000100079d80 T _vpi_flush
00000001000777b0 T _vpi_fopen
0000000100077d04 T _vpi_free_object
0000000100094ec0 t _vpi_free_object.cold.1
0000000100077d90 T _vpi_get
...
00000001000785bc T _vpi_get_time
0000000100081a80 T _vpi_get_userdata
000000010007917c T _vpi_get_value
0000000100078660 T _vpi_get_vlog_info
0000000100077998 T _vpi_handle
00000001000796cc T _vpi_handle_by_index
00000001000796ec T _vpi_handle_by_name
00000001000795dc T _vpi_iterate
000000010007728c T _vpi_mcd_close
...
0000000100079d50 T _vpi_printf
0000000100079cdc T _vpi_put_delays
0000000100081a3c T _vpi_put_userdata
000000010007932c T _vpi_put_value
there they are!
What happens when a simulation starts? #
Running make
in a cocotb project spits out the following:.
> make
rm -f results.xml
/Library/Developer/CommandLineTools/usr/bin/make -f Makefile results.xml
rm -f results.xml
MODULE=testbench TESTCASE= TOPLEVEL=fir TOPLEVEL_LANG=verilog \
/opt/homebrew/bin/vvp -M /.venv/lib/python3.10/site-packages/cocotb/libs -m libcocotbvpi_icarus sim_build/sim.vvp
-.--ns INFO gpi ..mbed/gpi_embed.cpp:110 in set_program_name_in_venv Using Python virtual environment interpreter at /.venv/bin/python
-.--ns INFO gpi ../gpi/GpiCommon.cpp:101 in gpi_print_registered_impl VPI registered
make
runs a call to vvp
which starts the cocotb<->vpi interaction via loading libcocotbvpi_icarus.vpl
into the simulator.
From resources linked above:
The simulator run time (The “vvp” program) gets a handle on a freshly loaded module by looking for the symbol “vlog_startup_routines” in the loaded module. This table, provided by the module author and compiled into the module, is a null terminated table of function pointers. The simulator calls each of the functions in the table in order.
Its important to emphasize the cocotb, from my understanding, runs entirely from the simulator. Cocotb is acting like a compiled library to the simulator even though much of the cocotb is written in python and not compiled. In the background, cocotb is secretly spinning up a python interpreter and the compiled C++ code we told the simulator about is offloading functionality to that python code.
To figure out what’s going on at startup, we need to track down cocotb’s startup routine table in vlog_startup_routines
. Since cocotb is loading module libcocotbvpi_icarus.vpl
into vvp
, all we need to do is look for the table definition in the compiled library source files - VpiImpl.cpp.
In VpiImpl.cpp
we see the definition of the special symbol vlog_startup_routines[]
and the table it provides:
COCOTBVPI_EXPORT void (*vlog_startup_routines[])() = {
register_impl, gpi_entry_point, register_initial_callback,
register_final_callback, nullptr};
investigating one routine at a time here…
register_impl() #
static void register_impl() {
vpi_table = new VpiImpl("VPI");
gpi_register_impl(vpi_table);
}
VpiImpl starts to bridge the gap between cocotb <-> gpi <-> vpi. files of interest (in order of call/relation)
file | description |
---|---|
VpiImpl.h |
definition of VpiImpl class, inherits from GpiImplInterface and overrides a bunch of virtual functions |
gpi_priv.h |
definition of GpiImplInterface , an abstract class with a bunch of virtual functions to override |
GpiCommon.cpp |
definition of gpi_register_impl function which just seems to create a vector of GpiImplInterface objects, not sure why there would be > 1?? perhaps you can register multiple interfaces (vpi, fli, vhpi, etc…) TODO: why? |
Here is just one function in VpiImpl
class traced from GpiImplInterface
class down to its VPI function supplied by vvp
virtual GpiImplInterface.get_sim_time()
which is a virtual function overriden byVpimpl.get_sim_time()
which callsvpi_get_time()
so register_impl()
just populates a VpiImpl
class with necessary stuff - function definitions, variables, etc. The underlying abstract class GpiImplInterface
is used in gpi_register_impl
where the vpi_table
is upcasted from a VpiImpl
to a GpiImplInterface
.
As a side note: I think what cocotb developes mean by “impl” is the simulator interface implementation - vpi, fli, vhpi…
gpi_entry_point() #
gpi_entry_point
does the following:
- loads extra GPI libs (GPI_EXTRA flag)
embed_init_python()
- embeds python into the C++ program (we can see this in the output ofmake
)gpi_print_registered_impl()
- prints that the implementation was loaded (we can see this the output ofmake
as well)
from the cocotb gitter:
GPI_EXTRA was traditionally reserved for registering additional GPI implementations for simulators that supported more than one interface
make
output:
> make
rm -f results.xml
/Library/Developer/CommandLineTools/usr/bin/make -f Makefile results.xml
rm -f results.xml
MODULE=testbench TESTCASE= TOPLEVEL=fir TOPLEVEL_LANG=verilog \
/opt/homebrew/bin/vvp -M /.venv/lib/python3.10/site-packages/cocotb/libs -m libcocotbvpi_icarus sim_build/sim.vvp
-.--ns INFO gpi ..mbed/gpi_embed.cpp:110 in set_program_name_in_venv Using Python virtual environment interpreter at /.venv/bin/python
-.--ns INFO gpi ../gpi/GpiCommon.cpp:101 in gpi_print_registered_impl VPI registered
register_initial_callback() #
static void register_initial_callback() {
sim_init_cb = new VpiStartupCbHdl(vpi_table);
sim_init_cb->arm_callback();
}
Cocotb passes vpi_table
, which is just the VpiImpl
class we setup previously, to VpiStartupCbHdl
. The VpiStartupCbHdl
class calls its constructor which gives the vpi_table
to both GpiCbHdl
and VpiCbHdl
, tracing the calls down:
Class/instantiation | defined in | Calls |
---|---|---|
VpiStartupCbHdl(vpi_table) |
VpiCbHdl.cpp |
GpiCbHdl(impl) , VpiCbHdl(impl) |
GpiCbHdl(impl) |
gpi_priv.h |
GpiHdl(impl) |
VpiCbHdl(impl) |
VpiCbHdl.cpp |
GpiCbHdl(impl) , populated cb_data and vpi_time structs |
GpiHdl(impl) |
gpi_priv.h |
sets up m_impl and m_obj_hdl=Null |
GpiImplInterface *m_impl
is the generic pointer to whatever implementation you are using - VPI/VHPI/FLI routines. m_obj_hdl
is set to null at this time (I think).
VpiStartupCbHdl
class and VpiCbHdl
class both inherit from GpiCbHdl
class, however GpiCbHdl
is a virtual base class of VpiCbHdl
. Therefore GpiCbHdl
only gets initialized once - and its the one from the VpiStartupCbHdl : GpiCbHdl(impl)
call.
During arm_callback()
, m_obj_hdl
is initiallys set vpiHandle new_hdl = vpi_register_cb(&cb_data);
cb_data reference #
typedef struct t_cb_data
{
PLI_INT32 reason; /* callback reason */
PLI_INT32 (*cb_rtn)(struct t_cb_data *); /* call routine */
vpiHandle obj; /* trigger object */
p_vpi_time time; /* callback time */
p_vpi_value value; /* trigger object value */
PLI_INT32 index; /* index of the memory word or
var select that changed */
PLI_BYTE8 *user_data;
} s_cb_data, *p_cb_data;
- reason an integer constant which represents what simulation activity will cause the callback to occur. defined in the
vpi_user.h
. - cb_rtn the name of the PLI routine which should be called when the specified simulation activity occurs.
- obj a handle for an object, not required for all callbacks.
- time a pointer to an s_vpi_time structure.
- value a pointer to an
s_vpi_value
structure. - index not used when a callback is registered.
- user_data user data value.
register_final_callback() #
this just ends/cleans up the simulation, not too interested in this function at the moment.
synopsis of startup routine #
VpiImpl
is called setting up the implementation interface and providing a class to upcast to gpigpi_entry_point()
links the C++ to python investigated further in embedded python section- sets up callbacks still not sure whats going on here TODO understand better
- registers a call back to cleanup simulation TODO understand better