Icarus Verilog cadpli 模块:在 vvp 中直接运行 Verilog-XL / NC-Verilog 编译的 PLI1 应用
Icarus Verilog cadpli 模块:在 vvp 中直接运行 Verilog-XL / NC-Verilog 编译的 PLI1 应用
本文基于 cadpli 开发者指南,系统讲解 Icarus Verilog 中 cadpli 兼容模块的用途、调用方式与底层实现。cadpli 是一个动态加载接口模块,让 vvp 运行时能够加载、引导并注册那些原本面向 Cadence Verilog-XL / NC-Verilog 动态加载机制编译的 PLI1(Programming Language Interface version 1,即 ACC/TF 例程)应用。读完本文,你将掌握 -mcadpli 与 -cadpli=<module>:<boot_func> 的完整用法、共享库引导函数的契约格式,以及该模块在源码层面的加载与注册流程,从而在 Icarus Verilog 中直接复用现成的第三方 Cadence PLI1 库。
为什么需要 cadpli:PLI1 生态的兼容诉求
PLI1(IEEE 1364 中已弃用的 ACC 与 TF 例程)曾是 Verilog-XL / NC-Verilog 时代仿真器扩展系统任务与函数的标准手段。大量第三方厂商的模型库(例如门级库、硬件模型接口、SWIFT 模型适配层)都是按这一机制编译成动态加载的共享对象(shared object),并通过 Verilog-XL 的 +loadpli1= 参数在仿真启动时装入。
Icarus Verilog 的 vvp 运行时本身并不直接支持 PLI1,而是通过 libveriuser 库以 VPI(PLI2)为底层来仿真 PLI1 语义(见 开发者指南总览 中 "The PLI-1 compatibility library (libveriuser/)" 一节)。但若要加载那些已经按 Cadence 约定编译好的二进制 PLI1 应用,还需要一个专门对接 Cadence 引导协议的模块——这就是 cadpli。
平台限制:PLI1 应用是编译产物,只能在为其编译的操作系统上运行。例如,为 Linux 编译的模块只能在 Linux 下由 Icarus Verilog 加载;同理,64 位版本的 vvp 只能加载 64 位的 PLI1 应用(见 用户文档 vpi.rst 中 "Cadence PLI Modules" 一节)。
cadpli 是什么:连接两个世界的接口模块
cadpli 是随 Icarus Verilog 一起安装的一个 VPI 库模块(构建产物为 cadpli.vpl,安装在 libdir/ivl$(suffix) 目录下,见 cadpli/Makefile.in)。它扮演"翻译层"角色:
- 通过 vvp 的常规
-m参数被加载,例如-mcadpli; - 加载后主动扫描进程的命令行扩展参数,寻找
-cadpli=开头的参数; - 对每个匹配参数解析出共享对象路径与引导函数名,完成动态加载、引导与系统任务注册。
其源码位于 cadpli/cadpli.c,核心逻辑集中在 thunker_register 函数,并通过 vlog_startup_routines 数组在模块被 vvp 装载时自动执行。
基本用法:命令行示例
原文档给出了最直接的使用示例——运行 product.so 共享库,其引导函数为 my_boot:
vvp -mcadpli a.out -cadpli=./product.so:my_boot
各参数含义:
| 参数 | 作用 |
|---|---|
-mcadpli |
让 vvp 加载 cadpli.vpl 库模块,激活 -cadpli= 参数解释器 |
a.out |
由 iverilog 编译生成的 vvp 可执行程序文件 |
-cadpli=./product.so:my_boot |
指定要动态加载的 PLI1 共享对象及其引导函数 |
-cadpli= 扩展参数与 Verilog-XL 的 +loadpli1= 参数格式基本一致,这也是该模块能直接装载 Cadence 系 PLI1 应用的兼容性基础。
cadpli 模块同样可用于 iverilog 的 -m 参数,其加载机制与 vvp 一致;SWIFT 硬件模型场景下的完整示例可参见 swift.rst:
vvp -mcadpli a.out -cadpli=$LMC_HOME/lib/x86_linux.lib/swiftpli.so:swift_boot
参数格式解析:-cadpli=<module>:<boot_func>
-cadpli= 参数由两个以冒号分隔的部分构成:
<module>:共享对象(.so/ DLL)的文件路径;<boot_func>:该共享对象中导出的引导函数符号名。
从源码看,cadpli/cadpli.c 的解析流程为:
- 遍历
vlog_info.argv,用strncmp("-cadpli=", argv[idx], 8)识别前缀为-cadpli=的参数; - 跳过前 8 个字符后,用
strchr(cp, ':')定位冒号,将冒号前的部分作为模块路径,冒号后的部分作为引导函数符号名; - 模块路径与引导函数名均不能为空(源码中通过
assert保证)。
需要留意的是:该参数必须出现在 -mcadpli 之后,因为 -mcadpli 负责加载 cadpli 模块、激活参数解释器,随后 cadpli 才会扫描并处理 -cadpli= 参数。
底层实现:从共享库到系统任务注册的完整调用链
结合 cadpli/cadpli.c 与 libveriuser/veriusertfs.c,-cadpli= 参数的完整处理链路如下:
- 获取命令行参数:
thunker_register调用vpi_get_vlog_info(&vlog_info)获得进程的argc/argv; - 解析参数:识别
-cadpli=前缀并切分出<module>与<boot_func>; - 动态加载共享库:调用
ivl_dlopen(module, false)打开共享对象。ivl_dlopen在 ivl_dlfcn.h 中按平台分别封装为dlopen(Linux/Unix,RTLD_LAZY)、LoadLibrary(MinGW/Windows)或shl_load(HP-UX)。加载失败时输出"<argv> link: <dlerror>"并跳过该参数; - 解析引导符号:调用
ivl_dlsym(mod, bp)查找引导函数地址。ivl_dlsym在 ELF 平台上还兼容带前导下划线的符号(找不到时尝试去掉开头的_再查一次);符号缺失时输出"<argv>: Symbol <name> not found."; - 调用引导函数:将符号强制转换为
void* (*)(void)函数指针并调用之,得到struct t_tfcell *(即 veriusertfs 表); - 注册系统任务/函数:将得到的表交给
veriusertfs_register_table(tf)(声明于 veriuser.h,实现在 libveriuser/veriusertfs.c)。
veriusertfs 表的注册机制
veriusertfs_register_table 是 libveriuser 提供给 cadpli 的关键入口,其职责是把 PLI1 风格的 s_tfcell 表逐条转换为 VPI 系统任务/函数注册:
- 遍历
t_tfcell数组,遇到type == 0的表项即视为结尾(null-terminated); - 依据
tf->type映射为 VPI 类型:usertask→vpiSysTask;userfunction→vpiSysFunc+vpiIntFunc;userrealfunction→vpiSysFunc+vpiRealFunc;不认识的类型会被打印警告并跳过; - 用统一的 VPI 包装函数
compiletf/calltf/sizetf填充s_vpi_systf_data,通过vpi_register_systf完成注册; - 包装函数在运行时把 PLI1 的
checktf、calltf、sizetf、misctf回调以及reason_*语义翻译成 VPI 回调,并借助vpi_put_userdata/vpi_get_userdata在 VPI 句柄与 PLI1 用户数据之间建立映射; - 注册时还会设置
PLI_TRACE环境变量支持(值为-时输出到 stdout,否则写入指定文件),便于调试 PLI1 调用过程。
引导函数契约:两种引导方式
veriuser.h 明确描述了 PLI1 应用的两种引导方式:
- Cadence 兼容方式(cadpli 采用):应用导出形如
struct s_tfcell* my_bootstrap(void)的引导函数,返回 null 结尾的 veriusertfs 表;不需要vlog_startup_routines,由-mcadpli在兼容模式下加载; - Icarus Verilog 原生方式:应用提供
vlog_startup_routines数组与veriusertfs_register函数,由 vvp 自动加载(ivtest/vpi/下的putp.c、getp.c、timescale.c等测试即为该方式的典型范例)。
/* Cadence 兼容引导函数,供 cadpli 调用 */
static struct s_tfcell my_veriusertfs_table[] = {
/* ... 表项,最后以 {0,...} 结尾 ... */
};
struct s_tfcell* my_boot(void) {
return my_veriusertfs_table;
}
集成后的透明性
注册完成后,PLI1 应用即完全纳入 vvp 的系统任务分发机制。由于 libveriuser 在 VPI 之上仿真了 PLI1 的 ACC/TF 例程语义,PLI 应用几乎察觉不到自己运行在 Icarus Verilog 而非 Verilog-XL 之下——这正是原文档所称"seamless"(无缝)集成的含义。
构建与启用前提
PLI1 属于 IEEE 1364-2005 已弃用的接口。从 Icarus Verilog v13 开始,libveriuser 库与 cadpli 模块仅在配置阶段显式启用 --enable-libveriuser 时才会构建(见 安装文档 与 开发者入门指南):
./configure --enable-libveriuser
make
make install
若未启用该选项,cadpli.vpl 将不会被构建和安装,-mcadpli 将无法加载。
局限与注意事项
- 平台强绑定:只能加载与当前 vvp 同一操作系统、同一字长(32/64 位)编译的 PLI1 共享库;
- 依赖符号可见性:引导函数必须以全局符号形式导出(
ivl_dlsym才能找到),必要时需在链接时使用-rdynamic或导出表; - 参数顺序:
-cadpli=需在-mcadpli之后给出; - 调试手段:可借助
PLI_TRACE环境变量观察 veriusertfs 表的注册过程及各回调的调用序列。
进一步阅读
- 模块源码与构建规则:cadpli/cadpli.c、cadpli/Makefile.in
- PLI1 兼容库实现:libveriuser/veriusertfs.c、veriuser.h
- 平台动态加载封装:ivl_dlfcn.h
- 用户视角文档:Documentation/usage/vpi.rst 中 "Cadence PLI Modules" 一节
- 真实应用场景:SWIFT 硬件模型通过 cadpli 加载的完整说明见 Documentation/developer/guide/misc/swift.rst
- 开发者总览:Documentation/developer/guide/index.rst