大数跨境

pyuvm + cocotb + Verilator:从 VPI 到 Python 验证全流程

pyuvm + cocotb + Verilator:从 VPI 到 Python 验证全流程 ai算法芯片与系统
2026-10-08
5
导读:pyuvm 用 Python 实现 UVM,cocotb 通过 VPI 连接 Verilator,编译 RTL 后注入 Python 解释器,启动 run_test,驱动 DUT 并自动比对,形成开源

 

UVM(Universal Verification Methodology) 是一套基于 SystemVerilog 的标准化验证框架,核心是 事务级建模 + 随机约束 + 自动比对 + 覆盖率驱动 + 分层可重用。

pyuvm 是 UVM 1.2 的 Python 实现,cocotb 是协程驱动的 Python 验证框架,Verilator 是开源 Verilog 仿真器。

VPI(Verilog Procedural Interface) 是 IEEE 1364 标准定义的 C 语言接口,cocotb 正是通过 VPI 与 Verilator 通信。

pyuvm 不直接驱动 Verilator,而是通过 cocotb 的 VPI 接口间接访问编译后的 DUT 模型。

架构图

这张图给出整体分层关系,说明 pyuvm、cocotb、VPI、Verilator 之间的调用路径,核心是 pyuvm 不直接接触 Verilator,而是经过 cocotb 的 VPI 层访问 DUT。


第一部分:VPI 基础

1. VPI 核心概念

VPI 是仿真器提供的 C 语言插件接口,让外部程序能读写信号、注册回调、控制仿真。

1.1 VPI 能做什么

  • • 读写信号值:直接获取或修改 DUT 内部信号。
  • • 遍历层次:从顶层逐层访问模块、端口、变量。
  • • 注册回调:在时钟沿、信号变化、时间步触发 C 函数。
  • • 控制仿真:暂停、继续、结束仿真。
  • • 扩展系统任务:实现自定义的 $display、$dumpvars 等。
  • • force / release:强制驱动或释放信号。

1.2 VPI 核心数据结构

类型 作用
vpiHandle
不透明句柄,指向仿真器对象
s_vpi_value
信号值容器,支持多种格式
s_vpi_time
仿真时间
s_vpi_vecval
向量值
s_vpi_systf_data
系统任务注册信息
s_cb_data
回调注册信息

1.3 VPI 常用函数

函数 作用
vpi_handle_by_name
按名字获取句柄
vpi_get_value
读取信号值
vpi_put_value
写入信号值
vpi_register_cb
注册回调
vpi_register_systf
注册系统任务
vpi_get_str
获取字符串属性
vpi_free_object
释放句柄
vpi_printf
输出信息

2. VPI C 代码示例

2.1 示例 DUT

