Home / Resources / Manipulation of icarus simulator with Verilog Programming Interface (VPI)
Introduction
Icarus Verilog is an implementation of the Verilog hardware description language Its compiler translates Verilog source code into executable programs for simulation, or other netlist formats for further processing. It supports the 1995, 2001 and 2005 versions of the standard, portions of SystemVerilog, and some extensions.
The Verilog Procedural Interface (VPI) is an interface primarily intended for the C programming language. It allows behavioral Verilog code to invoke C functions, and C functions to invoke standard Verilog system tasks. The Verilog Procedural Interface is part of the IEEE 1364 Programming Language Interface standard
Here is shown how to create user-defined Verilog tasks in C and write callbacks to manipulate the simulator.
User-defined tasks
Using the VPI, we can write a user-defined Verilog task in C language. To do so, we must create 2 functions:
- one to define the behaviour of the task (its core)
- the other register it to the Verilog tasks (and map a Verilog task name to the C function)
C code
Here is a C function to define a task that increments all its arguments (considered as integers): +1 to the first arg, +2 to the second arg, etc.
static int32_t myCFunc_increment(
char* theUserData //!< Pointer to the user data
) {
int32_t theLocalValue; // to manipulate an integer locally
int32_t i; // to add more than 1
vpiHandle theTaskHandle, theArgsListHandle, theArgHandle;
struct t_vpi_value theArgStruct;
if ( strlen(theUserData) > 0 ) {
vpi_printf("The user data: %s\n", theUserData);
}
// obtain a handle to the argument list:
theTaskHandle = vpi_handle(vpiSysTfCall, NULL);
theArgsListHandle = vpi_iterate(vpiArgument, theTaskHandle);
i = 0;
for (
theArgHandle = vpi_scan(theArgsListHandle);
theArgHandle != NULL;
theArgHandle = vpi_scan(theArgsListHandle)
) {
// grab the value of the argument as an integer:
theArgStruct.format = vpiIntVal;
vpi_get_value(theArgHandle, &theArgStruct);
theLocalValue = theArgStruct.value.integer;
vpi_printf("VPI routine received %d\n", theLocalValue);
// increment the value and put it back as argument:
theArgStruct.value.integer = theLocalValue + 1 + i;
vpi_put_value(theArgHandle, &theArgStruct, NULL, vpiNoDelay);
i++;
}
// cleanup:
vpi_free_object(theTaskHandle);
// No need to free theArgsListHandle and theArgHandle: the iterator objects
// shall automatically be freed when vpi_scan() returns NULL.
return 0;
}
All functions can receive user-defined data that is chosen when the task is registered. We write a register function to do so:
void register_increment(
void
) {
s_vpi_systf_data theTaskData;
theTaskData.type = vpiSysTask;
theTaskData.tfname = "$increment";
theTaskData.calltf = myCFunc_increment;
theTaskData.compiletf = NULL;
theTaskData.user_data = "My user data";
vpi_register_systf(&theTaskData);
}
And we can add the register function in the externally visible
vlog_startup_routines[]
array.
void (*vlog_startup_routines[])( void ) = {
register_increment,
0
};
Verilog
This user-defined task can be tested from a Verilog module.
`timescale 1ns/100ps
module verilog();
reg[7:0] val1, val2;
initial begin
val1 = 41;
val2 = 14;
$increment(val1, val2);
$display("After $increment: val1 =%d, val2 =%d", val1, val2);
end
endmodule
Makefile
Now, we must call Icarus Verilog to compile the VPI extension and the testbench. Then we execute the resulting binary and observe the results.
Here is a Makefile that provides the recipe (make sure to indent with tabulations):
HW = verilog.v
SRC = increment.c
VPI = $(addsuffix .vpi, $(basename $(notdir ${SRC})))
OUT = $(addsuffix .o, $(basename $(notdir ${SRC})))
all: $(VPI) binary
vvp -M. -m$(basename $(VPI)) binary
binary : ${HW}
iverilog -o $@ $^
$(VPI) : ${SRC}
iverilog-vpi $^
clean:
$(RM) $(VPI) $(OUT) binary
We expect both integers val1
and val2
to be set to 42 and 16, respectively.
Callbacks
VPI callbacks allow a user to request that a Verilog HDL software product, such as a Icarus Verilog, call a user-defined function when a specific event occurs. Here too, we must define 2 functions.
Here is a C function to print to standard output the list of ports of every top modules.
static int32_t CFunc_listPorts(
p_cb_data theCallbackDataPtr //!< pointer to the callback data
) {
vpiHandle theModuleIterator, theModuleHandle;
vpiHandle thePortIterator, thePortHandle;
PLI_INT32 thePortDirection, thePortSize;
// make sure this function is not called when not supposed to:
if ( theCallbackDataPtr->reason != cbEndOfCompile ) {
return 1;
}
// iterate on all the top modules:
theModuleIterator = vpi_iterate( vpiModule, NULL );
if ( theModuleIterator != NULL ) {
for ( theModuleHandle = vpi_scan(theModuleIterator);
theModuleHandle != NULL;
theModuleHandle = vpi_scan(theModuleIterator)
) {
vpi_printf("Top Module: %s\n", vpi_get_str(vpiFullName, theModuleHandle));
// iterate on all the top modules:
theModuleIterator = vpi_iterate( vpiModule, NULL );
if ( theModuleIterator != NULL ) {
for ( theModuleHandle = vpi_scan(theModuleIterator);
theModuleHandle != NULL;
theModuleHandle = vpi_scan(theModuleIterator)
) {
vpi_printf("Top Module: %s\n", vpi_get_str(vpiFullName, theModuleHandle));
// iterate on all the ports:
thePortIterator = vpi_iterate(vpiPort, theModuleHandle);
if ( thePortIterator != NULL ) {
for ( thePortHandle = vpi_scan(thePortIterator);
thePortHandle != NULL;
thePortHandle = vpi_scan(thePortIterator)
) {
thePortDirection = vpi_get(vpiDirection, thePortHandle);
switch ( thePortDirection ) {
case vpiInput:
vpi_printf(" input ");
break;
case vpiOutput:
vpi_printf(" output ");
break;
case vpiInout:
vpi_printf(" inout ");
break;
case vpiMixedIO:
vpi_printf(" mixed ");
break;
default:
vpi_printf(" unkown ");
break;
}
thePortSize = vpi_get(vpiSize, thePortHandle);
if ( thePortSize > 1 ) {
vpi_printf("[%d:0] ", thePortSize-1);
}
vpi_printf("%s\n", vpi_get_str(vpiName, thePortHandle));
}
}
}
}
/**
* No need for a cleanup: the iterator object shall automatically be freed
* when vpi_scan() returns NULL.
*/
return 0;
}
The register function makes sure this function is called at the end of compilation.
void register_callbacks(
void
) {
s_cb_data theCallbackData;
theCallbackData.reason = cbEndOfCompile;
theCallbackData.cb_rtn = CFunc_listPorts;
theCallbackData.user_data = 0;
vpi_register_cb(&theCallbackData);
}
Associated resources
Here is an archive which contains the discussed examples and their respective Makefiles.