Verilog HDL 学习笔记(十三)| 第13章 编程语言接口(PLI)

13.1 什么是PLI?

一、为什么需要PLI?

Verilog虽然功能强大,但它毕竟是一种硬件描述语言 ,不是通用编程语言。在实际工程中,我们常常会遇到一些纯Verilog难以甚至无法完成的任务:

场景 纯Verilog的局限 PLI的解决方案
从外部文件读取复杂格式的数据 $readmemb/$readmemh格式固定 用C语言实现任意格式的数据解析
与C语言模型协同仿真 Verilog无法直接调用C函数 通过PLI在仿真中调用C模型
自定义调试工具 只能使用有限的系统任务 创建自己的 $my_display 等调试工具
提取设计层次信息 无法获取当前模块的父模块名 用PLI访问内部数据结构
生成复杂测试激励 激励生成能力有限 用C语言生成任意复杂的测试向量
与外部工具交互 Verilog是封闭的 通过PLI连接MATLAB、Simulink等工具

一句话总结PLI :PLI是Verilog的"API接口",让你可以用C/C++写函数,然后在Verilog里像调用 $display 一样调用它们

二、PLI的正式定义

编程语言接口(Programming Language Interface,PLI) 是Verilog标准(IEEE 1364)中定义的一组接口子程序(Interface Routines)

通过PLI,C/C++程序可以:

  • 读取 Verilog仿真器的内部数据结构

  • 写入/修改 Verilog仿真器的内部数据结构

  • 提取 仿真环境的信息

三、PLI的几个关键术语

