Icarus Verilog cadpli 模块:在 vvp 中直接运行 Verilog-XL / NC-Verilog 编译的 PLI1 应用

原创2026-10-03 21:04:511,754 阅读
文章标签:编译器编程语言硬件开发

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 的解析流程为:

  1. 遍历 vlog_info.argv,用 strncmp("-cadpli=", argv[idx], 8) 识别前缀为 -cadpli= 的参数;
  2. 跳过前 8 个字符后,用 strchr(cp, ':') 定位冒号,将冒号前的部分作为模块路径,冒号后的部分作为引导函数符号名;
  3. 模块路径与引导函数名均不能为空(源码中通过 assert 保证)。

需要留意的是:该参数必须出现在 -mcadpli 之后,因为 -mcadpli 负责加载 cadpli 模块、激活参数解释器,随后 cadpli 才会扫描并处理 -cadpli= 参数。

底层实现:从共享库到系统任务注册的完整调用链

结合 cadpli/cadpli.c 与 libveriuser/veriusertfs.c,-cadpli= 参数的完整处理链路如下:

  1. 获取命令行参数:thunker_register 调用 vpi_get_vlog_info(&vlog_info) 获得进程的 argc/argv;
  2. 解析参数:识别 -cadpli= 前缀并切分出 <module> 与 <boot_func>;
  3. 动态加载共享库:调用 ivl_dlopen(module, false) 打开共享对象。ivl_dlopen 在 ivl_dlfcn.h 中按平台分别封装为 dlopen(Linux/Unix,RTLD_LAZY)、LoadLibrary(MinGW/Windows)或 shl_load(HP-UX)。加载失败时输出 "<argv> link: <dlerror>" 并跳过该参数;
  4. 解析引导符号:调用 ivl_dlsym(mod, bp) 查找引导函数地址。ivl_dlsym 在 ELF 平台上还兼容带前导下划线的符号(找不到时尝试去掉开头的 _ 再查一次);符号缺失时输出 "<argv>: Symbol <name> not found.";
  5. 调用引导函数:将符号强制转换为 void* (*)(void) 函数指针并调用之,得到 struct t_tfcell *(即 veriusertfs 表);
  6. 注册系统任务/函数:将得到的表交给 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 表的注册过程及各回调的调用序列。

进一步阅读

登录后查看全文
iverilog