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_printf、tf_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的 logic、reg、bit、数组等需要映射到DPI提供的特殊类型,如 svLogic、svBitVecVal 等。

完整代码示例

一个经典的"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.u1、top.u2
模块端口 top.u1.clk、top.u1.data
原语实例 and门、nand门
原语端口(Terminal) 门的输入/输出引脚
线网(Net) wire、wand、wor
寄存器(Reg) reg变量
路径 模块内部的端到端路径、模块间的路径

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

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

  • 读取任意信号的值

  • 修改任意信号的值

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

13.4 PLI库子程序

一、库子程序的分类

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

类别 功能 典型子程序
存取子程序(Access Routines) 读写或修改设计对象(信号、模块、路径等) acc_fetch_value、acc_handle_*
实用子程序(Utility Routines) 数据传输、格式化输出等辅助功能 io_printf、tf_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工具
相关推荐
小lu飞1 小时前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程
前端·笔记·ai·ai编程
无心水5 小时前
24、系统架构师论文写作专题 核心考点与学习笔记
笔记·学习·系统架构·架构师·安全架构·软考架构师·软考系统架构设计师
蒸蒸yyyyzwd5 小时前
邓明 Agent 系统面试-意图识别学习笔记
笔记·学习
Zhao-Jichao8 小时前
AI Prompting for Everyone 课程笔记 - P21 总结 Conclusion
人工智能·笔记
欢欢李8 小时前
【LTE L3 笔记 05】LTE同步信号
笔记·无线通信·物理层·lte·空口协议
La Pulga10 小时前
【FreeRTOS源码学习笔记】空闲任务与阻塞延时
笔记·mcu·学习·嵌入式·freertos·实时操作系统
蒸蒸yyyyzwd10 小时前
秋招学习笔记 day106
笔记·学习
小李不想当小白11 小时前
WDG看门狗(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
欢欢李12 小时前
【LTE L3 笔记 04】LTE帧结构
笔记·无线通信·物理层·lte·空口协议
Zhao-Jichao14 小时前
AI Prompting for Everyone 课程笔记 - P20 实验与期末项目 Lab and Final Project
前端·人工智能·笔记