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 核心数据结构
1.3 VPI 常用函数
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 抽象统一。
关键: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 的区别
cocotb 对 Verilator 的驱动走的是 VPI,不是 DPI。
8. Verilator 编译选项
cocotb 调用 Verilator 时,会传入以下选项:
--public-flat-rw有 性能代价,会关闭模块内联优化。如 DUT 信号层次很深,可考虑--public-depth N替代。
8.1 RTL 源码要改吗
通常不需要修改。--public-flat-rw 已让所有信号对 VPI 可见。
第三部分:pyuvm 完整实战
9. 工具链版本要求
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 的仿真规则。
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 对比
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 世界的桥梁。

