基本讲解
什么是DPI?
SystemVerilog引入了DPI(Direct Programming Interface),能够更简洁的连接C/C++或者其他非Verilog的编程语言。只要使用import声明和使用,导入一个C子程序,就可以像调用SystemVerilog中的子程序一样来调用它。
为什么DPI?
Verilog与C之间进行程序交互,PLI(Programming Language Interface),经过了TF,ACC,VPI等模式演变。通过PLI方式连接一个简单的C程序,需要编写很多代码,并理解多仿真阶段的同步,调用段,实例指针等概念。PLI方式给仿真带来了额外的负担。
那么,为什么SV/UVM需要与其它语言产生交互呢?这就涉及到了一些应用场景,我们可以说几个看看。
(1)我们知道,在模块级乃至子系统级的验证,使用SV就完全够用了,而在更上面的层级,例如系统级,多采用大量的C代码组成。那么为了完成测试用例从子系统到系统级的复用,我们最好在子系统级开始就有意识地建立支持C测试的环境,并且使用一些基于C的测试用例,这样才能使得整体具有更好的复用性。
(2)处理器的需要。子系统测试时,不一定本身自带了处理器的硬件实例(Verilog编写的),因此在构建子系统时还要考虑如何模拟外部处理器对子系统的访问行为。当然,我们直接加上一个外部的处理器实例是完全可以的,等验证完子系统的功能后再撤掉就行。注意,这个处理器实例也是verilog编写的。但是,这种方法也存在了一些弊端:即使这个外部处理器再小巧,但硬件体积还是过大了,肯定会影响整个仿真的速度;针对不同子系统的预留接口(APB/AHB/AXI等),处理器子系统也要进行对应的调整吧;这种真实的处理器,必然要处理C代码,所以要进行编译、转换成二进制bin文件以及下载到memory中,还是太麻烦;还需要额外的启动配置文件,而且仿真时需要额外的额时间进行初始化。总结一下真实处理器的弊端,那就是麻烦,处理C代码也繁琐,影响了仿真速度。
如何实现DPI
我们要明白两个问题:SV和C之间的数据类型不是完全一致的,所以肯定需要转换;SV和C之间的控制信号又该如何传递呢?
通过import声明定义C任务和函数的原型,带有返回值的C函数被映射成一个systemverilog的函数(function);void类型的C函数被映射为一个systemverilog的任务(task)或者void 函数(function)。
如果C函数名和SystemVerilog中的命名冲突,可以在import导入时,赋予新的函数名。
import "DPI-C" function void test(); //将C中的test函数,import进来,变为SV中的void function
import "DPI-C" test=function void my_test(); //将C中的test函数,修改名字为my_test。
经“DPI-C”引入的C函数,可直接在function中调用,但是只在该DPI被声明的空间内有效。
参数方向
对于参数方向,支持input, output以及inout。Ref则不被支持。当然默认是input类型。
例子:Import “DPI-C” function int addmul(input int a, b, output int sum);
数据映射关系
通过DPI传递的每个变量都有两个相匹配的定义,一个在SystemVerilog中,一个在C语言中。 在使用中必须,确认使用的是兼容的数据类型。C输出数据给SV,只能通过指针的方式,输出。所以输出数据也是在SV中建立空间,然后在C中得到指针,将值写进去,这样C的内存空间的控制不会影响到SV端。
以上这些定义,都可以在svdpi.h中找到相应的操作函数。C中本身是不具有这些交互类型的,需要在使用前添加头文件,也就是:该头文件必须被包含到C函数实现端。
关于DPI调入的C的函数返回值:
SV LRM推荐使用small values :void,byte,shortint,int,longint,real,shortreal,chandle,string,bit,logic
不推荐使用bit[6:0]或者logic[6:0]这样的值,因为这样需要返回一个svBitVecVal或者svLogicVecVal的指针。
四值逻辑变量的转换关系
上面的看起来都比较常规,接下来这个就有意思了。我们知道,C语言是软件环境,所以只会有0和1,那么它怎么才能表示诸如logic等四值变量呢??
这个很巧妙,假设SV一侧有一个logic f,那么它在C一侧,会用一个无符号的字节来保存。分为aval和bval,其中aval保存在最低位,bval保存在紧邻的高位。
下面给出转换表:
四状态值 | bval | aval |
0 | 0 | 0 |
1 | 0 | 1 |
Z | 1 | 0 |
X | 1 | 1 |
两位表示一位,所以1’b0在C中就是0x0,1’b1在C中是0x1,1’bz在C中是0x2,1’bx在C中是0x3。
那么logic[31:0] lword,就更有意思了,我们采用一对32bit的变量来表示,第一个aval[31:0]包含低32位的数值,bval[31:0]包含了紧邻高位的数值,表示如下:
所以在导入数组时,需要同时赋值aval和bval,例如data[31:0],例如要将data[0]=1,那么需要data[0].aval=1, data[0].bval=0;
导入方式的分类
import “DPI-C” context task core0_thread();
这个关键词表示的是关联导入,其实分为三种导入方法,pure、context以及generic。
如果一个函数严格根据输入来计算输出,跟外部环境没有其它交互,那么就是pure的;如果使用了全局变量,那它就不会是pure方法了,但如果它也没有调用任何的PLI,所以其实也不必声明为context(因为关联类型会导致额外的开销),所以可以声明为generic(缺省下为此类型)。
从SV中导出
export “DPI-C” function sv_display;
注意后面不需要加括号和参数,如果需要改变名字,则可以
export “DPI-C” dpi_writew = task writew;
而在C一侧,则只需要extern void sv_display();就可以了
当然有参数还是要加的
extern void dpi_writew(unsigned int addr, unsigned int data);
extern void dpi_readw(unsigned int addr, unsigned int *data);
连接C语言的例子
C语言一侧:
reset和load是一个双状态的比特信号,以svBit类型进行传递。输入i是双状态7bit 变量,用svBitVecVal类型传递。
SV语言一侧:
import “DPI-C” function void counter7(output bit[6:0] out, input bit[6:0] in, input bit reset, load);
下面一些具体方法就不写了,我们注意到,bit[N:0]与bit还是不一样的,还有一点可以注意一下,即使是bit[0:0]与bit,虽然在SV一侧是相同的(假定都是输入),但是到了C一侧,它们的类型分别是const svBitVecVal*与svBit。
SV中也能直接应用C的一些数学函数,这一点还是很方便的。
import “DPI-C” function read sin(input real r);
测试平台:
import “DPI-C” function void counter7(
output bit [6:0] out,
input bit [6:0] in,
input bit reset, load
);
program automatic counter;
bit[6:0] out, in;
bit reset, load;
initial begin
$monitor("SV: out=% 3d, in =%3d, reset = %0d, load = %0d\n", out,in,reset,load);
reset = 0;
load = 0;
in = 126;
out = 42;
counter7(out, in, reset, load);
end
endprogram
如果reset/load使用svLogic的类型,C程序中需要检查X、Z的状态
if(reset & 0x02) //检查变量bval中的X/Z
printf("reset val include X/Z value");
如果counter使用svLogicVal类型,C程序中检查X、Z的状态
counter.aval = inst->cnt;
counter.bval = 0; //aval与bval实际存在一个logicval的结构体中。
chandle类型允许在System Verilog中存储一个C/C++的指针,指向一段地址,来保存一些常量。
typedef struct{unsigned char cnt;} c7;
void *counter7_new() { c7* c=(c7*) malloc (sizeof(c7));
c-> cnt = 0;
return c;}
void counter7(c7* inst, ...)
测试平台:
import “DPI-C” function chandle counter7_new();
import "DPI-C" function void counter7(input chandle inst, ...);
program automatic test;
initial begin
chandle inst1;
inst1 = counter7_new();
counter7(inst1,...);
end
endprogram
C与SV之间传递数组,可以是openarray,也可以是定宽数组。
定宽数组:
void fib(svBitVecVal data[20]) {
}
import "DPI-C" function void fib(output bit[31:0] data[20])
program automatic test;
bit[31:0] data[20];
initial begin
fib(data);
end
endprogram
openarray型指针,需要在C端通过svGetArrayPtr来得到来自SV的动态数据的地址。
其他类型的指针,可以直接在C中通过*ptr来赋值或调用。
void fib_oa(const svOpenArrayHandle data_oa) {
}
import "DPI-C" function void fib_ca(output bit[31:0]data[])
program automatic test;
bit[31:0] data[20],r;
fib_ca(data);
endprogram
openarray定义的查询方法:
int svSizeOfArray(h): 以字节计量的数组大小
int svSize(h,d): 维数d的元素总个数
int svLeft(h,d): 维数d的左边界, svLeft(h,1)一维数组的左边界,svleft(h,2)二维数组的左边界
int svRight(h,d): 维数d的右边界,=
openarray定义的定位函数:
void * svGetArrayPtr(h): 整个数组的存储位置
void *svGetArrElemPtr(h,i1,...): 数组中的一个元素
void *svGetArrElemPtr1(h,i1): 一维数组中的一个元素
void *svGetArrElemPtr2(h,i1,i2): 二维数组中的一个元素