术语 含义
系统任务/系统函数 Verilog中以 $ 开头的内置任务/函数(如 $display$monitor
用户自定义系统任务 用户通过PLI自己创建的、以 $ 开头的任务
用户自定义C子程序 用C语言编写的、实现用户自定义系统任务具体功能的函数
PLI库子程序 Verilog仿真器提供的标准PLI接口函数(如 io_printftf_putp等)

13.2 PLI的发展历程

PLI经历了三个发展阶段,每一代都在前一代的基础上增加了更多功能:

一、第一代PLI(PLI 1.0):TF和ACC子程序

这是最早期的PLI实现,包含两类子程序:

子程序类型 前缀 主要功能
任务/函数子程序 tf_ 处理用户自定义任务的参数、实用函数、回调机制、输出设备写入
存储/存取子程序 acc_ 提供面向对象的访问,直接读写Verilog结构描述中的对象

tf_acc_的定位tf_主要处理"任务调用"相关的事情(参数传递、返回值),acc_主要处理"设计数据"相关的事情(读写信号、遍历层次)。

二、第二代PLI(PLI 2.0):VPI子程序

Verilog 2001标准引入了VPI(Verilog Procedural Interface)

特点 说明
功能超集 VPI是 tf_acc_ 功能的超集,包含了两者的所有功能
统一接口 提供了更统一、更规范的API
推荐使用 新设计应优先使用VPI

VPI vs TF/ACC :你可以把TF/ACC想象成"旧版API",VPI是"新版API"。VPI功能更强大、更规范,新设计应该用VPI。但由于大量遗留代码仍在使用TF/ACC,理解它们仍然有意义。

三、第三代:DPI(SystemVerilog时代)

SystemVerilog引入了DPI(Direct Programming Interface) ,比PLI更简单、更高效。但本书主要讨论传统的PLI。

DPI(Direct Programming Interface)是SystemVerilog(SV)与外来编程语言(如C、C++、SystemC等)之间的接口 。它允许在SV和外部语言之间进行直接、双向的函数调用,你可以把它理解为一个更现代、更便捷的"桥梁",用于连接硬件验证环境和软件世界。

DPI vs. PLI:为什么有了PLI还需要DPI?

在DPI出现之前,Verilog主要通过PLI( Programming Language Interface) 来调用C函数。PLI虽然功能强大,但使用起来非常复杂。DPI就是为了解决PLI的痛点而生的。

特性 PLI (传统方式) DPI (现代方式)
使用复杂度 非常复杂。需要编写大量接口代码,并维护一个"注册表"来映射C函数和SV系统任务。 非常简单 。只需一行 import 声明即可"导入"C函数,无需繁琐的注册过程。
函数调用 通过$开头的系统任务/函数 (如 $my_task)间接调用。 像调用本地SV函数一样直接调用导入的C函数。
类型安全 无参数类型检查,容易出错。 支持参数原型检查,编译器可在编译时发现类型不匹配的错误。
双向交互 单向。主要是SV调用C。 双向 。不仅SV能调用C (import),C代码也能回调SV中定义的函数或任务 (export)。
应用场景 功能强大,能深入仿真内核,进行底层控制(如强制赋值、事件同步)。 适用于**"简单数据"交互** ,在验证环境中集成C/C++参考模型、算法库等。

DPI 核心特性与工作原理

1. 两种方向的函数调用
  • import(导入):在SV代码中声明一个用C语言实现的函数。当SV调用它时,控制权就转移到C函数。

  • export(导出):在SV代码中声明一个SV函数/任务,并将其"导出"给C语言调用。

2. 数据类型映射 (Data Type Mapping)

SV和C有各自的数据类型,DPI通过 svdpi.h 头文件定义了标准的映射关系。

  • 简单类型int, byte, string 等可以直接映射。

  • 复杂类型 :SV的 logicregbit、数组等需要映射到DPI提供的特殊类型,如 svLogicsvBitVecVal 等。

完整代码示例

一个经典的"Hello World"DPI示例。

1. C语言代码 (hello.c)

复制代码
// 必须包含DPI标准头文件
#include <stdio.h>
#include "svdpi.h"

// C函数实现:打印一条消息
int hello_from_c() {
    printf("Hello from C function!\n");
    return 0;
}

// C函数实现:计算两个整数的和
int add_from_c(int a, int b) {
    return a + b;
}

2. SystemVerilog代码 (top.sv)

复制代码
module top;

    // 使用 "DPI-C" 规范导入C函数
    import "DPI-C" function int hello_from_c();
    import "DPI-C" function int add_from_c(input int a, input int b);

    int result;
    initial begin
        $display("Entering SystemVerilog...");
        
        // 像调用本地函数一样调用C函数
        hello_from_c();
        result = add_from_c(10, 20);
        $display("Result from C add function: %0d", result);
        
        $finish;
    end

endmodule
代码说明
  • import "DPI-C":这是DPI的核心导入声明。

  • function <返回值类型> <函数名>(<参数列表>):声明的形式与SV函数完全一致。调用时也和本地函数一样。

  • 编译与运行 :在实际使用中,需要先将C代码编译成共享库(.so.dll),然后在运行仿真器时指定该库的路径。

DPI 的主要应用场景

  • 集成参考模型:在验证图像处理、加密算法等模块时,直接调用已有的C/C++模型代码作为"黄金参考"。

  • 生成复杂激励:利用C语言的强大能力(如文件I/O、复杂算法)生成SV难以构造的测试数据。

  • 实现BFM (总线功能模型):用C语言实现复杂的总线协议行为,供SV测试环境调用。

  • 连接外部工具:作为SV与MATLAB/Simulink等工具协同仿真的桥梁。

13.3 PLI的工作原理

一、核心工作流程

PLI的工作流程可以概括为四个步骤

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                        PLI 工作流程                                         │
│                                                                             │
│  第1步:编写C函数                                                           │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │  #include "veriuser.h"                                             │    │
│  │  int my_task() {                                                    │    │
│  │      io_printf("Hello from C!\\n");                                │    │
│  │      return 0;                                                     │    │
│  │  }                                                                 │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                    ↓                                        │
│  第2步:编译C函数为共享库(.so / .dll)[reference:29]                         │
│                                    ↓                                        │
│  第3步:将PLI任务注册/链接到仿真器[reference:30]                              │
│                                    ↓                                        │
│  第4步:在Verilog代码中调用[reference:31]                                        │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │  module test;                                                      │    │
│  │      initial $my_task;  // 像调用系统任务一样调用!                │    │
│  │  endmodule                                                         │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────────────┘

二、详细的仿真执行流程

当仿真器执行Verilog代码时,遇到用户自定义系统任务会发生什么?

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                      仿真执行流程                                            │
│                                                                             │
│  1. 设计者编写Verilog设计和激励,其中包含用户自定义系统任务调用[reference:33]  │
│                          ↓                                                  │
│  2. 编译器和仿真器将Verilog代码转换为内部数据结构[reference:34]               │
│     (这种内部数据格式是仿真器私有的,用户不可读)[reference:35]              │
│                          ↓                                                  │
│  3. 仿真开始运行[reference:36]                                                   │
│                          ↓                                                  │
│  4. 仿真器执行到 `$my_task` 时[reference:37]                                 │
│                          ↓                                                  │
│  5. 执行控制权**转移**给对应的C函数[reference:38]                            │
│                          ↓                                                  │
│  6. C函数执行完毕后,**返回**控制权给仿真器[reference:39]                        │
│                          ↓                                                  │
│  7. 仿真继续执行后续代码[reference:40]                                           │
└─────────────────────────────────────────────────────────────────────────────┘

三、内部数据表示

在仿真器内部,Verilog设计被表示为一组对象(Objects)

对象类型 举例
模块实例 top.u1top.u2
模块端口 top.u1.clktop.u1.data
原语实例 and门、nand
原语端口(Terminal) 门的输入/输出引脚
线网(Net) wirewandwor
寄存器(Reg) reg变量
路径 模块内部的端到端路径、模块间的路径

PLI的acc_vpi_子程序就是用来访问和操作这些内部对象的。通过PLI,你可以:

  • 遍历设计层次(找到某个模块的所有子模块)

  • 读取任意信号的值

  • 修改任意信号的值

  • 获取设计信息(扇出、互联等)

13.4 PLI库子程序

一、库子程序的分类

PLI库子程序主要分为两类:

类别 功能 典型子程序
存取子程序(Access Routines) 读写或修改设计对象(信号、模块、路径等) acc_fetch_valueacc_handle_*
实用子程序(Utility Routines) 数据传输、格式化输出等辅助功能 io_printftf_putp

二、常用的PLI库子程序

1. 实用子程序(Utility Routines)
子程序 功能
io_printf 格式化输出到标准输出或文件
tf_putp 将值传递给Verilog任务/函数的输出参数
tf_getp 从Verilog任务/函数获取输入参数
2. 存取子程序(Access Routines)
子程序 功能
acc_handle_* 获取设计对象的句柄(handle)
acc_fetch_value 读取对象的值
acc_fetch_type 获取对象的类型信息
acc_fetch_name 获取对象的层次名
3. VPI子程序(VPI Routines)

VPI提供了更统一的接口:

子程序 功能
vpi_printf 格式化输出
vpi_get_value 获取信号值
vpi_put_value 设置信号值
vpi_register_cb 注册回调函数

13.5 完整示例:创建 $hello_verilog 系统任务

这是一个经典的"Hello World"示例,展示创建用户自定义系统任务的完整流程。

第一步:编写C子程序

复制代码
// ============================================================
// 文件名:hello_verilog.c
// 功能:实现 $hello_verilog 系统任务的C子程序
// ============================================================

#include "veriuser.h"    // PLI 标准头文件[reference:57]

// 用户自定义系统任务的C实现
int hello_verilog() {
    // io_printf 是PLI提供的实用子程序,用于格式化输出[reference:58]
    io_printf("Hello Verilog World!\n");
    return 0;            // 返回0表示成功
}

第二步:注册PLI任务

不同的仿真器注册方式略有不同,但基本原理相同。以下是典型方式:

复制代码
// ============================================================
// 文件名:veriuser.c
// 功能:将 $hello_verilog 注册到仿真器
// ============================================================

#include "veriuser.h"

// 定义任务/函数条目结构体
s_tfcell veriusertfs[] = {
    // 用户任务条目
    {usertask, 0, 0, 0, hello_verilog, 0, "$hello_verilog"},
    // 结束标记
    {0, 0, 0, 0, 0, 0, 0}
};

第三步:编译并链接到仿真器

复制代码
# Linux/Unix(生成共享库)
gcc -shared -o hello_verilog.so hello_verilog.c veriuser.c -I/path/to/simulator/include

# Windows(生成DLL)
# 具体命令取决于使用的仿真器

第四步:在Verilog中调用

复制代码
// ============================================================
// 文件名:test.v
// 功能:调用用户自定义系统任务 $hello_verilog
// ============================================================

module hello_top;
    initial begin
        $hello_verilog;    // 像调用内置系统任务一样调用!
        $finish;
    end
endmodule

仿真输出:

复制代码
Hello Verilog World!

13.6 PLI的应用场景

PLI的应用非常广泛:

一、自定义系统任务和函数

应用 说明
监控任务 自定义信号监视器,比 $monitor 更灵活
激励任务 生成复杂的测试向量
调试任务 自定义调试输出工具

二、设计信息提取

应用 说明
层次分析 遍历设计层次,获取模块实例信息
互联分析 提取信号连接关系
扇出分析 计算信号的扇出数

三、EDA工具开发

应用 说明
波形查看器 生成VCD等波形格式
代码浏览器 提取源代码结构和层次信息
延迟计算器 计算路径延迟

四、协同仿真

应用 说明
C模型接口 在Verilog仿真中调用C语言模型
MATLAB/Simulink协同 与MATLAB/Simulink联合仿真
外部工具集成 连接其他EDA工具
相关推荐
点心的游戏开发世界2 小时前
UnityShaderGraph滚动UV笔记
笔记·uv
瑞码空间3 小时前
git知识点黄金笔记
笔记·git·elasticsearch
我爱cope3 小时前
【计算机网络 | 网络层11:ICMP 协议:Ping 和 Traceroute 的底层原理】
网络·学习·计算机网络
天若有情6734 小时前
Node+MySQL小型全栈笔记项目实战课程分享
数据库·笔记·mysql
问天_观心4 小时前
大模型微调学习(一)
开发语言·人工智能·学习·语言模型·github
苏生Susheng4 小时前
【软件实施】Linux系统Shell脚本教程
linux·运维·服务器·chrome·spring boot·学习·实施
我命由我123455 小时前
Photoshop - Photoshop 把两个 PSD 文件合并
学习·ui·职场和发展·产品运营·产品经理·学习方法·photoshop
摇滚侠6 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 JDBC 事务管理 阅读笔记 33
spring boot·笔记·后端
小雪崩7 小时前
嵌入式学习 day39:TCP并发服务器模型
linux·服务器·c语言·网络·学习·tcp/ip