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 API dut.signal.value PyGPI cocotb.simulator GPI Core libgpi VPI 库 libcocotbvpi_verilator Verilator
关键 :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. 属性访问 :Python 层通过 __setattr__ 拦截对 .value 的赋值。 2. 调用 PyGPI :Python 层调用 cocotb.simulator 模块中相应的 C 函数,如 set_signal_val_long 。 3. GPI 抽象 :PyGPI 将请求转发给 GPI 核心层。GPI 层通过虚函数调用到具体的 VpiImpl 类。 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 方向 标准 典型用途
谁主动 cocotb
cocotb 对 Verilator 的驱动走的是 VPI ,不是 DPI。
8. Verilator 编译选项 cocotb 调用 Verilator 时,会传入以下选项:
选项 作用 是否必需 --vpi 必需 --public-flat-rw 推荐 --prefix Vtop -lcocotbvpi_verilator 必需
--public-flat-rw 有 性能代价 ,会关闭模块内联优化。如 DUT 信号层次很深,可考虑 --public-depth N 替代。
8.1 RTL 源码要改吗 通常不需要修改 。 --public-flat-rw 已让所有信号对 VPI 可见。
情况
需要改吗 说明 普通信号 --public-flat-rw interface 信号 Verilator 不支持的语法 想控制可见性 用 /* verilator public */ 标记
第三部分:pyuvm 完整实战 9. 工具链版本要求 工具 最低版本 推荐版本 Verilator cocotb pyuvm Python
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 TOPLEVEL_LANG verilog VERILOG_SOURCES TOPLEVEL MODULE
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 语言 环境 事务 序列 驱动 监控 记分板 连接
配置 工厂 Phase 随机 覆盖 生态 性能 适用
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. 常见问题排查 重要提醒 :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 世界的桥梁 。