SystemVerilog DPI
Introduction
Direct Programming Interface (DPI) gives SystemVerilog (SV) the ability to call functions written in other languages. It can be thought of as an interface between SV and other programming languages. For this article, we will focus on calling C functions from the SV side. The main advantage is that one can use highly optimised C libraries for processing data (I’d any day prefer writing C code over SV code). There are some excellent tutorials out there which cover all aspects of DPI. In fact, I wrote most of my code by following these articles:
- How to Call C-functions from SystemVerilog Using DPI-C by AMIQ Consulting
- SystemVerilog DPI Tutorial
- Passing and receiving different SystemVerilog datatypes with C/C++ using DPI · GitHub
If you are starting out, I’d strongly advice you to go through the above tutorials first. They are thorough and complete. However, I ran into some issues and spent hours ironing out a few pesky bugs. The aim of this article is to list down some general advice which I believe will be helpful. Think of it as a random collection of workarounds and hacks. I am no expert on either DPI-C or SystemVerilog. So I could be wrong. There could even be better, more efficient workarounds. Feel free to reach out to me in such cases. Before I start, let me mention that the code has been tested on Aldec Riviera and Questa. Can’t vouch for other simulators. Let’s begin!
Transferring data
Firstly, let’s talk about passing data from SV to C and vice-versa. It can be frustrating at times. e.g. you cannot directly pass SV vectors which have more than 32 bits. The aforementioned tutorials explain how SV data types are mapped to their C equivalents. However, the C-equivalent data types (such as svOpenArrayHandle and svBit) can be daunting at first (If you haven’t heard of them, visit the links mentioned in the previous section). Don’t worry: they are simple typedefs. Knowing these will go a long way in understanding the limitations of passing data using DPI. Note that although DPI has been standardised by IEEE, it is implemented differently by different simulators. As a result, these typedefs may vary. These typedefs (and functions which we will discuss later) can be found in a DPI header file which is included with each simulator.
DPI data type | Typedef |
---|---|
svBit | uint8_t |
svBitVecVal | uint32_t |
svLogicVecVal | A union of 4 uint32_t values |
svOpenArrayHandle | void * |
I avoided using svLogicVecVal since svBitVecVal is easier to understand and manipulate.
For SV signals which are less than 32 bits wide, you can use svBitVecVal or svBit to pass it to the C function. To assign SV signals from the C code, you have to pass them by reference. However, if you only plan to read the SV values on the C side, passing by value should ideally work. It didn’t in my case. I had to pass them by reference. So, if you face the same issue, try passing them by reference.
As you might have guessed, passing arrays or signals with width > 32 bits needs to be treated carefully since it involves a void * pointer. To pass more than 32 bits, one can:
-
Pack it into an array. e.g.
//packing the vector into an array logic [127:0] var_original; typedef logic [31:0][4] bit_to_array; logic [31:0] var_dpi [4]; //you could use int instead of logic [31:0] var_dpi = bit_to_array’(var_original); //DPI function declaration import "DPI-C" function void func1 ( input bit [31:0] var []; ); //Actual DPI call func1(var_dpi)
-
Pass var_dpi to the C function with argument type as svOpenArrayHandle
//C function declaration void func1 ( svOpenArrayHandle var );
As mentioned in the table, svOpenArrayHandle is just a typedef for void . To read the array elements, use the DPI array manipulation functions instead of manually calculating the address i.e. In the above example, use
*(uint32_t *)(svGetArrElemPtr1(var, i));
to get the ith element of the array instead of*((uint32_t *)var+ i)
Similar to reading a value from the SV array, there exists functions for writing a value into the array. However, the corresponding /svPutArrElem/ functions *did not work. I had to resort to pointer arithmetic to dump values into the array:*((uint32_t *)var + i) = x
where x is the value to be assigned to the ith element. The important thing to note is that for writing data into the array, the DPI function definition on the SV side MUST declare the size of the array you are planning to pass i.e.//This DOES NOT work import "DPI-C" function void func1 ( output bit [31:0] var []; ); //This works import "DPI-C" function void func1 ( output bit [31:0] var [4]; );
I ran into this issue ONLY while writing values into the array. I could read values without declaring the size in the DPI function declaration. In both cases, you need to also ensure that memory for the array is already allocated on the SV side. One can also use the svGetArrElemSize function to get the size of the array. Be careful: this function may return the number of bytes instead of number of elements i.e. if the SV array is an array of 5 integers, this function may return 5*4 = 20 instead of 5.
Choosing DPI-C data types
Given a SV signal/variable, how do you choose the equivalent DPI-C type and more importantly, how can you pass them to the C side and vice-versa? These rules might help:
-
If the SV variable is a list, ensure that each element of the list <= 32 bits wide. For lists with elements more than 32 bits wide, split the elements into chunks of 32 bits and make separate lists for each chunk. Then pass it to the DPI-C side with argument type as svOpenArrayHandle. Access the elements using the aforementioned functions and process them. For lists with elements which are more than 32 bits wide, concatenate them back on the SV side after processing them using DPI-C.
//Declare list int list1 [32]; //initialise it foreach(list1[i]) list1[i] = i;
//DPI declaration import "DPI-C" function void func_list ( input int var []; //specify the size (32) in case you want to write to the same array );
//DPI-C function void func_list (svOpenArrayHandle var) { //Use svGetArrElemPtr to access the elements }
-
If the SV variable is a typedef: use svBitVecVal since 32 bits will suffice in most cases.
-
We have already discussed the technique for passing signals which have more than 32 bits in the previous section. Given below is one more example which uses a byte array instead of a logic [31:0] array. Note that the vector, which has 100 bits, can still be packed into a byte array even though 100 is not a multiple of 8.
//typecasting the signal logic [99:0] var_original; typedef byte [13] bit_to_byte_array; byte var_array [13]; var_array = bit_to_byte_array’(var_original); //DPI function declaration import "DPI-C" function void func1 ( input byte var [13] ); //Actual DPI call func1(var_dpi)
On the C side, one can use:
void func1 (svOpenArrayHandle var) { int i = 0 //element index int elem0 = *(uint8_t *)(svGetArrElemPtr1(var, i)); //read *((uint8_t *)var + i) = elem0 * 2; //multiply byte i with 2 }
After processing it on the C side, you can unroll the array into a 100-bit vector if required:
logic [99:0] var_final; typedef logic [99:0] byte_array_to_bit; //assuming var_array is the processed output of DPI-C var_final = byte_array_to_bit’(var_array);
-
For SV signals with less than 32 bits, one can use svBitVecVal as discussed previously
Including DPI-C functions
We need to include these DPI-C functions during simulation so that they can be called by the SV code. If you have multiple C files, consider maintaining a single file which simply imports all the C code. e.g. if you have /lib1.c/ and /lib2.c/ which contain DPI functions, maintain a /top_dpi.c/ file:
//top_dpi.c
#include "lib1.c"
#include "lib2.c"
Next, we need to compile this file and create a shared object. Think of it as a library which is included by the simulator. Riviera comes with a C compiler: it’s called ccomp. Generate the shared object using the following command:
ccomp -dpi dpi/top_dpi.c -Ic/ -o c/dpi.so
Let’s break this down:
- ccomp is the compiler.
- /top_dpi.c/ is our top file, which in turn includes all the C files containing the actual DPI functions.
- /-I
/ includes a folder during compilation. In our case, we specify the path which contains /lib1.c/ and /lib2.c/ - /-o c/dpi.so/ is the output path of the shared object.
For Questa, use gcc:
gcc -shared -fPIC -o c/dpi.so dpi/top_dpi.c -Ic/
The flags are similar to the ones used for Riviera.
Then, you need to add this library to the simulator by providing the shared object path to the -sv_lib flag. Go through the simulator docs for more details since this step will vary from simulator to simulator.
That’s it. If you run into more issues, keep debugging. And write a blog post of your own to help the next victim!