准备
SV可以通过DPI(Direct Programming Interface)和C/C++代码交互;DPI作为编程接口的演进过程可从🔗Verilog PLI已死( 可能), SystemVerilog DPI当立中了解,相比TF (Task / Function),ACC (Access)和VPI (Verification Procedural Interface)这类编程接口,DPI更适合C程序的直接调用,不涉及内存的来回拷贝,效率更高,部署也更方便;推荐🔗《SystemVerilog for Verifaction(Third Edition)》 Chapter12 和 🔗Cadence 《SystemVerilog DPI Engineering NoteBook》作为参考资料,IEEE协议作为查阅资料。
数据结构
DPI中C和SV之间的数据传输,本质是仿真器在C/SV两侧调度切换时,利用指针来操作同一类数据结构的内存空间,指针在被使用前,只需要在C或SV中的一侧被初始化分配内存空间即可。用户需要保证C/SV两侧的数据格式相匹配(vcs会自动生成函数原型文件vc_hdrs.h
,用户可参考这个文件; xrun使用-dpiheader
和-dpiimpheader
产生export,import函数的头文件),DPI支持的数据格式除C语言内置类型外,在$VCS_HOME/include/svdpi.h
中有声明,一些函数原型(包含 获取指针,从指针位置取数据,在指针位置放数据,获取数组index,等)也在此头文件中。
数据结构映射如下:
svBit
SV中最小的数据格式为2值1位宽的bit,C语言最小的为8位宽的char字节;所以C中svBit
对SV中bit
的映射,其实是unsigned char
类型。所以高位bit是可以被非法赋值的,使用时需注意。建议在C侧做mask处理。
svBitVecVal
C中svBitVecVal
类型是unsigned int
类型,和SV中的bit[N:0]
相映射。对于SV侧传入大于32bit的packed array,C侧需要拆分为svBitVecVal
类型的数组。可以调用C侧的宏SV_PACKED_DATA_ELEMS()
获得拆分后的数组宽度,在利用svGetSelectBit
或者memcpy
从中取出数据。
open arrays
如下示例中:
svSizeOfArray
获取数组大小为多少bytes,这里为4*4=16个;
p = (int*)svGetArrayPtr(a);
获取数组的指针,并强制转换为int类型指针;
q[i]=p[i] + p[i];
直接使用指针索引,相当于数组索引;
svGetArrElemPtr1(a,low);
从1维数组a
指针中获取index=low
的地址
temp_data[0] = *(int*)svGetArrElemPtr1(a,low);
第一个*
为解引用操作符,根据地址取数据;(int*)
将地址强制转化为int类型地址;所以*(int*)svGetArrElemPtr1(a,low)
是从内存地址svGetArrElemPtr1(a,low)
中取出int类型的4 bytes数据,存入temp_data[0]
中。
C:
#include <stdio.h>
#include "svdpi.h"
#include "vpi_user.h"
#include "stdlib.h"
/* nbcode "addarray" start */
void add_array(int test, const svOpenArrayHandle a, const svOpenArrayHandle b)
{
int i;
int temp_data[4];
int *p, *q, r[4] ;
int length,left,right,low,high;
/* Obtains the left index of the array */
/* The second argument '1' corresponds to the first unpacked dimension of the array */
/*
p = (int*)malloc(sizeof(int)*5);
q = (int*)malloc(sizeof(int)*5);
*/
left = svLeft(a,1);
/* Obtains the lowest index of the array */
low = svLow(a,1);
/* Obtains the right index of the array */
right = svRight(a,1);
/* Obtains the highest index of the array */
high = svHigh(a,1);
/* Obtains the size of the array */
length = svSizeOfArray(a);
vpi_printf("Left index of array = %d\n",left);
vpi_printf("Right index of array = %d\n",right);
vpi_printf("Low index of array = %d\n",low);
vpi_printf("High index of array = %d\n",high);
vpi_printf("size of array = %d\n", length);
if(test == 1) {
p = (int*)svGetArrayPtr(a);
q = (int*)svGetArrayPtr(b);
vpi_printf("Using normalized indices\n");
for(i=0;i<4;i++){
q[i]=p[i] + p[i];
vpi_printf("a[%d] = %d\n",i,p[i]);
vpi_printf("b[%d] = %d\n",i,q[i]);
}
}
else{
/* Returns the value that corresponds to Verilog index -1 of input array */
temp_data[0] = *(int*)svGetArrElemPtr1(a,low);
/* Returns the value that corresponds to Verilog index 0 of input array */
temp_data[1] = *(int*)svGetArrElemPtr1(a,low+1);
/* Returns the value that corresponds to Verilog index 1 of input array */
temp_data[2] = *(int*)svGetArrElemPtr1(a,low+2);
/* Returns the value that corresponds to verilog index 2 of input array */
temp_data[3] = *(int*)svGetArrElemPtr1(a,high);
vpi_printf("Using verilog indices\n");
for(i=0;i<4;i++){
r[i]= temp_data[i] + temp_data[i];
vpi_printf("a[%d] = %d\n",i,temp_data[i]);
vpi_printf("b[%d] = %d\n",i,r[i]);
}
}
}
/* nbcode "addarray" end */
SV:
module top;
int input_1 [-1:2];
int output_1[-1:2];
int input_2 [-1:2];
int output_2[-1:2];
int test = 1;
int cnt = 1;
// nbcode "importarray" start
import "DPI-C" function void add_array(input int test, input int i[],output int o[]);
// nbcode "importarray" end
initial begin
for (int j = -1; j < 3; j++)
begin
input_1[j] = cnt;
$display("input_1[%d] = %h",j,input_1[j]);
cnt++;
end
add_array(test,input_1,output_1);
for (int j = -1; j < 3; j++)
begin
input_2[j] = cnt;
$display("input_2[%d] = %h",j,input_2[j]);
cnt++;
end
add_array(0,input_2,output_2);
end
endmodule
LOG:
vcs -full64 \
-kdb -lca \
-sverilog \
-l vcs.log \
./add.c \
./top.v \
-R
input_1[ -1] = 00000001
input_1[ 0] = 00000002
input_1[ 1] = 00000003
input_1[ 2] = 00000004
Left index of array = -1
Right index of array = 2
Low index of array = -1
High index of array = 2
size of array = 16
Using normalized indices
a[0] = 1
b[0] = 2
a[1] = 2
b[1] = 4
a[2] = 3
b[2] = 6
a[3] = 4
b[3] = 8
input_2[ -1] = 00000005
input_2[ 0] = 00000006
input_2[ 1] = 00000007
input_2[ 2] = 00000008
Left index of array = -1
Right index of array = 2
Low index of array = -1
High index of array = 2
size of array = 16
Using verilog indices
a[0] = 5
b[0] = 10
a[1] = 6
b[1] = 12
a[2] = 7
b[2] = 14
a[3] = 8
b[3] = 16
string
如下示例中:
const char *str1 = "ADD";
字符串"ADD"
是RO只读的,不可以被修改,使用const修饰;字符串是一个char类型的数组,可以使用char*
表示。
strcmp(a,str1)
string.h中用于比较两个字符串大小的函数,返回值为0表明相等
int operate_strings(const char *a, const char **c, int d, int e, int* f)
这里比较tricky的是,对于SV端的output string
,不论是string还是string数组,C端都是char**
类型,传递数组的指针,而不是数组。这样可以确保SV端获取内存处的数据。若使用const char *c
SV端的打印为空,可参考VCS自动生成的vc_hdrs.h
文件,会生成和SV端相匹配的C端函数原型:extern void operate_strings(/* INPUT */const char* a, /* OUTPUT */SV_STRING *c, /* INPUT */int d, /* INPUT */int e, /* OUTPUT */int *f);
CPP:
#include <stdio.h>
#include <vpi_user.h>
#include "svdpi.h"
#include "string.h"
/* nbcode "operatestrings" start */
int operate_strings(const char *a, const char **c, int d, int e, int* f)
{
const char *str1 = "ADD";
const char *str2 = "SUBTRACT";
vpi_printf("Operation to be performed = %s\n",a);
if(strcmp(a,str1) == 0){
*f = d + e;
*c = "ADD operation performed";
vpi_printf("%d + %d = %d\n",d,e,*f);
}
else if(strcmp(a,str2) == 0){
*f = d - e;
*c = "SUBTRACT operation performed";
vpi_printf("%d - %d = %d\n",d,e,*f);
}
else{
*c = "No operation performed";
}
return svIsDisabledState(); /* Since this task is not in disabled state, so return value is 0 */
}
/* nbcode "operatestrings" end */
SV:
module top();
// nbcode "importstring" start
import "DPI-C" task operate_strings(input string a, output string c,input int d, input int e, output int f);
// nbcode "importstring" end
string a,b,c;
int d,e,f;
initial
begin
a = "ADD";
b = "SUBTRACT";
d = 400;
e = 200;
operate_strings(a,c,d,e,f);
$display("%s\n",c);
operate_strings(b,c,d,e,f);
$display("%s\n",c);
end
endmodule
LOG:
vcs -full64 \
-kdb -lca \
-sverilog \
-l vcs.log \
./add.c \
./top.v \
-R
Operation to be performed = ADD
400 + 200 = 600
ADD operation performed
Operation to be performed = SUBTRACT
400 - 200 = 200
SUBTRACT operation performed
structure
structure类型也是使用指针的方式,参考实例:https://github.com/holdenQWER/cvt_dpi
vcs会根据SV侧的structure自动推测生成C侧的structure:
SV:
typedef struct{
int H_resol;
int W_resol;
shortreal refresh_rate;
int RB_v11;
int RB_v11_force;
int RB_v12;
int film_optimized;
int interlaced;
} argv_struct;
C:
in vc_hdrs.h
struct _vcs_dpi_argv_struct {
int H_resol;
int W_resol;
float refresh_rate;
int RB_v11;
int RB_v11_force;
int RB_v12;
int film_optimized;
int interlaced;
};
Topics
only C code
DPI只支持C代码,C++代码需要C wrapper封装;g++编译时,需要extern "C"
封装;
分配内存
C代码需要手动分配内存,否则仿真遇到Segmentation fault
时,难以debug。
常见的内存操作如下:
typedef struct { unsigned char cnt; } c7; c7 * c = (c7*) malloc(sizeof(c7));
: 为c7
类型的句柄c
调用malloc
分配堆空间;
free(c);
: 释放堆空间
char src[] = "sample test"; char dest[50]; memcpy(dest,src,strlen(src)+1);
: 复制src
到dest
,字节数为strlen(src)+1
char src[] = "Hello World!"; char dest[50]; strcpy(dest,src)
: 将src
复制到dest
;只用于字符串的复制;
CPP中:
std::vector<unsigned int> dat; dat = std::vector<unsigned int>(dat_h,(dat_h+dat_size));
: 为vector数组dat
复制地址dat_h
到dat_h+dat_size
的内存数据;
dat.clear()
:清空dat
数组
int *p1 = new int[10];
: 使用new创建一个10元素的数组,p1指向数组首元素;
delete [] p1;
:释放内存空间
C11:
unique_ptr<int> pInt(new int(5));
使用unique_ptr,自动回收内存。
info打印
C侧调用的printf
函数,打印信息只会显示在terminal上;调用io_pirntf
函数(include $VCS_HOME/include/veriuser.h),可以将打印信息放入仿真工具的log中。
chandle
chandle
在SV代码中代表C端的一个void*
句柄;void*
句柄可以向任意类型转换🔗void* 详解及应用;SV侧每次调用C侧函数,函数原有的栈空间都会在调用结束时释放,通过chandle
的回传,可以保证数据,实例或者特定程序在仿真过程中持续存在:
//top.sv
import "DPI-C" function void counter7(input chandle inst,output bit[6:0] out,input bit[6:0] in,input bit reset,load);
import "DPI-C" function chandle counter7_new();
module tb;
bit [6:0] o1,o2,i1,i2;
bit reset,load,clk;
chandle inst1,inst2;
initial begin
inst1 = counter7_new();
inst2 = counter7_new();
reset = 0;
load = 0;
i1 =120;
i2 = 10;
fork
forever #10 clk = ~clk;
forever @(posedge clk) begin
counter7(inst1,o1,i1,reset,load);
counter7(inst2,o2,i2,reset,load);
end
join_none
@(negedge clk) load = 1;
@(negedge clk) load = 0;
@(negedge clk) $finish;
end
endmodule
//counter7.c
#include <svdpi.h>
#include <malloc.h>
#include <veriuser.h>
typedef struct {
unsigned char cnt;
} c7;
void * counter7_new() {
c7 * c = (c7*) malloc(sizeof(c7));
c->cnt = 0;
return c;
}
void counter7(c7 *inst,
svBitVecVal *count,
const svBitVecVal *i,
const svBit reset,
const svBit load
) {
io_printf("addr:%p",inst);
if(reset) inst->cnt = 0;
else if (load) inst->cnt=*i;
else inst->cnt++;
inst->cnt &=0x7f;
*count = inst->cnt;
io_printf("C:count=%d,i=%d,reset=%d,load=%d\n",*count,*i,reset,load);
}
//makefile
all: clean
vcs -full64 \
-kdb -lca \
-sverilog \
-l vcs.log \
./counter7.c \
./top.sv \
-R
clean:
-rm -rf simv* *.log csrc ucli.key ucli.key
pure and context
DPI把C函数分成 pure函数,context函数或者 generic函数。
Pure C函数:
作为pure函数,函数的结果必须仅仅依赖于通过形参传递进来的数值。Pure函数的优点在于仿真器可以执行优化以改进仿真性能。Pure函数不能使用全局或者静态变量,不能执行文件I/O操作,不能访问操作系统环境变量,不能调用来自Verilog PLI库的函数。只有没有输出或者inout的非void函数可以被指定成pure。
例如:
import "DPI-C" pure function int calc_parity(input int a);
Context C函数:
context C函数明白函数声明所在工作域的Verilog的层次。 这使得被导入的C函数能够调用来自PLI TF,ACC或者VPI库的函数, 从而DPI函数可以充分利用PLI的优势特性, 比如写仿真器的log文件以及Verilog源代码打开的文件。
如果一个import函数调用了export函数,也需要声明为context类型。
例如:
import "DPI-C“ context function int myclassfunc_func1();
Generic C函数:
那些既没有明确声明为pure,也没有声明为context的函数称为generic函数(SystemVerilog标准没有给除了pure或context之外的函数特定的称呼)。generic C函数可以作为Verilog函数或者Verilog任务导入。任务或者函数可以由输入、输出以及inout的参数。函数可以有一个返回值,或者声明为void。generic C函数不允许调用Verilog PLI函数,不能访问除了参数以外的任何数据,只能修改这些参数。
inout
DPI不支持ref
修饰形参,可以使用inout
代替;
例如:
import "DPI-C" context task mod_t(inout real r[10]);
export scope
SV引入了编译单元(compilation unit
)这一概念,一起编译的源文件的一个组合。$unit
,module
,program
,package
都会划分编译单元;其中$unit
代表全局;
对于import
函数来说,SV侧调用C函数,和正常SV函数一样,只要在可见范围内,就可以被正常调用;
但是对于export
函数,因为是C侧调用,SV侧定义,所以C侧调用需要显示指明context
上下文,就是通过svSetScope
指明SV侧被调用函数定义的位置;
函数 | 含义 |
---|---|
svGetScope | 获取当前C侧的svScope |
svGetScopeFromName | 将string类型的作用域转化为svScope 类型 |
svGetNameFromScope | 将svScope 类型转化为string类型的作用域 |
svSetScope | 在调用export函数之前,设定export函数的context 上下文 |
示例1:
save_my_scope
在block
中调用,my_scope = svGetScope();
获得当前block
module的scope:top.b1
block
先调用c_display
,打印当前调用的scope,然后为sv_display
设置scope,使得C端调用block
中定义的sv_diaplay
函数;
top
之后调用c_display
,打印当前调用的scope,然后为sv_display
设置scope(top.b1
),使得C端调用block
中定义的sv_diaplay
函数;
svSetScope(my_scope);
用于设置调用的export函数sv_diaplay
是定义在top
中还是block
中。
对于import函数c_display()
,都是调用C侧唯一的一个函数;但是因为block
和top
是独立的两个作用域空间,所以需要分别import一次。
打印log:
C: c_display called from scope top.b1
C: calling top.b1.sv_display
SV: In top.b1.sv_display
C: c_display called from scope top
C: calling top.b1.sv_display
SV: In top.b1.sv_display
//top.sv
module block;
import "DPI-C" context function void c_display();
import "DPI-C" context function void save_my_scope();
export "DPI-C" function sv_display;
function void sv_display();
$display("SV: In %m");
endfunction
initial begin
save_my_scope();
c_display();
end
endmodule
module top;
import "DPI-C" context function void c_display();
export "DPI-C" function sv_display;
function void sv_display();
$display("SV: In %m");
endfunction
block b1();
initial #1 c_display();
endmodule
//test.c
#include <svdpi.h>
#include "veriuser.h"
extern void sv_display();
svScope my_scope;
void save_my_scope() {
my_scope = svGetScope();
}
void c_display() {
io_printf("\nC: c_display called from scope %s\n",svGetNameFromScope(svGetScope()));
svSetScope(my_scope);
io_printf("C: calling %s.sv_display\n",svGetNameFromScope(svGetScope()));
sv_display();
}
//makefile
all: clean
vcs -full64 \
-kdb -lca \
-sverilog \
-l vcs.log \
./test.c \
./top.sv \
-R
clean:
-rm -rf simv* *.log csrc ucli.key ucli.key
示例2:
如果注释掉scope_name = svGetScopeFromName("$unit");
,则会报如下错误:
Error-[DPI-DXFNF] DPI export function not found The DPI export function/task ‘store_in_fifo’ called from a user/external
C/C++/DPI-C code originated from import DPI function ‘store_output_data’ at file ‘./top.sv’(line 75) is not defined or visible.
Please check the called DPI export function/task is defined in the mentioned module, or check if the DPI declaration of the DPI import function/task which invokes that DPI export function/task is made with ‘context’. Another work-around is using svGetScopeFromName/svSetScope to explicitly set the scope to the module which contains the definition of the DPI export function/task.
因为store_in_fifo
放在SV测的$unit
作用域;store_output_data
在top
中被调用,默认处于top
作用域;需要重新设定scop为$unit
,才可以调用store_in_fifo
。
//top.sv
typedef struct {
bit enable;
int cnt;
int op;
} packet;
parameter MAX_CNT = 5;
parameter FIFO_SIZE = 10;
packet p;
int a, b, out, ret, fifo_ptr, enable;
bit[1:0] rand_data;
int result_fifo[FIFO_SIZE];
// nbcode "formalarg" start
import "DPI-C" context function int math_operation(inout packet p, input int a, input int b, output int out);
// nbcode "formalarg" end
export "DPI-C" function valid_operation;
// nbcode "disablep" start
function int valid_operation(input int cnt);
if(cnt > MAX_CNT) begin
$display("The number of valid operations has exceeded the limit.");
$display("The store_output_data function will be disabled");
enable = 0;
disable store_output_data;
end
else begin
$display("The number of valid operations has not reached its upper limit.");
$display("More operations can be performed");
end
endfunction
// nbcode "disablep" end
function int fifo_full();
if(fifo_ptr > FIFO_SIZE)
return 1;
else
return 0;
endfunction
import "DPI-C" context task store_output_data(inout packet p);
export "DPI-C" task store_in_fifo;
task store_in_fifo();
if(!fifo_full()) begin
ret = math_operation(p,a,b,out);
if(enable) begin
fifo_ptr++;
result_fifo[fifo_ptr] = out;
end
end
else
$display("FIFO is already full.");
endtask
// nbcode "compileunit" end
module top();
initial
begin
a = 400;
b = 200;
fifo_ptr = 0;
p.enable = 1'b1;
enable = 1;
for(int i = 1; i < 7; i++) begin
if(std::randomize(rand_data)) begin
p.cnt = i;
p.op = rand_data;
store_output_data(p);
end
end
end
endmodule
//calc.c
#include <stdio.h>
#include "svdpi.h"
#include "vpi_user.h"
/* nbcode "structlayout" start */
typedef struct {
char enable;
int cnt;
int op;
} data;
/* nbcode "structlayout" end */
extern int valid_operation (int _a1);
extern int store_in_fifo();
int ret;
/* nbcode "disableack" start */
int store_output_data(data *p)
{
svScope scope_name;
scope_name = svGetScope();
vpi_printf("KKK1 scope_name:%s\n",svGetNameFromScope(scope_name));
scope_name = svGetScopeFromName("$unit");
svSetScope(scope_name);
vpi_printf("KKK2 scope_name:%s\n",svGetNameFromScope(scope_name));
if(p->enable == 1){
store_in_fifo();
}
/* The following API is used to check the whether the import task
has been disabled by export task or not*/
if(svIsDisabledState() == 1) {
vpi_printf("The imported task store_output_data has been disabled\n");
}
return (svIsDisabledState());
}
/* nbcode "disableack" end */
/* nbcode "formalpointer" start */
/* The import task returns an int */
int math_operation(data *p, int a, int b, int *c)
{
/* nbcode "formalpointer" end */
/*The export task returns an int */
ret = valid_operation(p->cnt);
/* The following API is used to check the whether the import task
has been disabled by export task or not*/
if(svIsDisabledState() == 1) {
vpi_printf("The imported function math_operation has been disabled\n");
svAckDisabledState();
}
else {
switch(p->op) {
case 0 : /* ADD */
*c = a + b;
printf(" Sum of %d and %d is %d\n", a, b, *c);
break;
case 1 : /* SUB */
*c = a - b;
printf(" Difference of %d and %d is %d\n", a, b, *c);
break;
case 2 : /* MUL */
*c = a * b;
printf(" Multliplication of %d and %d is %d\n", a, b, *c);
break;
case 3 : /* DIV */
if(b != 0)
*c = a / b;
printf(" Division of %d and %d is %d\n", a, b, *c);
break;
default: break;
}
}
return *c;
}
//makefile
all: clean
vcs -full64 \
-kdb -lca \
-sverilog \
-l vcs.log \
./calc.c \
./top.sv \
-R
clean:
-rm -rf simv* *.log csrc ucli.key ucli.key