文章目录
前言
SV使用DPI直接编程接口,能更简单地连接C、C++。一旦你声明或者使用 import 语句导入了一个C子程序,可以像调用SV子程序一样调用它,C代码也可以调用SV的子程序。
前半部分以数据为中心,后半部分以控制为中心。
1 传递简单的数值
1.1 传递整数和实数类型
SV调用C语言子程序factorial
import "DPI-C" function int factorial(input int i);
program automatic test;
initial begin
for (int i = 1; i<=10; i++)
$display("%0d != %0d", i, factorial(i));
end
endprogram
C语言factorial函数
int factorial (int i) {
if(i<=1) return 1;
else return i* factorial(i-1);
}
1.2 导入声明 import
import 声明定义了C任务和函数的原型,但是用的是SV的数据类型,带有返回值的C函数会被映射成一个SV函数。 void类型的C函数则被映射成一个SV任务或者void函数。
如果C函数名和SV的命名冲突,可以在导入时赋予新的函数名。
C函数expect映射到SV的fexpect,expect是全局有效,fexpect是SV中局部有效。
子程序不可以被重载,例如,不能在expect函数中导入一次实型参数,然后再导入一次整型参数。
改变导入函数的名字
program automatic test;
// 改变C函数名"test" 为 "my_test"
import "DPI-C" test=function void my_test();
initial begin
// C函数与关键词同名,需要修改函数名
import "DPI-C" \expect=function int fexpect();
...
if(actual != fexpect()) $display("ERROR");
...
end
endprogram
1.3 参数方向
导入的C子程序,缺省的情况,参数的方向是 input(数据从SV流向C函数);支持 output和inout,不支持 ref 。
为了减少C代码中的漏洞,可以将输入参数定义为const,一旦对输入变量进行写操作,C语言编译器就会报错。
C语言factorial函数
int factorial (const int i) {
if(i<=1) return 1;
else return i* factorial(i-1);
}
1.4 参数类型
通过DPI传递的每个变量有两个相匹配的定义,一个是SV,一个是C语言。SV仿真器不能比较数据类型,因为它无法读取C代码。
SV的bit类型映射到C语言svBit,
1.5 导入 数学库 函数
直接调用C语言数学函数库中的多个函数,而不需要使用C封装wrapper,这样就减少了需要编写的代码量。
import "DPI-C" function real sin(input real r);
...
initial $display("sin(0)=%f",sin(0.0));
2 连接简单的C子程序
2.1 使用静态变量的计数器
# include<svdpi.h> // SV DPI结构和方法的定义
void counter7(svBitVecVal * o,
const svBitVecVal * i,
const svBit reset,
const svBit load){
static unsigned char count = 0; //静态的计数变量
if(reset) count = 0;
else if(load) count =*i; // 加载数值, i是双状态7bit位宽的变量
else count++; // 计数
count &=0x7f; //最高位清0
* o = count; //
}
使用静态存储的7位计数器的测试平台
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);
#10 reset = 1;
counter7(out, in, reset, load); //复位
...
end
endprogram
2.2 chandle数据类型
chandle数据类型 允许你在SV代码中存储一个C、C++指针。
# include<svdpi.h>
# include<malloc.h>
# include<veriuser.h> //io_print
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) {
if (reset) inst -> cnt=0; // 复位
else if(load) inst -> cnt=*i; // 加载数值
else inst -> cnt++; // 计数
inst -> cnt &=0x7f; // 最高位置0
* count = inst -> cnt; // 赋值给输出变量
io_print("C: count=%d, i=%d, reset=%d, load=%d\n", *count, *i, reset, load);
这个子程序再同时调试 C 和 SV 代码的时候有用
}
该计数器测试平台和使用静态变量的计数器测试平台相比, 首先,该计数器在使用前需要创建,其次计数器在时钟边沿上升调用,在时钟下降沿加载激励,以免避免信号竞争。
2.3 值得压缩 packed
字符串“DPI-C”表明你在使用压缩值的表示方式。
出于性能的考虑,SV仿真器可能 不会再调用了一个DPI函数之后屏蔽变量未使用的高位,所以SV变量的值可能会产生错误,在C语音中需要确保这些变量的正确使用。
如果需要在 位 bit 和 字 word之间进行转换,便可以使用宏 SV_PACKED_DATA_NELEMS
2.4 四状态数值
2.5 从双状态数值转换到四状态数值
SV导入声明中把双状态类型改为四状态类型,如修改 bit 和 int 类型为 logic 和integer。
任何对这些参数的引用都需要使用 .aval 前缀以便正确地访问数据。
当从四状态变量中读取数据的时候,需要检查 bval 位是否存在X或Z值。
3 调用C++程序
3.1 C++中的计数器
3.2 静态方法
DPI 只能调用 静态的C或 C++方法,即在连接时已经存在的方法。但是C++的方法,在运行时还不存在。
解决方法: 创建可以与 C++动态对象 和 方法通信 的静态方法,
静态方法和链接
extern "C" void* counter7_new()
代码extern “C” 告诉C++编译器,送入链接器的外部信息应当使用C调用风格,并不能执行名字调整。
{
return new Counter7;
}
// 调用一个计数器实例,并传递信号值
extern "C" void Counter7( void* inst,
svBitVecVal* i,
const svBitVecVal*i,
const svBit reset,
const svBit load)
{
Counter7 * c7 = (Counter7 *) inst;
c7 -> counter7_signal(count, i, reset, load);
}
从测试平台的角度来看,C++计数器看起来就像每一个实例都独立保存计数值的计数器。
3.3 和事务级(transaction level)C++ 模型通信
当需要 创建复杂设备的模型 的时候,例如 处理器 和 网络设备,使用事务级通信会使仿真的速度加快。
使用方法通信的C++计数器
C++ 事务级计数器的静态封装 wrapper
4 共享简单数组
SV 和 C 中传递 标量和向量。C模型 可能会读入一个数组,执行一些计算然后返回另一个数组作为执行结果。
4.1 一维数组—双状态 p345
4.2 一维数组—四状态
5 开放数组 open array
在SV和C之间共享数组的时候。
- 为了得到最快的仿真速度,反向工程的方式分析出数组在SV的存储方式,在C中根据数组的内存映射方式进行操作。 容易出错,有任何一个数组的大小由变化,必须重寻编写和调试C代码。
- 更稳妥的方法是使用 “开放数组 open array” 和 对应的SV子程序 操作他们。
5.1 基本的开放数组
SV 和 C 使用开放数组传递一个简单的数组,SV import 语句中使用空白的【】来表明要传递的是一个开放数组。
调用带有开放数组的C子程序的测试平台
import "DPI-C" function void fib_oa(output bit [31:0] data[]);
program automatic test;
bit [31:0] data [20];
initial begin
fib_oa(data);
foreach (data[i])
$display(i, data[i]);
end
endprogram
使用基本开放数组的C代码
void fib_oa( const svOpenArrayHandle data_oa){
int i, *data;
data = (int *) svGetArrayPtr(data_oa);
data[0] = 1;
data[1] = 1;
for (i=2; i<=20; i++)
data[i] = data[i-1] + data[i-2];
}
5.2 开放数组的方法
svdpi.h 中定义了很多可以访问开放数组的内容和范围 的DPI方法。
仅对定义位 svOpenArrayHadle 的开放数组句柄起作用,而不适用于svBitVecVal 或 svLogicVecVal类型的指针。
5.3 传递大小 未定义的开放数组
C函数使用了svLow 和 svHigh 方法来确定数组的方位。
调用参数为 多维开放数组的C函数的测试平台
import "DPI-C" function void mydisplay(inout int h[][]);
program automatic test;
int a[6:1][8:3]; 此处字范围由高到低
...
方法svLow(handle, dimension) 返回指定位数的最小索引值。如对[6:1]数组,svLow(h,1)返回1;
svHigh(h,1)返回6.
svLeft 和 svRight 方法返回数组声明中的左边索引值和右边索引值
svGetArrElemPtr2 方法的调用返回一个指向 二维数组某个元素的指针。
参数为多维开放数组的C代码
void mydisplay(const svOpenArrayHandle h){
}
5.4 DPI中压缩(packed)的开放数组
一个开放数组 被视为 拥有一个压缩的维度 和 一个或多个非压缩的维度
压缩开放数组的测试平台
import "DPI-C" function void view_pack(input bit[63:0] b64[]);
program automatic test;
bit [1:0][0:3][6:-1] bpack[9:1];
...
使用压缩开放数组的C代码
void view_pack(const svOpenArrayHandle h){
int i;
...
}
svGetArrayElemPtr1 类型强制转换成 long long int 类型
6 共享复合类型
类属性的内存映射方式,SV和C不一致,不能直接共享对象。
解决方法: 为了达到共享对象,必须在两边创建相似的结构,并且使用压缩和解压缩方法对这两种格式进行转换。
6.1 SV和C之间传递结构
C将char视为有符号变量,
共享结构的C代码
typedef struct {
unsigned char b, g, r; // x86小尾序
// unsigned char r, g, b; //SPARC格式
} * p_rgb;
共享结构的 测试平台
typedef struct packed {bit[7:0] r,g,b;} RGB_T;
import "DPI-C" function void invert(inout RGB_T pstruct);
program automatic test;
class RGB;
rand bit[7:0] r, g, b;
function void display(string preix="");
$display("%s RGB = %x, %x, %x", prefix, r, g, b);
endfunction
// 将类成员压缩到一个结构中
function RGB_T pack();
pack.r=r; pack.g=g; pack.b=b;
endfunction
// 将结构解压后赋值给类成员
function void unpack(RGB_T pstruct);
r=pstruct.r; g=pstruct.g; b=pstruct.b;
endfunction
initial begin
RGB pixel;
RGB_T pstruct;
pixel = new();
repeat(5) begin
assert(pixel.randomize()); 创建随机像素
pixel.display("\nSV:before"); 打印像素值
pstruct = pixel.pack(); 转换为结构
invert(pstruct); 调用C函数将位取反
pixel.unpack(pstruct); 把结构解压后赋值给类
pixel.display("SV: after");
end
end
endprogram
6.2 SV和C之间传递字符串
为结构的符合值传递一个字符串
7 纯导入方法和关联导入方法
导入方法 分为 纯导入方法、关联导入 和 通用导入。
纯函数 仅依赖于他们的输入,更外部环境没有交互,不访问全局或静态变量,不会进行文件操作,不跟函数体以外的事务如操作系统、进程、共享内存和套接字等有交互。
调用关联导入方法时,需要记录调用的上下文环境,给仿真器带来额外的开支。—不建议
8 在C中与SV通信
在C中调用SV的方法
8.1 一个简单的导出方法
导出一个SV函数
module block;
import "DPI-C" context function void c_display();
export "DPI-C" function sv_display; 禁止带任何返回值声明或者参数,也不能加函数声明使用的空阔号。
function void sv_display();
endfunction
endmodule
在C中调用一个SV导出函数
extern void sv_display();
void c_display() {
io_printf("c:c_display");
sv_display();
}
8.2 调用SV函数的C函数
函数不耗时
SV模块
module memory;
import "DPI-C" function read_file(string fname);
export "DPI-C" function mem_build; 没有类型定义或者参数
..
initial read_file("mem.dat");
function mem_build(input int size);
endfunction
endmodule
C代码
extern void mem_build(int);
int read_file(char * fname){
...
}
8.3 调用SV任务的C任务
真实的内存模型 有读写之类消耗时间的操作,需使用任务建模。
SV模块
module memory;
import "DPI-C" function read_file(string fname);
export "DPI-C" task mem_read;
export "DPI-C" task mem_write;
export "DPI-C" function mem_build; 没有类型定义或者参数
..
initial read_file("mem.dat");
function mem_build(input int size);
endfunction
task mem_read(input int addr, output int data);
#20 data = mem[addr];
endtask
task mem_write(input int addr, output int data);
#10 mem[addr] = data;
endtask
endmodule
C代码
extern void mem_build(int);
extern void mem_write(int,int);
extern void mem_read(int,int*)
int read_file(char * fname){
int cmd;
FILE*file;
file = fopen(fname, "r");
while(!feof(file)) {
cmd = fgetc(file);
switch(cmd){
case 'M':{
int hi;
fscanf(file, "% d %d", &hi);
mem_build(hi);
break;
}
case 'R':{
int addr, data, exp;
fscanf(file, "%c %d %d", &cmd, &addr, &data);
mem_read(addr, &exp);
if (data!=exp)
io_printf("C: Data=%d, exp=%d\n",data,exp);
break;
}
case 'W':{
int addr, data;
fscanf(file, "%c %d %d", &cmd, &addr, &data);
mem_write(addr, data);
break;
}
}
}
fclose(file);
}
8.4 调用对象中的方法 P357
你可以导出SV方法,但是定义在类中的方法除外。类似限制SV导入静态C函数。
解决方法: 在SV和C代码之间传递一个对象引用。跟C指针不同的是,SV句柄不能通过DPI传递。定义一个句柄数组,然后再两种语言之间传递数组的索引。
8.5 上下文context的含义
导入函数的上下文是该函数定义所在的位置,比如$unit、模块、program或者package作用域scope,
把一个函数导入两个不同的作用域,对应的C代码会一句import语句所在位置的上下文执行。
module top;
import "DPI-C" context function void c_display();
export "DPI-C" function sv_display;
block b1();
initial c_display();
function void sv_display();
$display("SV:top");
endfunction
endmodule
module block;
import "DPI-C" context function void c_display();
export "DPI-C" function sv_display; 禁止带任何返回值声明或者参数,也不能加函数声明使用的空阔号。
initial c_display();
function void sv_display();
$display("SV:block");
endfunction
endmodule
8.6 设置 导入函数的作用域
如同SV代码可以再局部作用域调用方法,导入的C方法也可以再它默认的上下文之外调用方法。
使用 svGetScope 方法可以获得当前作用域的句柄,然后就可以再对 svGetScope的调用中使用该句柄,是的C代码认为它处在另一个上下文中。
获取和设置上下文的C代码
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()));
// 设置新的作用域
svGetScope(my_scope);
io_printf("C: calling %s.sv_display\n", svGetNameFromScope(svGetScope()));
sv_display();
}
调用获取和设置上下文方法的模块
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;
initial begin
save_my_scope();
c_display();
end
function void sv_display();
$display("SV:%m");
endfunction
endmodule
module top;
import "DPI-C" context function void c_display();
export "DPI-C" function sv_display;
block b1();
initial #1 c_display();
function void sv_display();
$display("SV:top");
endfunction
endmodule
9 与其它语言交互 P364
写了一个Perl脚本的C封装,SV部分代码与调用C语言的代码相同。
总结
使用DPI-C代码可以调用 SV子程序,同时允许外部应用程序控制仿真过程。
难点在于将SV的数据类型映射到C,共享结构和类的时候。