用于 VPI 示例的 RTL,信号带 /*verilator public_flat_rw*/ 标记。


   
   
   
   
    
   
   
   
   module vpi_demo (
    input
  logic       clk,
    input
  logic       rst,
    output
 logic [7:0] cnt,
    output
 logic       done
);
    logic
 [7:0] internal_reg /*verilator public_flat_rw*/;
    logic
       status_flag  /*verilator public_flat_rd*/;

    always_ff
 @(posedge clk or posedge rst) begin
        if
 (rst) begin
            cnt <= 0;
            internal_reg <= 0;
            status_flag <= 0;
        end
 else begin
            cnt <= cnt + 1;
            internal_reg <= cnt;
            status_flag <= (cnt == 8'hFF);
        end

    end

    assign
 done = status_flag;
endmodule

这段 RTL 定义了一个带内部寄存器和状态标志的计数器模块,内部信号通过 Verilator 属性标记为可被 VPI 访问,便于后续 C 代码或 Python 测试平台读写。

2.2 读写信号

通过 vpi_handle_by_name 获取句柄,vpi_get_value 读取,vpi_put_value 写入。


   
   
   
   
    
   
   
   
   #include "vpi_user.h"
#include <stdio.h>


int
 read_counter() {
    vpiHandle vh = vpi_handle_by_name(
        (PLI_BYTE8*)"vpi_demo.cnt", NULL);
    if
 (!vh) return -1;

    s_vpi_value val;
    val.format = vpiIntVal;
    vpi_get_value(vh, &val);
    vpi_printf("cnt = %d\n", val.value.integer);

    vpi_free_object(vh);
    return
 0;
}

这段 C 代码演示如何按层次名找到 vpi_demo.cnt 信号句柄,并以整数格式读取当前值,最后释放句柄。


   
   
   
   
    
   
   
   
   int write_reg(int value) {
    vpiHandle vh = vpi_handle_by_name(
        (PLI_BYTE8*)"vpi_demo.internal_reg", NULL);
    if
 (!vh) return -1;

    s_vpi_value val;
    val.format = vpiIntVal;
    val.value.integer = value;
    vpi_put_value(vh, &val, NULL, vpiNoDelay);

    vpi_free_object(vh);
    return
 0;
}

这段 C 代码演示如何按名字获取内部寄存器句柄,并用 vpi_put_value 以无延迟方式写入整数值。

Verilator 与事件驱动仿真器的重要区别:VPI 写入的值不会立即传播,必须调用顶层模型的 eval() 方法才能生效。

2.3 信号回调

注册信号变化回调,在 cnt 变化时触发 C 函数。


   
   
   
   
    
   
   
   
   #include "vpi_user.h"
#include <stdio.h>


static
 PLI_INT32 cnt_callback(p_cb_data cb_data) {
    vpiHandle vh = vpi_handle_by_name(
        (PLI_BYTE8*)"vpi_demo.cnt", NULL);
    s_vpi_value val;
    val.format = vpiIntVal;
    vpi_get_value(vh, &val);
    vpi_printf("[CB] cnt = %d\n", val.value.integer);
    vpi_free_object(vh);
    return
 0;
}

这段 C 代码定义了一个回调函数,当目标信号发生变化时,会重新读取 cnt 的值并打印出来。


   
   
   
   
    
   
   
   
   void register_cnt_callback() {
    vpiHandle vh = vpi_handle_by_name(
        (PLI_BYTE8*)"vpi_demo.cnt", NULL);
    if
 (!vh) return;

    s_cb_data cb;
    cb.reason = cbValueChange;
    cb.cb_rtn = cnt_callback;
    cb.obj = vh;
    cb.time = NULL;
    cb.value = NULL;
    cb.user_data = NULL;
    vpi_register_cb(&cb);
}

这段 C 代码把前面的回调函数注册到 cnt 信号上,触发条件是信号值变化,从而建立 VPI 事件监听。

对于信号回调,主循环必须调用 VerilatedVpi::callValueCbs() 才能触发回调。

2.4 注册系统任务

实现自定义系统任务 $my_display,类似 $display。


   
   
   
   
    
   
   
   
   #include "vpi_user.h"
#include <stdio.h>


static
 PLI_INT32 my_display_calltf(char *user_data) {
    vpiHandle systf = vpi_handle(vpiSysTfCall, NULL);
    vpiHandle arg_iter = vpi_iterate(vpiArgument, systf);

    if
 (arg_iter) {
        vpiHandle arg;
        while
 ((arg = vpi_scan(arg_iter))) {
            s_vpi_value val;
            val.format = vpiIntVal;
            vpi_get_value(arg, &val);
            vpi_printf("$my_display: %d\n",
                       val.value.integer);
        }
    }
    return
 0;
}

这段 C 代码实现自定义系统任务的主体逻辑,遍历调用时传入的参数,并以整数形式读取后打印。


   
   
   
   
    
   
   
   
   void register_my_display() {
    s_vpi_systf_data data;
    data.type = vpiSysTask;
    data.tfname = "$my_display";
    data.calltf = my_display_calltf;
    data.compiletf = NULL;
    data.sizetf = NULL;
    data.user_data = NULL;
    vpi_register_systf(&data);
}

void
 (*vlog_startup_routines[])() = {
    register_my_display,
    NULL

};

这段 C 代码把 $my_display 注册为 VPI 系统任务,并通过 vlog_startup_routines 让 Verilator 启动时自动调用注册函数。

vlog_startup_routines 是 Verilator 加载 VPI 模块时的入口点数组,仿真器启动时会自动调用其中的注册函数。

2.5 编译与运行 VPI 模块


   
   
   
   
    
   
   
   
   # 编译 VPI C 代码为共享库
gcc -shared -fPIC -o my_vpi.so my_vpi.c \
    -I$(verilator --getenv VERILATOR_ROOT)/include/vltstd

这条命令把 VPI C 代码编译成共享库,并包含 Verilator 提供的头文件路径,供后续仿真器动态加载。


   
   
   
   
    
   
   
   
   # Verilator 编译时加载 VPI 模块
verilator --cc --vpi --public-flat-rw \
    --top-module vpi_demo vpi_demo.sv \
    -LDFLAGS "-Wl,-rpath,. -L. -lmy_vpi" \
    --exe sim_main.cpp --build -j

这条命令在 Verilator 编译阶段启用 VPI,并链接前面生成的共享库,使仿真器启动时可以加载 VPI 模块。

编译出的 my_vpi.so 会被 Verilator 可执行文件在启动时加载,vlog_startup_routines 中的注册函数自动执行。


第二部分:cocotb 如何调用 VPI

3. cocotb 的 VPI 分层架构

cocotb 不直接调用 VPI,而是通过 GPI 中间层 统一不同仿真器的接口。

这张图展示 cocotb 从 Python API 到 GPI Core、再到具体 VPI 库和 Verilator 的分层结构,说明不同仿真器接口被 GPI 抽象统一。

层次 组件 作用
Python API dut.signal.value
用户接口
PyGPI cocotb.simulator
Python C 扩展
GPI Core libgpi
C/C++ 抽象层
VPI 库 libcocotbvpi_verilator
Verilator 专用实现
Verilator
仿真器
RTL 执行

关键:cocotb 本身就是一个 VPI 扩展模块。它在启动时嵌入 Python 解释器,并通过 VPI 回调将 DUT 信号暴露给 Python。

4. 启动与注册

4.1 编译链接


   
   
   
   
    
   
   
   
   SIM = verilator
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = counter.sv
TOPLEVEL = counter
MODULE = test_counter

include
 $(shell cocotb-config --makefiles)/Makefile.sim

这个 Makefile 指定使用 Verilator、Verilog 顶层语言、RTL 源文件、顶层模块名和 Python 测试模块,并引入 cocotb 提供的通用仿真 Makefile。

通过 -LDFLAGS 将 cocotb 的 VPI 库(libcocotbvpi_verilator)链接进最终的可执行文件。

4.2 启动回调

Verilator 启动时,会调用其内部的 vlog_startup_routines_bootstrap() 函数,进而调用 cocotb 提供的 VPI 启动例程。

4.3 嵌入 Python

在 cocotb 的 VPI 启动回调中,会执行 gpi_embed_init,其核心任务是 启动一个 Python 解释器,并加载你的测试模块。

4.4 开始测试

Python 解释器启动后,会立即执行 cocotb 模块的初始化逻辑,最终启动你定义的 @cocotb.test() 测试用例。

这张图描述从 Verilator 启动、加载 cocotb VPI、嵌入 Python 到执行 cocotb 测试用例的启动顺序。

5. 信号读写:从 Python 到 VPI

当你执行 dut.signal.value = 1 时,底层经历了一次完整的“翻译”过程。

这张图展示 Python 层设置信号值时,如何经过 PyGPI、GPI Core、VPI 实现,最终调用 vpi_put_value 修改 Verilator 中的信号。

  1. 1. 属性访问:Python 层通过 __setattr__ 拦截对 .value 的赋值。
  2. 2. 调用 PyGPI:Python 层调用 cocotb.simulator 模块中相应的 C 函数,如 set_signal_val_long。
  3. 3. GPI 抽象:PyGPI 将请求转发给 GPI 核心层。GPI 层通过虚函数调用到具体的 VpiImpl 类。
  4. 4. VPI 操作:VpiImpl 最终调用标准的 VPI 函数 vpi_put_value() 来修改 Verilator 模型中的信号值。

读取操作(如 val = dut.signal.value)路径类似,只是最终调用的 VPI 函数是 vpi_get_value()。

6. 事件驱动:回调与仿真推进

cocotb 的测试用例通常是 协程,它需要与仿真时间同步。这依赖于 VPI 的 回调机制。

在 Verilator 的 main 循环中,cocotb 会注册一系列 VPI 回调:

  • • cbAfterDelay:用于实现 Timer 等延时。
  • • cbValueChange:用于监听信号变化,触发 Edge 等敏感事件。
  • • cbReadWriteSynch:用于在合适的时间点同步读写操作。

当 Verilator 推进到相应的时间点或事件时,会调用已注册的 C 回调函数,这些函数再通过 GPI 层唤醒挂起的 Python 协程,实现仿真与测试的协同推进。

7. VPI 与 DPI 的区别

对比 VPI DPI
方向
C 访问仿真器
SystemVerilog 调用 C
标准
IEEE 1364
IEEE 1800
典型用途
cocotb、波形、调试
参考模型、C 函数
谁主动
外部程序
SV 代码
cocotb
用 VPI
不主要用

cocotb 对 Verilator 的驱动走的是 VPI,不是 DPI。

8. Verilator 编译选项

cocotb 调用 Verilator 时,会传入以下选项:

选项 作用 是否必需
--vpi
启用 VPI 编译
必需
--public-flat-rw
让所有信号对 VPI 可见
推荐
--prefix Vtop
设置 C++ 模型类名前缀
推荐
-lcocotbvpi_verilator
链接 cocotb VPI 库
必需

--public-flat-rw 有 性能代价,会关闭模块内联优化。如 DUT 信号层次很深,可考虑 --public-depth N 替代。

8.1 RTL 源码要改吗

通常不需要修改。--public-flat-rw 已让所有信号对 VPI 可见。

情况 需要改吗 说明
普通信号
不需要
--public-flat-rw
 已覆盖
interface 信号
可能
用 --public-depth 时可能不可见
Verilator 不支持的语法
需要
改写成可综合子集
想控制可见性
可选
用 /* verilator public */ 标记

第三部分:pyuvm 完整实战

9. 工具链版本要求

工具 最低版本 推荐版本
Verilator
5.022
5.036+
cocotb
1.9
2.0+
pyuvm
2.6
4.0+
Python
3.8
3.12

cocotb 对 Verilator 的支持目前仍是 实验性的,部分 VPI 功能可能不完整。建议使用最新版本。

10. 环境搭建

10.1 安装


   
   
   
   
    
   
   
   
   pip install pyuvm

这条命令安装 pyuvm,同时会连带安装 cocotb;仿真器需要另行准备 Verilator。

pyuvm 会 自动安装 cocotb,仿真器只需你有 Verilator。

10.2 目录结构(最小版)


   
   
   
   
    
   
   
   
   counter_tb/
├── counter.sv          # DUT
├── Makefile            # cocotb+verilator
└── test_counter.py     # cocotb + pyuvm

这个目录结构展示最小验证环境的三个核心文件,分别是 DUT、Makefile 和 Python 测试文件。

三个文件即可构成最小验证环境,DUT 与测试平台分离。

10.3 DUT:counter.sv


   
   
   
   
    
   
   
   
   module counter (
    input
  logic clk,
    input
  logic rst,
    output
 logic [7:0] cnt
);
    always_ff
 @(posedge clk or posedge rst) begin
        if
 (rst) cnt <= 0;
        else
     cnt <= cnt + 1;
    end

endmodule

这段 RTL 是一个 8 位计数器,异步复位,每个时钟上升沿加一,作为后续 pyuvm 验证环境的目标 DUT。

8 位计数器,异步复位,每个时钟上升沿加一,是验证平台的目标。

10.4 Makefile(Verilator)


   
   
   
   
    
   
   
   
   SIM = verilator
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = counter.sv
TOPLEVEL = counter
MODULE = test_counter

include
 $(shell cocotb-config --makefiles)/Makefile.sim

这个 Makefile 配置 cocotb 使用 Verilator 仿真器,指定 RTL 文件、顶层模块和 Python 测试模块,并加载 cocotb 的仿真规则。

变量 作用 说明
SIM
选择仿真器
设为 verilator
TOPLEVEL_LANG
RTL 语言
verilog
 或 vhdl
VERILOG_SOURCES
RTL 源文件
可多个
TOPLEVEL
顶层模块名
对应 module counter
MODULE
Python 测试模块
不带 .py 后缀

cocotb-config --makefiles 返回 cocotb 的通用 Makefile,根据 SIM 自动加载 Verilator 适配。

10.5 波形与覆盖率(可选)


   
   
   
   
    
   
   
   
   EXTRA_ARGS += --trace --trace-structs
EXTRA_ARGS += --coverage --coverage-line --coverage-toggle

这些额外参数会传给 Verilator,用于生成波形文件和覆盖率信息,便于调试和验证收敛。

EXTRA_ARGS 会同时传给 Verilator 的编译阶段和运行阶段,用于生成波形或覆盖率。

11. pyuvm 与标准 UVM 对比

维度 标准 UVM pyuvm
语言
SystemVerilog
Python
环境
商业仿真器
cocotb+Verilator
事务
sequence_item
uvm_sequence_item
序列
sequence
uvm_sequence
驱动
driver
uvm_driver
监控
monitor
uvm_monitor
记分板
scoreboard
uvm_scoreboard
连接
TLM
TLM
配置
config_db
ConfigDB
工厂
factory
自动注册
Phase
四阶段
同 UVM
随机
rand/constraint
Python random
覆盖
covergroup
cocotb_coverage
生态
VIP丰富
Python生态
性能
事件驱动
解释型
适用
SoC/大团队
快速原型/教学

pyuvm 保持了 UVM 的分层与 TLM 语义,但用 Python 的 async/await 替代 SystemVerilog 的 phase 调度。

12. 完整测试平台:test_counter.py(拆分为多个部分)

以下各部分组合起来就是完整的 test_counter.py。每个部分独立说明,便于阅读和复用。

12.1 导入与基础


   
   
   
   
    
   
   
   
   import cocotb
from
 cocotb.triggers import RisingEdge
from
 cocotb.clock import Clock
import
 pyuvm
from
 pyuvm import *

这段导入 cocotb 的时钟、边沿触发器以及 pyuvm 的公共组件,为后续测试平台提供基础依赖。

导入 cocotb 时钟、边沿触发器和 pyuvm 全部组件。

12.2 Sequence Item


   
   
   
   
    
   
   
   
   class CounterItem(uvm_sequence_item):
    def
 __init__(self, name="CounterItem"):
        super
().__init__(name)
        self
.nclks = 10

这个类继承 uvm_sequence_item,用来承载一次激励的数据,这里只包含需要等待的时钟周期数 nclks。

对应 uvm_sequence_item,承载一次激励的数据,这里仅携带 nclks。

12.3 Driver


   
   
   
   
    
   
   
   
   class CounterDriver(uvm_driver):
    def
 build_phase(self):
        self
.dut = cocotb.top

    async
 def run_phase(self):
        while
 True:
            req = await self.seq_item_port.get_next_item()
            self
.dut.rst.value = 1
            await
 RisingEdge(self.dut.clk)
            self
.dut.rst.value = 0
            for
 _ in range(req.nclks):
                await
 RisingEdge(self.dut.clk)
            self
.seq_item_port.item_done()

这个 driver 从 sequencer 获取激励项,先驱动复位一个周期,再等待指定数量的时钟上升沿,最后通知 item 完成。

通过 cocotb.top 拿到 DUT 句柄,先复位一个周期,再等待 nclks 个时钟。

12.4 Monitor


   
   
   
   
    
   
   
   
   class CounterMonitor(uvm_monitor):
    def
 build_phase(self):
        self
.dut = cocotb.top
        self
.ap = uvm_analysis_port("ap", self)

    async
 def run_phase(self):
        while
 True:
            await
 RisingEdge(self.dut.clk)
            self
.ap.write(int(self.dut.cnt.value))

这个 monitor 在每个时钟上升沿采样计数器输出,并通过 analysis port 把采样值广播给 scoreboard。

每个时钟沿采样 cnt,通过 analysis_port 广播给 scoreboard。

12.5 Scoreboard


   
   
   
   
    
   
   
   
   class CounterScoreboard(uvm_scoreboard):
    def
 build_phase(self):
        self
.expected = 0

    def
 write(self, got):
        assert
 got == self.expected, \
            f"FAIL: got {got}, expected {self.expected}"

        self
.expected = (self.expected + 1) & 0xFF
        self
.logger.info(f"OK cnt={got}")

这个 scoreboard 维护期望计数值,每收到一次采样就进行比对,成功时更新期望值并输出日志。

用 assert 自动比对期望值,logger 输出结果。

12.6 Environment


   
   
   
   
    
   
   
   
   class CounterEnv(uvm_env):
    def
 build_phase(self):
        self
.seqr = uvm_sequencer("seqr", self)
        self
.driver = CounterDriver("driver", self)
        self
.mon = CounterMonitor("mon", self)
        self
.sb = CounterScoreboard("sb", self)

    def
 connect_phase(self):
        self
.driver.seq_item_port.connect(
            self
.seqr.seq_item_export)
        self
.mon.ap.connect(self.sb)

这个 env 在 build 阶段创建 sequencer、driver、monitor 和 scoreboard,在 connect 阶段完成 TLM 端口连接。

build_phase 创建组件,connect_phase 连接 TLM 端口。

12.7 Sequence


   
   
   
   
    
   
   
   
   class CounterSeq(uvm_sequence):
    async
 def body(self):
        item = CounterItem()
        await
 self.start_item(item)
        await
 self.finish_item(item)

这个 sequence 产生一个 CounterItem,通过 sequencer 发送给 driver,是测试激励的入口。

产生一个 CounterItem,通过 sequencer 发给 driver。

12.8 Test


   
   
   
   
    
   
   
   
   @pyuvm.test()
class
 CounterTest(uvm_test):
    def
 build_phase(self):
        self
.env = CounterEnv("env", self)

    async
 def run_phase(self):
        self
.raise_objection()
        seq = CounterSeq("seq")
        await
 seq.start(self.env.seqr)
        self
.drop_objection()

这个测试类创建环境,在 run 阶段启动 sequence,并通过 objection 控制仿真结束。

@pyuvm.test() 装饰器自动注册测试类,用 objection 控制仿真结束。

12.9 cocotb 入口


   
   
   
   
    
   
   
   
   @cocotb.test()
async
 def test_counter(dut):
    cocotb.top = dut
    cocotb.start_soon(
        Clock(dut.clk, 10, units="ns").start())
    dut.rst.value = 0
    await
 uvm_root().run_test("CounterTest")

这个 cocotb 测试入口保存 DUT 句柄,启动时钟,初始化复位信号,并调用 pyuvm 的 run_test 启动指定测试类。

pyuvm 的 run_test() 必须在 cocotb 的 @cocotb.test() 里启动,时钟由 cocotb 启动,DUT 句柄通过 cocotb.top 传递。


第四部分:整合 VPI 示例

13. 数据流与 Phase

这张图展示 pyuvm 测试平台中的数据流和 phase 调度关系,说明激励、驱动、采样、比对如何按阶段推进。

这张图进一步补充 phase 与组件之间的执行顺序,帮助理解 build、connect、run 等阶段如何组织验证平台。

14. 运行与查看结果


   
   
   
   
    
   
   
   
   # 基本运行
make

这条命令执行默认 Makefile 流程,完成 Verilator 编译、cocotb 启动和 pyuvm 测试运行。


   
   
   
   
    
   
   
   
   # 带波形
make EXTRA_ARGS="--trace --trace-structs"
gtkwave dump.vcd

这条命令在运行仿真时启用波形跟踪,并用 gtkwave 打开生成的波形文件查看信号变化。


   
   
   
   
    
   
   
   
   # 带覆盖率
make EXTRA_ARGS="--coverage"

这条命令在运行仿真时启用覆盖率收集,用于观察代码行和翻转覆盖情况。

运行成功后,cocotb 会输出日志:


   
   
   
   
    
   
   
   
   INFO     cocotb: Running test_counter
INFO     pyuvm: build_phase
INFO     pyuvm: connect_phase
INFO     pyuvm: run_phase
INFO     scoreboard: OK cnt=0
INFO     scoreboard: OK cnt=1
INFO     scoreboard: OK cnt=2
...
INFO     cocotb: Test Passed

这段日志展示一次成功测试的典型输出,包括测试启动、phase 执行、scoreboard 比对成功以及最终测试通过。

15. 完整验证平台:test_vpi_demo.py(拆分为多个部分)

将 VPI 示例中的 vpi_demo 整合到 pyuvm 测试平台。以下各部分组合起来就是完整的 test_vpi_demo.py。

15.1 DUT 与 Makefile


   
   
   
   
    
   
   
   
   module vpi_demo (
    input
  logic       clk,
    input
  logic       rst,
    output
 logic [7:0] cnt,
    output
 logic       done
);
    logic
 [7:0] internal_reg /*verilator public_flat_rw*/;
    logic
       status_flag  /*verilator public_flat_rd*/;

    always_ff
 @(posedge clk or posedge rst) begin
        if
 (rst) begin
            cnt <= 0;
            internal_reg <= 0;
            status_flag <= 0;
        end
 else begin
            cnt <= cnt + 1;
            internal_reg <= cnt;
            status_flag <= (cnt == 8'hFF);
        end

    end

    assign
 done = status_flag;
endmodule

这段 DUT 与前面的 VPI 示例一致,包含可被 VPI 访问的内部寄存器和状态标志,并输出计数值和完成信号。


   
   
   
   
    
   
   
   
   SIM = verilator
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = vpi_demo.sv
TOPLEVEL = vpi_demo
MODULE = test_vpi_demo

EXTRA_ARGS += --vpi --public-flat-rw
EXTRA_ARGS += --trace --trace-structs

include
 $(shell cocotb-config --makefiles)/Makefile.sim

这个 Makefile 针对 vpi_demo 配置 Verilator,启用 VPI 和公开信号访问,同时打开波形跟踪,并指定 Python 测试模块。

DUT 内部信号用 /*verilator public_flat_rw*/ 标记,Makefile 中启用 --vpi 和 --public-flat-rw 使信号对 VPI 可见。

15.2 导入与 Item


   
   
   
   
    
   
   
   
   import cocotb
from
 cocotb.triggers import RisingEdge
from
 cocotb.clock import Clock
import
 pyuvm
from
 pyuvm import *


class
 VpiDemoItem(uvm_sequence_item):
    def
 __init__(self, name="VpiDemoItem"):
        super
().__init__(name)
        self
.nclks = 20

这段代码导入必要模块,并定义携带 nclks 的激励项,用于控制 driver 等待的时钟周期数。

定义激励数据,携带 nclks 表示运行时钟周期数。

15.3 Driver


   
   
   
   
    
   
   
   
   class VpiDemoDriver(uvm_driver):
    def
 build_phase(self):
        self
.dut = cocotb.top

    async
 def run_phase(self):
        while
 True:
            req = await self.seq_item_port.get_next_item()
            self
.dut.rst.value = 1
            await
 RisingEdge(self.dut.clk)
            self
.dut.rst.value = 0
            for
 _ in range(req.nclks):
                await
 RisingEdge(self.dut.clk)
            self
.seq_item_port.item_done()

这个 driver 负责驱动复位信号,并根据激励项中的周期数等待时钟,完成后释放 item。

驱动复位信号,并在指定周期内等待时钟沿。

15.4 Monitor


   
   
   
   
    
   
   
   
   class VpiDemoMonitor(uvm_monitor):
    def
 build_phase(self):
        self
.dut = cocotb.top
        self
.ap = uvm_analysis_port("ap", self)

    async
 def run_phase(self):
        while
 True:
            await
 RisingEdge(self.dut.clk)
            cnt = int(self.dut.cnt.value)
            done = int(self.dut.done.value)
            self
.ap.write((cnt, done))

这个 monitor 在每个时钟上升沿采样 cnt 和 done,并把两者打包后通过 analysis port 发送。

每个时钟沿采样 cnt 和 done,通过 analysis port 发送。

15.5 Scoreboard


   
   
   
   
    
   
   
   
   class VpiDemoScoreboard(uvm_scoreboard):
    def
 build_phase(self):
        self
.expected = 0

    def
 write(self, data):
        cnt, done = data
        assert
 cnt == self.expected, \
            f"FAIL: cnt={cnt}, expected={self.expected}"

        if
 cnt == 0xFF:
            assert
 done == 1, "FAIL: done=1 at cnt=0xFF"
        self
.expected = (self.expected + 1) & 0xFF
        self
.logger.info(f"OK cnt={cnt} done={done}")

这个 scoreboard 比对计数值是否符合期望,并在计数值达到 0xFF 时检查 done 信号是否正确。

自动比对计数值,并在计数值达到 0xFF 时检查 done 信号。

15.6 Environment


   
   
   
   
    
   
   
   
   class VpiDemoEnv(uvm_env):
    def
 build_phase(self):
        self
.seqr = uvm_sequencer("seqr", self)
        self
.driver = VpiDemoDriver("driver", self)
        self
.mon = VpiDemoMonitor("mon", self)
        self
.sb = VpiDemoScoreboard("sb", self)

    def
 connect_phase(self):
        self
.driver.seq_item_port.connect(
            self
.seqr.seq_item_export)
        self
.mon.ap.connect(self.sb)

这个 env 创建并连接 sequencer、driver、monitor 和 scoreboard,组成完整验证环境。

创建并连接 sequencer、driver、monitor、scoreboard。

15.7 Sequence


   
   
   
   
    
   
   
   
   class VpiDemoSeq(uvm_sequence):
    async
 def body(self):
        item = VpiDemoItem()
        await
 self.start_item(item)
        await
 self.finish_item(item)

这个 sequence 生成一个 VpiDemoItem,并通过 sequencer 发送给 driver。

产生一个 VpiDemoItem,通过 sequencer 发送给 driver。

15.8 Test


   
   
   
   
    
   
   
   
   @pyuvm.test()
class
 VpiDemoTest(uvm_test):
    def
 build_phase(self):
        self
.env = VpiDemoEnv("env", self)

    async
 def run_phase(self):
        self
.raise_objection()
        seq = VpiDemoSeq("seq")
        await
 seq.start(self.env.seqr)
        self
.drop_objection()

这个 pyuvm 测试类创建环境,启动 sequence,并用 objection 控制仿真运行和结束。

pyuvm 测试类,启动 sequence,用 objection 控制仿真结束。

15.9 cocotb 入口


   
   
   
   
    
   
   
   
   @cocotb.test()
async
 def test_vpi_demo(dut):
    cocotb.top = dut
    cocotb.start_soon(
        Clock(dut.clk, 10, units="ns").start())
    dut.rst.value = 0
    await
 uvm_root().run_test("VpiDemoTest")

这个 cocotb 入口保存 DUT 句柄,启动时钟,初始化复位,并运行 pyuvm 测试类。

cocotb 的 VPI 层自动处理信号访问:dut.cnt.value 底层通过 vpi_get_value 读取,dut.rst.value = 0 底层通过 vpi_put_value 写入。

16. 常见问题排查

现象 原因 解决
verilator not found
Verilator 未安装
apt install verilator
Vtop has no member
Verilator 版本过低
升级到 5.022+
仿真不结束
缺少 objection
test 中加 raise/drop
波形为空
未加 --trace
EXTRA_ARGS += --trace
信号无法驱动
Python 环境冲突
使用干净的 venv
VPI 信号不可见
未加 --public-flat-rw
加 EXTRA_ARGS
VPI 写入不生效
未调用 eval()
Verilator 需显式 eval
corrupted size 崩溃
Verilator 版本 bug
升级或降级 Verilator

重要提醒:pyuvm 的 run_phase 必须是 async def;只有 test 里 raise_objection / drop_objection 才能让仿真正确结束。

17. 总结

cocotb 调用 VPI 的流程:编译时链接 VPI 库 → 启动时注入 Python 解释器 → 运行时通过 GPI 层代理 VPI 调用 → 事件回调驱动协程调度。

pyuvm + cocotb + Verilator 的流程:Verilator 编译 RTL(--vpi)→ cocotb 加载 VPI 库 → 嵌入 Python → pyuvm 启动 run_test → 组件通过 VPI 驱动 DUT → 自动比对。

  • • VPI 本质:C 语言接口,连接外部程序与仿真器。
  • • VPI 核心操作:vpi_handle_by_name 获取句柄、vpi_get_value 读、vpi_put_value 写、vpi_register_cb 回调。
  • • cocotb VPI 分层:Python API → PyGPI → GPI Core → VPI 库 → Verilator。
  • • 编译关键:--vpi 启用 VPI、--public-flat-rw 暴露信号、-lcocotbvpi_verilator 链接 cocotb。
  • • RTL 修改:通常不需要,由 Verilator 编译选项控制信号可见性。
  • • 优势:Python 开发快、开源工具链、与 UVM 概念一一对应。
  • • 劣势:解释型性能较低、随机约束需自己实现、生态不如商业 UVM 成熟。
  • • 适用:模块级验证、快速原型、Python 背景团队、开源工具链项目。
  • • 核心结论:pyuvm + cocotb + Verilator 是 Verilator 生态下最接近标准 UVM 的 Python 验证方案,VPI 是连接 Python 世界和 RTL 世界的桥梁。

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 229
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读6.0k
粉丝0
内容229