HardwareTeams.com - The #1 job board and blog for electrical and computer engineers


VPI deep dive #

Verilog Procedural Interface deep dive in the context of cocotb with a focus on the icarus simulator

Good Resources on VPI #

  1. wikipedia
  2. icarus verilog description
  3. simple example
  4. good powerpoint

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 by Vpimpl.get_sim_time() which calls vpi_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:

  1. loads extra GPI libs (GPI_EXTRA flag)
  2. embed_init_python() - embeds python into the C++ program (we can see this in the output of make)
  3. gpi_print_registered_impl() - prints that the implementation was loaded (we can see this the output of make 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 #

  1. VpiImpl is called setting up the implementation interface and providing a class to upcast to gpi
  2. gpi_entry_point() links the C++ to python investigated further in embedded python section
  3. sets up callbacks still not sure whats going on here TODO understand better
  4. registers a call back to cleanup simulation TODO understand better
HardwareTeams.com Copyright © 2024
comments powered by Disqus