社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

ai算法芯片与系统 • 4 周前 • 155 次点击  

 

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 


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 


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 


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 APIdut.signal.value
用户接口
PyGPIcocotb.simulator
Python C 扩展
GPI Corelibgpi
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 的区别

对比VPIDPI
方向
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 对比

维度标准 UVMpyuvm
语言
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 世界的桥梁。

 


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/201922