前言
之前试了用DPI调用C程序,很方便,两行解决:
import "DPI-C" function int 函数名
;上周五因需要,计划用DPI调用C++程序,结果!好多好多bug!找了整整一天!折磨!
为什么会这么久、这么痛苦嘞?
好在最后捣鼓出来了。俺很高兴,记录一下!(默认是 Linux下)
主要是以下几点内容:
g++
封装成动态库;
SV中使用动态库;
SV和C的数据类型转换(实现输入输出数据传递);
太曲折了!T_T
源代码文件 -> 编译 -> 链接 -> 执行.
Windows下:
.obj
文件;.obj
文件 静态链接 后得到 .lib
文件,多个 .obj
文件 动态链接 后得到 .dll
文件;.exe
Linux下:
C文件分别 编译,分别得到 .o
中间文件(是 可以单独执行 的);
多个 .o
文件 静态链接 后得到 .a
静态库文件,多个 .o
文件 动态链接 后得到 .so
共享的动态库文件;
共享库必须放在特定系统目录下,不然需手动指定.
各库文件,再次链接 后可得到可执行文件(无后缀
);
多语言混合编程时:可以各自分别编译成.o文件,再链接成可执行文件.
以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
gcc/g++ 链接库的编译与链接 —— CSDN surgewong
GCC 命令行详解 -L 指定库的路径 -l 指定需连接的库名 —— cnblogs
.c
、.cc
、.cpp
后缀的区别
主要是给compiler识别用的。
.c
是C文件;
C++是 .cc
和 .cpp
:unix系统用 .cc
;非unix系统用 .cpp
;其实都是C++文件,实际上可以混用.
目的
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步:
VPI调用 “helloworld” 的C程序,需要4步,少了上面的第二步.
DPI调用 “helloworld” 的C程序,只需要3步!
import "DPI-C" function void helloword();
C程序本来就要编译;RTL中本来就要call程序;四舍五入一下,DPI中call简单的C程序,只需要1步——加个 import ...
就好了,是不是很方便~ ( ̄▽ ̄)"
基本flow
DPI是不能直接调用C++的,只能调用C程序。
故实现思路 是:将C++程序用C进行封装、编译成动态库,再用DPI进行调用.
以下【2.1】、【2.2】是循序渐进的.
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
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
}
基本概念
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
” 的写法.
C++内容 ——期望调用的C++程序
// CPP.cpp
#include
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
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
大端模式和小端模式——多字节数据内不同字节之间的存放优先顺序,字节内是按“大端”的。
无争议的点:数据都是从低地址往高地址开始放的,只有堆栈式倒着生长的.
大端模式,先存数据的高位部分:即高位在低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字节.
见绿皮书上的表格,如下:
注意两个问题:
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++的传输足以。
以下的Demo功能,SV通过形参,调用C++获得不同的64bit的数据!
用C++的话,别忘了用C进行封装;详细过程在前文,不赘述.
C++程序
#include
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
//每个人路径不同,自己在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的:
reg
数据类型位宽可以自定义;int*
指针,自行用 “小端模式” 自己算地址,然后把数据取出来就行了。