Title:如何用DPI调用C++程序,并成功仿真
-
前言
之前试了用DPI调用C程序,很方便,两行解决:
- 一行在Verilog/SV中加
import "DPI-C" function int 函数名
; - 一行在VCS compile中补上此C文件名;
上周五因需要,计划用DPI调用C++程序,结果!好多好多bug!找了整整一天!折磨!
为什么会这么久、这么痛苦嘞?
- 网上DPI的信息少,google上都不多;
- 搜来搜去,title起的是《DPI调用C/C++程序》,结果通篇只能调C程序,右半边的C++压根不能用好吧,编译过没哈?就忽悠人…
- 好不容易找到一些信息,都解决不了问题,太general或者 无效啊无效!
刷了一天google、睡眼惺忪;
看到几个帖子兴冲冲;
百般实验一场空. - 在SV调用C、C调用C++时,涉及到的输入形参、输出结果的传递,要如何进行,这个地方会 涉及到细节的指针的使用,信息不好搜集,自个儿试错debug很痛苦…
好在最后捣鼓出来了。俺很高兴,记录一下!(默认是 Linux下)
主要是以下几点内容:
-
g++
封装成动态库; -
SV中使用动态库;
-
SV和C的数据类型转换(实现输入输出数据传递);
太曲折了!T_T
- 一行在Verilog/SV中加
1 基本概念
1.1 编译、链接、执行
A 基本flow与文件后缀
源代码文件 -> 编译 -> 链接 -> 执行.
-
Windows下:
- C文件分别 编译,分别得到
.obj
文件; - 多个
.obj
文件 静态链接 后得到.lib
文件,多个.obj
文件 动态链接 后得到.dll
文件; - 各库文件(静态库或动态库),再次链接 后可得到可执行文件
.exe
- C文件分别 编译,分别得到
-
Linux下:
-
C文件分别 编译,分别得到
.o
中间文件(是 可以单独执行 的); -
多个
.o
文件 静态链接 后得到.a
静态库文件,多个.o
文件 动态链接 后得到.so
共享的动态库文件;共享库必须放在特定系统目录下,不然需手动指定.
-
各库文件,再次链接 后可得到可执行文件(
无后缀
);
-
多语言混合编程时:可以各自分别编译成.o文件,再链接成可执行文件.
B g++ 下编译、链接语法
以Linux为例.
-
g++进行 编译 语法
g++ -c file1.cpp file2.cpp ... filen.cpp
把文件们 分别 编译成库文件.o
-
g++进行 静态链接 的语法
用linux
ar
指令,没用到故没看,可见:linux ar命令 —— CSDN xuhongning -
g++ 生成可执行文件
g++ file.cpp -o target
把所有文件 链接成一个target 可执行文件
顺序可以自己打乱,但是-o
后面接的一定是target 文件. -
g++进行 动态链接 的语法
生成动态链接库:
g++ filename.cpp -fPIC -shared -o target.so
把文件 链接 生成 动态链接库target.so
动态链接库 可以嵌套:
g++ -o target -L./lib -lcpp
使用动态链接库得到 可执行文件target
.-
其中option解释:
-
-fPIC
:生成动态链接库; -
-shared
:编译为位置独立的代码,否则动态链接库动态载入时是以代码拷贝方式满足多进程,不是真正的代码共享. -
-L
后直接紧跟(无空格!坑死我了!)动态lib库的目录path;若不写,系统默认会去:
/lib
、/usr/lib
、/usr/local/lib
三个系统path下查找依赖的动态库;否则必须自己用-Lxxx
指定(即使是当前路径下,也得用-L
指定). -
-l
后直接紧跟(无空格!)动态lib的名字(不是文件名!)e.g. 动态lib的文件名为
libmath.so
,则此动态链接库的lib名是math
!
注意: 利用动态库,新生成 可执行文件 或 新的动态库 后(为方便,称新生成的东西为target),使用时,可能会因 “查询不到动态库” 而失败.
(报错:
cannot open shared object file: No such file or directory
)——这是因为:-
虽然 g++ 支持用相对路径或
-L
来指定 待生成动态库、现有动态库的path,但在shell中、VCS中 执行可执行文件或引用现有动态库时,不支持沿用之前g++
链接时的相对路径或-L
访问嵌套动态库的 path!(是不是很离谱)故,最后执行“可执行文件”或“用VCS调用动态库”时,需要把所有用到的动态库都放在 当前路径下.
可用linux指令
ldd 文件
来检查 此文件所需的动态lib 的路径是否可found! -
所有用到的动态库,都得在
target
的目录下 或系统lib
目录下(上面提到的三个lib
目录). -
要单独执行
target
的话,须cd
到target 目录下 执行,不可用相对路径执行:
i.e.:./target
✔️ ;./myfile/target
❌
-
-
部分Reference
-
1.2 Linux下C与C++的文件后缀
-
.c
、.cc
、.cpp
后缀的区别主要是给compiler识别用的。
.c
是C文件;C++是
.cc
和.cpp
:unix系统用.cc
;非unix系统用.cpp
;其实都是C++文件,实际上可以混用.
1.3 DPI 是什么
-
目的
verilog中有一些内建的系统调用,如:
$display(...)
、sformatf(...)
;那若我想在verilog中调用自定义的C程序咋办?可以用 PLI接口,也可以用 VPI 接口,也可以用 DPI接口.
-
PLI、VPI、DPI
-
PLI (verilog Programming Language Interface) ,是Verilog HDL的simulator environment的一个API协议,可以在verilog中调用C程序. 本来捏是 PLI1.0,但是用起来挺麻烦的,于是优化了一下,进化成了 PLI 2.0 ——VPI.
-
VPI (Verilog Procedial Interface) ,也是用于 Verilog HDL调用C程序的接口协议,是PLI的新版本,已收录于IEEE 1364,比PLI 1.0调用C程序的方法更简单点,但还是挺麻烦的,于是在VPI上面封装一层,第三个接口出现 ——DPI.
-
DPI (Direct Programming Interface),比VIP调用C语言更简单,但是少了一些功能性.
PLI 和 DPI 的内容此处略,后续另文写。它们仨给我最大的感觉就是:
PLI在verilog中调用最简单的 “helloworld” 的C程序,需要5步:
- 写C routine,其中 调用PLI;
- 把C的function associate 到system task上;
- 登记此system task(使env认识它);
- 把C程序compile、link一下;
- 在HDL中 调用system task来执行 C程序。
VPI调用 “helloworld” 的C程序,需要4步,少了上面的第二步.
DPI调用 “helloworld” 的C程序,只需要3步!
- 正常写C程序,然后把C程序compile、link一下;
- RTL中加:
import "DPI-C" function void helloword();
- 在RTL中调用即可.
C程序本来就要编译;RTL中本来就要call程序;四舍五入一下,DPI中call简单的C程序,只需要1步——加个
import ...
就好了,是不是很方便~ ( ̄▽ ̄)" -
2 如何在DPI中调用C++
-
基本flow
DPI是不能直接调用C++的,只能调用C程序。
故实现思路 是:将C++程序用C进行封装、编译成动态库,再用DPI进行调用.
以下【2.1】、【2.2】是循序渐进的.
2.1 C中如何调用C++程序
-
C中调用C++函数的写法:
-
写个C++文件,把C++的函数用
extern "C"{...}
括起来进行定义意思是:这段C++代码在编译时,要按C的规则来进行编译, 这样后续C程序才能调用这段C++的代码。
-
为什么C++程序在C中用,需要加
extern
?更多细节,可见它:extern “C“ 用法详细说明 —— CSDN 小学徒 其中的[3.1]可以解惑为什么C++需要加
extern
.
-
-
C中需要用
extern
声明外部的C++函数! ⭐️目的是:①声明此函数是来自外部库的;②声明函数的返回值类型!
因为实际使用中发现,不加在C中用
extern
声明C++的函数,只要在编译时链接了C++的库,其实也能编译通过而不会报错,但这些外部C++函数就会默认为32位返回值类型;因此,若C++函数事实上是64位返回值类型等,C中未用extern
声明,C中调用外部函数时就获取外部函数返回值的高位数据了(高位舍弃了)。【血与泪的教训…】
-
C中不需要也不能 include C++的头文件,因为C语法中无法识别C++的语法,include后反而会报错;应当:
- 用
g++
把C++文件compile成.so
(动态链接库); - 再 用
gcc
把C文件以及C++的.so
一起compile成新的动态库.so
或 可执行文件,根据需要即可(我们要用于后续DPI,因此是compile成 新的动态库).
- 用
-
-
C中调用C++函数,输入、输出数据的传递方式:
方法一:以函数形参的形式,给C++传递变量;以返回值的形式,获得计算后的结果,这个思路很简单。
这里要提的是 方法二:
“用指针传入、传出数据” 的方法中,要注意的地方——指针的使用;算C/C++的基础编程概念,但可能会没注意导致bug的产生:-
OOP编程时,指针不会忘记new;但 基本数据类型的指针,常常会忘记new,而直接往里塞数据,造成错误…
e.g.
#include<iostream> using namespace std; void getdata(int *addr){ *addr = 10; } void main(){ int *p; // 基本数据类型 指针也得new getdata(p); // p无法获得10,因为p还没有new或malloc(). p = new(int) // 或 p=(int*)malloc(sizeof(int)); getdata(p); // p可以获得10 }
-
2.2 用VCS通过DPI调用C++程序
-
基本概念
SV是systemverilog;
我用的EDA是VCS;
我VCS用的是 two-step flow(即compile+simulation). -
前提精要
-
DPI 只能call C程序;
否则 VCS compile不会报错,但仿真会报错找不到RTL内 import的DPI函数…
-
VCS直接编译C文件来实现DPI时(i.e. 用
vcs c文件
来直接编译C/C++文件),因为VCS会根据后缀调用gcc
或g++
来编译(C文件就调用gcc
,C++文件就调用g++
),故我们最后用C封装后的程序文件后缀只能为.c
,不能写为.cc
或.cpp
;否则 VCS compile不会报错,但仿真会报错找不到RTL内 import的DPI函数…
这俩情况,导致我找bug找得半死…
P.S. 后续我们并不会用VCS来编译C文件,这里只是顺嘴提一下。
-
最后对C文件 进行动态链接实现DPI调用的 C动态库时,生成
.so
必须用gcc
而不是g++
-
-
具体步骤:
-
把C++ code 内的函数 用
extern
修饰; -
把C++ code用
g++
编译链接成动态库A.so
; -
在C程序中把C++函数用 extern 修饰,即可直接调用C++程序中的函数,然后把 C程序、C++的动态库 一起 用
gcc
封装成 动态库B.so
;注意:不需要也不能 在C code中include C++文件,我们是用C++的动态库进行编译、链接的.
-
在SV或Verilog中加语句:
import "DPI-C" function int 函数名();
返回值可以自己调;
C函数内若是 void返回值类型,就使用SV的task类型而不是funciton. -
VCS的compile语句正常写,不需要用VCS来编译C coode,但要使用:
vcs -full64
不然后续仿真会报错:
shared library access error: ELFCLASS64
,这是因为 C程序默认是64位的,VCS不加-full64
却变成32位的了… -
VCS的simulation语句,要调用C程序的动态库
B.so
:仿真语句用simv -sv_lib B
,即可完成DPI对C++的调用!注意:simv中
-sv_lib
后 不加动态库文件名的后缀…
-
-
部分Reference
vcs中systemverilog和c/c++联合仿真 —— CSDN kevindas;
它讲了 如何“g++
生成动态链接库”,并如何用VCS实现 “在SV中用DPI调用动态库.so
” 的写法.
3 具体Demo例子【VCS用DPI调用C++】
-
C++内容 ——期望调用的C++程序
// CPP.cpp #include<iostream> using namespace std; typedef unsigned long long int u64; // 可以在 {} 内包含多个C++函数,或者只在.h中声明extern "C" 即可 extern "C"{ u64 helloworld_cpp(){ cout<<"Hello world!\n"<<endl; u64 data = 0x1234567891234567; return data; } }
-
C code内容 —— DPI真实调用的内容
// C.c #include<stdio.h> typedef unsigned long long int u64; // 若不用extern声明helloworld_cpp(),编译不报错 // 但helloworld_cpp() 默认是int返回值,高位数据会丢失!!! extern u64 helloworld_cpp(); void helloworld(){ u64 data; data = helloworld_cpp(); }
-
具体的Makefile Demo(可用!)
// makefile TOP = top_dut.v top_tb.v OPT = -sverilog # if need using SV TIMESCALE = "1ns/1ns" .PHONY: all Clib vcs simv clean all: clean Clib vcs simv Clib: g++ CPP.cpp -m64 -fPIC -shared -o libCPP.so gcc C.c -m64 -fPic -shared -o libC.so -L./ -lCPP #居然不能用libc.so为文件名,母鸡why...那就用libC.so吧 vcs: #务必用 -full64 ! vcs -full64 -debug_access+all -timescale=${TIMESCALE} ${OPT} ${TOP} -q simv: simv -lca -l simv.log -sv_lib libC clean: rm -rf csrc/ rm -rf simv.daidir/ rm -rf ucli.key vc_hdrs.h simv.* rm -rf libcpp.so libc.so
4 用DPI在SV、C/C++间进行数据交互
4.1 基础概念了解
-
大端模式和小端模式——多字节数据内不同字节之间的存放优先顺序,字节内是按“大端”的。
无争议的点:数据都是从低地址往高地址开始放的,只有堆栈式倒着生长的.
大端模式,先存数据的高位部分:即高位在低memory地址,低位在高memory地址;
小端模式,先存数据的低位部分:即低位在低memory地址,高位在高memory地址;
x86、arm常用小端模式,故我们得默认按小端去算。
-
C/C++的数据存储格式
是用 小端模式 去放数据,举个例子吧。
例如:64位数据,占据8个字节;则数据的高位字节的data,再放低位字节的data.
如下方的C程序例子:64位数据 d a t a = 0 x 1234 _ 5678 _ 9 a b c _ d e f 0 data=0x1234\_5678\_9abc\_def0 data=0x1234_5678_9abc_def0
其现实memory中分配的存储空间是 [ 7012080 , 7012087 ] [7012080, 7012087] [7012080,7012087]的8个字节;但低32位数据(0x9abcdef0),先存,放在起始地址 7012080 7012080 7012080中;高32位数据(0x12345678),后存,放在起始地址 7012084 7012084 7012084中.
注:要用u32的指针去输出u64内部数据各部分;因为指针+1 增加的地址是此指针对应数据类型宽度. i.e. u32指针+1,地址会增加4字节;u64指针+1,地址就增加了8字节.
4.2 SV与C/C++数据类型的对应
见绿皮书上的表格,如下:
注意两个问题:
-
SV没有指针;
-
DPI不支持返回复杂的数据类型.
因此,返回复杂的C/C++处理后的结果(如64位数据、128位数据),不能用返回值,得用 SV的数组 ⇔ \Leftrightarrow ⇔ C的指针! Demo见下面.
-
SV的
bit
类型 是可以自定义数据位宽的,但C中对应的svBitVecVal*
类型是固定数据尾位宽的——本质是取了宏名的int*
指针类型,因此 SV与C的数据传输,就是要在这两个类型中进行“指针类型强制转换”!-
点1:
可以进入
synopsys/vcs/include/svdpi.h
文件,查看 SV数据类型映射到C上的svBitVecVal*
类型 是个啥。 -
点2:
bit是SV的二值类型,单bit值只有0、1;
reg是四值类型,单bit值可以取为 0、1、x、z;
故用bit类型进行SV与C/C++的传输足以。
-
4.3 用SV调用DPI 与C/C++交互数据 【Demo】
以下的Demo功能,SV通过形参,调用C++获得不同的64bit的数据!
- 实现思路:
- C++用返回值,与C程序交互;
- C使用指针,与SV交互.
4.3.1 C 程序给 SV传 大于32位的数据
-
用C++的话,别忘了用C进行封装;详细过程在前文,不赘述.
-
C++程序
#include<iostream> using namespace std; typedef unsigned long long int u64; // compile the C++ function with C rule extern "C"{ u64 getdata_cpp(int type){ u64 data; if(type == 0) data = 0x123456789abcdef0; else if(type == 2)data = 0x0fedcba987654321; return data; } }
-
C程序
#include<stdio.h> //每个人路径不同,自己在VCS的安装目录下找这个文件的路径 #include "synopsys/vcs/include/svdpi.h" typedef unsigned longlong u64; // declare the C++ function in C extern u64 getdata_cpp(int type); void getdata_c( const svBitVecVal *type_t, const svBitVecVal *data_t){ // get 32-bit data from SV int type = *type_t; // get 64-bit data from C++ u64 data = getdata_cpp(type); // send 64-bit data to SV u64 *p = (u64*)data_t; *p = data; // 错误的写法: // data_t = &data; //因为SV的结果指针式不会改变的,C里改了没用,SV还是收不到数据. }
-
在SV中写
import "DPI-C" task getdata_c(input bit[1:0] type_t, output bit [63:0] data_t); // 千万别漏了SV的数据类型 bit,和输出 output关键字 module tb; bit [1:0] tmp_type; bit [63:0] tmp_data; getdata(tmp_type, tmp_data); $display( $sformatf("the data from C++ is %u", tmp_data) ); endmodule
-
SV需要用动态库的方式调用C,完成DPI的使用,具体过程上文已提,故不赘述.
成功运行~ ( ̄▽ ̄)"!
若需要SV与C/C++之间传输128位、甚至更多的数据,都是ok的:
- SV端,定义的
reg
数据类型位宽可以自定义; - C/C++ 端,数据接口是
int*
指针,自行用 “小端模式” 自己算地址,然后把数据取出来就行了。