PyUVM 完整入门教程 —— 基于 TinyALU 的 UVM 验证平台
本教程从零开始,面向初学者,带你理解 UVM(Universal Verification Methodology) 的核心概念,并一步步搭建基于 PyUVM 和 Verilator 的仿真环境,实现一个简易的 TinyALU,编写完整的 Python Testbench 进行功能验证。
目录
- 什么是 UVM?
- 什么是 PyUVM?
- PyUVM 核心架构详解
- 项目概述
- 环境 Setup
- RTL 源码详解
- Makefile
- PyUVM Testbench 详解
- 运行仿真
- PyUVM 常见踩坑与教训
- 扩展建议
- 总结
1. 什么是 UVM?
UVM(Universal Verification Methodology,通用验证方法学) 是 IC 行业中最主流的 RTL 验证方法论。它由 IEEE 定义在 IEEE Standard for Universal Verification Methodology Language Reference Manual(即 IEEE 1800.2 标准)中。
1.1 为什么需要 UVM?
在传统的数字验证中,验证工程师需要为每个新设计从头编写 Testbench。这导致:
- 代码难以复用:每个项目的 Testbench 都是”一次性”的。
- 结构不统一:不同工程师写的 Testbench 风格迥异,难以维护。
- 协作困难:新人加入项目后,需要很长时间才能理解现有的 Testbench 架构。
UVM 通过提供一套标准化的验证组件和流程,解决了以上问题:
- 组件复用:Sequence、Driver、Monitor、Scoreboard 等组件可以在不同项目间复用。
- 统一结构:所有 UVM Testbench 都遵循相同的层次结构(Test → Env → Agent → Driver/Monitor)。
- 工厂机制:通过
uvm_factory可以在不修改源码的情况下替换组件类型。
1.2 UVM 的核心组件
| 组件 | 英文 | 职责 |
|---|---|---|
| 事务 | Sequence Item | 描述一次总线传输或操作的参数 |
| 序列 | Sequence | 生成一系列事务,交给 Sequencer |
| 序列器 | Sequencer | 仲裁器,连接 Sequence 和 Driver |
| 驱动器 | Driver | 从 Sequencer 获取事务,驱动到 DUT 接口 |
| 监视器 | Monitor | 被动监控 DUT 接口,收集数据 |
| 记分板 | Scoreboard | 对比预期结果和实际结果 |
| 环境 | Env | 容器,包含 Sequencer、Driver、Monitor 等 |
| 测试 | Test | 顶层容器,负责配置环境、启动 Sequence |
2. 什么是 PyUVM?
PyUVM 是 UVM 的 Python 实现,基于 IEEE 1800.2 标准。它构建在 cocotb 之上,使用 Python 的面向对象特性来实现 UVM 的核心功能。
虽然 IEEE 1800.2 标准使用 SystemVerilog 定义,但标准本身并不强制要求用 SystemVerilog 实现 UVM 库。任何具有足够面向对象支持的语言都可以实现它——例如 Python。PyUVM 正是利用 Python 的易用性和强大的 OOP 能力,实现了 IEEE 1800.2 中最常用的部分。
2.1 PyUVM vs SystemVerilog UVM
| 特性 | SystemVerilog UVM | PyUVM |
|---|---|---|
| 语言 | SystemVerilog | Python |
| 宏依赖 | 大量宏(uvm_component_utils 等) |
无宏,自动工厂注册 |
| 类型系统 | 严格静态类型,需参数化类 | Python 动态类型,更灵活 |
| 多继承 | 不支持 | 支持 |
| 相位参数 | phase 变量贯穿所有 phase |
已简化,仅保留常用 phase |
| 报告系统 | UVM 专用报告宏 | 集成 Python logging 模块 |
| 仿真交互 | 直接调用仿真器 | 通过 cocotb 调用仿真器 |
2.2 PyUVM 实现的 IEEE 1800.2 章节
| 章节 | 名称 | 说明 |
|---|---|---|
| 5 | Base Classes | uvm_void、uvm_object 等基础类 |
| 6 | Reporting Classes | 使用 Python logging 模块实现 |
| 8 | Factory Classes | 完整工厂功能,无需宏 |
| 9 | Phasing | 仅实现常用 phase(build/connect/run/check/report) |
| 12 | UVM TLM Interfaces | 完整的 TLM 系统 |
| 13 | 预定义组件 | uvm_component、uvm_root、ConfigDB 等 |
| 14 & 15 | Sequences | Sequence、Sequencer、Sequence Item(重构了 Sequencer 功能) |
3. PyUVM 核心架构详解
在深入代码之前,我们需要先理解 PyUVM 的各个核心概念。本节的内容完全基于 pyuvm 对 IEEE 1800.2 的实现。
3.1 导入 PyUVM
SystemVerilog UVM 中通常这样导入:
import uvm_pkg::*;
在 PyUVM 中,使用 from 导入语法可以达到类似效果——无需包路径前缀即可使用类名:
import pyuvm
from pyuvm import *
为什么要同时导入两种方式?
from pyuvm import *让你可以直接使用uvm_test、uvm_env等类名,无需写pyuvm.uvm_test。import pyuvm让你可以使用@pyuvm.test()装饰器,明确区分 PyUVM 测试和@cocotb.test()装饰器。
3.2 基础类(Base Classes)
PyUVM 的类层次从 uvm_void 开始,所有 UVM 类都继承自它:
uvm_void
├── uvm_object
│ ├── uvm_sequence_item
│ └── ...
└── uvm_component
├── uvm_test
├── uvm_env
├── uvm_driver
├── uvm_monitor
└── ...
- **
uvm_void**:最顶层的基类,没有任何数据成员。 - **
uvm_object**:所有非组件类(如 Sequence Item)的基类,提供get_name()、get_full_name()等方法。 - **
uvm_component**:所有组件类(如 Test、Env、Driver)的基类,提供层次化构建功能和 phase 支持。
3.3 工厂机制(Factory Classes)
SystemVerilog UVM 中,你必须使用 uvm_component_utils 宏来注册类到工厂:
class my_driver extends uvm_driver;
`uvm_component_utils(my_driver) // 必须写这个宏
PyUVM 完全不需要宏。所有继承自 uvm_void 的类都会自动注册到工厂。你可以直接:
# 自动注册,无需任何宏
class Driver(uvm_driver):
pass
# 使用工厂创建实例
driver = Driver.create("driver", self)
工厂还支持类型覆盖(Type Override),允许你在不修改源代码的情况下替换组件类型:
# 将 TestAllSeq 替换为 TestAllForkSeq
uvm_factory().set_type_override_by_type(TestAllSeq, TestAllForkSeq)
create() 是类方法:PyUVM 的 create() 是一个简单的类方法,不像 SystemVerilog 那样需要复杂的类型驱动调用。
3.4 相位系统(Phasing)
IEEE 1800.2 定义了两套 phase 系统:
- 常用 Phase(Common Phases):这是所有人都使用的,包括 build、connect、run、check 等。
- 自定义 Phase(Custom Phases):几乎没有人使用的复杂系统。
PyUVM 只实现了常用 Phase,但这已经覆盖了 99% 的使用场景。你可以通过 Python OOP 技术来扩展 phase。
常用 Phase 的执行顺序和特性如下:
| Phase | 遍历方向 | 说明 |
|---|---|---|
build_phase |
自顶向下(Top-down) | 父组件先创建子组件。用于实例化 Env、Driver、Monitor 等。 |
connect_phase |
自底向上(Bottom-up) | 子组件先连接。用于连接 TLM Port/Export(如 Driver 连接 Sequencer)。 |
end_of_elaboration_phase |
自顶向下 | 连接完成后,做一些最终配置(如创建 Sequence)。 |
start_of_simulation_phase |
自顶向下 | 仿真开始前,做一些初始化(如获取 BFM 句柄)。 |
run_phase |
自顶向下 | 核心运行阶段,启动 Sequence、驱动 DUT。是唯一的 async phase。 |
extract_phase |
自底向上 | 从各组件中提取数据。 |
check_phase |
自底向上 | 检查数据(如 Scoreboard 对比结果)。 |
report_phase |
自底向上 | 生成报告(如 Coverage 报告)。 |
final_phase |
自顶向下 | 仿真结束前的清理工作。 |
关键特性:
build_phase和connect_phase是最常用的两个 phase。run_phase是唯一需要async关键字的 phase,因为它内部会等待仿真事件。- PyUVM 的 phase 没有
phase参数变量。SystemVerilog UVM 中每个 phase 方法都带一个phase变量,但 PyUVM 已经将其简化掉了。
3.5 报告系统(Reporting Classes)
PyUVM 没有实现自己的报告系统,而是直接集成 Python 的 logging 模块。
每个 uvm_report_object 及其子类(包括所有 uvm_component)都有一个 self.logger 数据成员:
class Scoreboard(uvm_component):
def check_phase(self):
self.logger.info("这是 INFO 级别的日志")
self.logger.error("这是 ERROR 级别的日志")
self.logger.critical("这是 CRITICAL 级别的日志")
PyUVM 的日志输出格式与 cocotb 保持一致,包含时间戳、日志级别、文件名行号和组件层次路径:
49000.00ns INFO testbench.py(244) [uvm_test_top.env.scoreboard]: PASSED: ...
3.6 TLM 接口(Transaction Level Modeling)
PyUVM 完整实现了 UVM TLM 系统。TLM 是组件之间传递事务(Transaction)的标准机制。
在本教程中使用的 TLM 组件:
| TLM 类型 | 用途 | 代码示例 |
|---|---|---|
uvm_analysis_port |
发送端,向多个订阅者广播事务 | self.ap = uvm_analysis_port("ap", self) |
uvm_analysis_export |
接收端,连接 analysis_port | self.cmd_export = self.cmd_fifo.analysis_export |
uvm_tlm_analysis_fifo |
带 FIFO 的分析通道,缓存事务 | self.cmd_fifo = uvm_tlm_analysis_fifo("cmd_fifo", self) |
uvm_get_port |
从 FIFO 中读取事务 | self.cmd_get_port = uvm_get_port("cmd_get_port", self) |
seq_item_port / seq_item_export |
Sequence 与 Driver 之间的专用通道 | self.driver.seq_item_port.connect(self.seqr.seq_item_export) |
连接(connect())在 connect_phase 中完成:
def connect_phase(self):
self.driver.seq_item_port.connect(self.seqr.seq_item_export)
self.cmd_mon.ap.connect(self.scoreboard.cmd_export)
3.7 ConfigDB(配置数据库)
SystemVerilog UVM 中有两套配置数据库:uvm_config_db 和 uvm_resource_db。PyUVM 简化为单一的 ConfigDB() 单例,因为 Python 中没有 SystemVerilog 的类管理问题。
写入 ConfigDB:
# 第一个参数 context 通常为 None,表示全局设置
ConfigDB().set(None, "*", "SEQR", self.seqr)
读取 ConfigDB:
# 在 uvm_component 子类中,第一个参数传 self
self.dut = ConfigDB().get(self, "", "DUT")
# 在 uvm_sequence 中,第一个参数必须传 None(因为 Sequence 不是 uvm_component)
seqr = ConfigDB().get(None, "", "SEQR")
3.8 Objection 机制
Objection 机制控制 run_phase 的生命周期:
- **
raise_objection()**:告诉 UVM “我还在运行,不要结束仿真”。 - **
drop_objection()**:告诉 UVM “我完成了,可以结束 run_phase 了”。
当所有组件的 objection 都被 drop 后,run_phase 结束,UVM 进入 extract/check/report 等后续 phase。
async def run_phase(self):
self.raise_objection() # 开始运行,保持仿真存活
await self.test_all.start() # 执行测试主体
self.drop_objection() # 测试完成,允许仿真结束
3.9 Sequencer 与 Sequence 机制
PyUVM 重构了 Sequencer 功能,相比 SystemVerilog UVM 的实现更加简洁。
Sequence 的核心 API:
| 方法 | 是否阻塞 | 说明 |
|---|---|---|
start_item(item) |
是 | 请求使用 Sequencer,等待获得仲裁权 |
finish_item(item) |
是 | 发送事务给 Driver,等待 Driver 调用 item_done() |
get_response() |
是 | 获取 Driver 返回的响应(可选) |
Driver 的核心 API:
| 方法 | 是否阻塞 | 说明 |
|---|---|---|
get_next_item() |
是 | 从 Sequencer 队列取出一个事务 |
item_done(rsp) |
否 | 通知 Sequencer 事务已完成,可附带响应 |
Sequence / Driver / Sequencer 数据流:
- Sequence 调用
start_item()将事务放入 Sequencer 队列,并等待 Sequencer 就绪。 - Driver 调用
get_next_item()从队列取出事务。 - Sequencer 通知 Sequence “item 已就绪”,Sequence 调用
finish_item()。 - Driver 收到
finish_item信号后开始执行(驱动 DUT)。 - Driver 执行完成后调用
item_done(rsp)将结果返回给 Sequencer。 - Sequence 调用
get_response()获取结果(如果需要)。
3.10 Sequence Item 的 Python 化
SystemVerilog UVM 使用 convert2string() 和 do_compare() 来处理字符串转换和对象比较。PyUVM 利用 Python 的魔术方法(Magic Methods)替代了这些功能:
| SystemVerilog | Python 魔术方法 | 触发方式 |
|---|---|---|
convert2string() |
__str__() |
print(obj) 或 str(obj) |
do_compare() |
__eq__() |
obj1 == obj2 |
class AluSeqItem(uvm_sequence_item):
def __eq__(self, other):
same = self.A == other.A and self.B == other.B and self.op == other.op
return same
def __str__(self):
return f"{self.get_name()} : A: 0x{self.A:02x} OP: {self.op.name} B: 0x{self.B:02x}"
# 使用方式
item1 = AluSeqItem("item1", 1, 2, Ops.ADD)
item2 = AluSeqItem("item2", 1, 2, Ops.ADD)
print(item1 == item2) # True,因为 __eq__ 被重载
print(item1) # 自动调用 __str__
4. 项目概述
我们要验证的是一个 TinyALU(微型算术逻辑单元),它支持四种操作:
- ADD:加法
- AND:按位与
- XOR:按位异或
- MUL:乘法(三周期)
验证方面,我们使用:
- PyUVM 提供标准 UVM 验证架构。
- cocotb 作为仿真框架,与 Verilator 交互。
- Verilator 作为仿真器,编译并执行 Verilog 代码。
4.1 项目目录结构
pyuvm_tutorial/
├── tinyalu.sv # RTL 源码(TinyALU)
├── tinyalu_utils.py # BFM(Bus Functional Model)和工具函数
├── testbench.py # PyUVM Testbench
├── Makefile # Cocotb 编译规则
└── pyuvm_tutorial.md # 本教程
4.2 验证架构
Testbench (Python + PyUVM)
├── uvm_test AluTest / ParallelTest / FibonacciTest / AluTestErrors
│ └── uvm_env AluEnv
│ ├── uvm_sequencer
│ ├── Driver
│ ├── Monitor (cmd_mon)
│ ├── Coverage
│ └── Scoreboard
└── TinyAluBfm (cocotb 与 DUT 的桥梁)
Simulator (Verilator)
└── DUT (tinyalu)
5. 环境 Setup
5.1 系统要求
- Linux (Ubuntu / WSL 均可)
- Python 3.10+
- GCC / G++ (用于编译 Verilator 生成的 C++ 代码)
5.2 安装 Verilator
# 安装依赖
sudo apt update
sudo apt install -y git help2man perl python3 make autoconf g++ flex bison libfl2 libfl-dev zlib1g-dev
# 从源码编译安装 Verilator
git clone https://github.com/verilator/verilator
cd verilator
autoconf
./configure --prefix=/usr/local
make -j$(nproc)
sudo make install
# 验证安装
verilator --version
注意:确保安装的是 **Verilator 5.0+**,因为 Cocotb 2.x 需要较新的 VPI 支持。
5.3 安装 Python 依赖
推荐使用 Conda 或 venv 创建虚拟环境:
# 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate
# 安装核心包
pip install cocotb pyuvm
# 验证安装
python -c "import cocotb; print(cocotb.__version__)"
python -c "import pyuvm; print(pyuvm.__version__)"
6. RTL 源码详解
下面是完整的 TinyALU Verilog 代码。它包含三个模块:
tinyalu:顶层模块,根据op[2]选择单周期或三周期执行路径。single_cycle:单周期模块,实现 ADD、AND、XOR。three_cycle:三周期流水线模块,实现 MUL。
创建文件 tinyalu.sv:
module tinyalu (input [7:0] A,
input [7:0] B,
input [2:0] op,
input clk,
input reset_n,
input start,
output done,
output [15:0] result);
wire [15:0] result_aax, result_mult;
wire start_single, start_mult;
wire done_aax;
wire done_mult;
assign start_single = start & ~op[2];
assign start_mult = start & op[2];
single_cycle and_add_xor (.A, .B, .op, .clk, .reset_n, .start(start_single),
.done(done_aax), .result(result_aax));
three_cycle mult (.A, .B, .op, .clk, .reset_n, .start(start_mult),
.done(done_mult), .result(result_mult));
assign done = (op[2]) ? done_mult : done_aax;
assign result = (op[2]) ? result_mult : result_aax;
endmodule // tinyalu
module single_cycle(input [7:0] A,
input [7:0] B,
input [2:0] op,
input clk,
input reset_n,
input start,
output logic done,
output logic [15:0] result);
always @(posedge clk)
if (!reset_n)
result <= 0;
else
case(op)
3'b001 : result <= {8'd0,A} + {8'd0,B};
3'b010 : result <= {8'd0,A} & {8'd0,B};
3'b011 : result <= {8'd0,A} ^ {8'd0,B};
default : result <= {A,B};
endcase // case (op)
always @(posedge clk)
if (!reset_n)
done <= 0;
else
done <= ((start == 1'b1) && (op != 3'b000));
endmodule : single_cycle
module three_cycle(input [7:0] A,
input [7:0] B,
input [2:0] op,
input clk,
input reset_n,
input start,
output logic done,
output logic [15:0] result);
logic [7:0] a_int, b_int;
logic [15:0] mult1, mult2;
logic done1, done2, done3;
always @(posedge clk)
if (!reset_n) begin
done <= 0;
done3 <= 0;
done2 <= 0;
done1 <= 0;
a_int <= 0;
b_int <= 0;
mult1 <= 0;
mult2 <= 0;
result<= 0;
end else begin
a_int <= A;
b_int <= B;
mult1 <= a_int * b_int;
mult2 <= mult1;
result <= mult2;
done3 <= start & !done;
done2 <= done3 & !done;
done1 <= done2 & !done;
done <= done1 & !done;
end
endmodule : three_cycle
6.1 模块接口解析
| 信号 | 方向 | 位宽 | 说明 |
|---|---|---|---|
A |
Input | 8 bit | 操作数 A |
B |
Input | 8 bit | 操作数 B |
op |
Input | 3 bit | 操作码(001=ADD, 010=AND, 011=XOR, 100=MUL) |
clk |
Input | 1 bit | 时钟,上升沿触发 |
reset_n |
Input | 1 bit | 低电平有效的异步复位 |
start |
Input | 1 bit | 启动信号,高电平有效 |
done |
Output | 1 bit | 操作完成标志 |
result |
Output | 16 bit | 运算结果 |
6.2 操作码编码
| op[2:0] | 操作 | 周期数 |
|---|---|---|
3'b001 |
ADD | 1 |
3'b010 |
AND | 1 |
3'b011 |
XOR | 1 |
3'b100 |
MUL | 3 |
op[2]为 1 时选择three_cycle乘法器,否则选择single_cycle。
7. Makefile
Cocotb 使用 Makefile 驱动仿真流程。下面是针对 Verilator 的完整 Makefile。
创建文件 Makefile:
CWD=$(shell pwd)
export COCOTB_REDUCED_LOG_FMT = 1
SIM ?= verilator
TOPLEVEL_LANG ?= verilog
VERILOG_SOURCES =$(CWD)/tinyalu.sv
ifeq ($(SIM), verilator)
COMPILE_ARGS += --timing
endif
MODULE := testbench
TOPLEVEL = tinyalu
COCOTB_HDL_TIMEUNIT = 1us
COCOTB_HDL_TIMEPRECISION = 1us
include $(shell cocotb-config --makefiles)/Makefile.sim
7.1 关键配置说明
| 变量 | 说明 |
|---|---|
SIM ?= verilator |
指定仿真器为 Verilator |
TOPLEVEL |
DUT 顶层模块名,必须和 Verilog 的 module 名一致 |
MODULE |
Python Testbench 文件名(不含 .py) |
COMPILE_ARGS += --timing |
Verilator 专用:启用时序处理(支持 #delay) |
COCOTB_HDL_TIMEUNIT |
HDL 时间单位 |
COCOTB_HDL_TIMEPRECISION |
HDL 时间精度 |
8. PyUVM Testbench 详解
Testbench 分为两个 Python 文件:
tinyalu_utils.py:BFM(Bus Functional Model),负责通过 cocotb 直接驱动 DUT 信号。testbench.py:PyUVM 验证平台,包含 Sequence、Driver、Monitor、Coverage、Scoreboard、Env 和 Test。
8.1 BFM 文件:tinyalu_utils.py
BFM 是 Bus Functional Model 的缩写,它是 cocotb 与 DUT 之间的”桥梁”。在 UVM 架构中,只有 Driver 直接与 BFM 交互,其他组件(如 Monitor)通过 TLM 接口与 Driver 通信。
创建文件 tinyalu_utils.py:
import enum
import logging
import cocotb
from cocotb.queue import Queue, QueueEmpty
from cocotb.triggers import FallingEdge
from pyuvm import Singleton
logging.basicConfig(level=logging.NOTSET)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
@enum.unique
class Ops(enum.IntEnum):
"""Legal ops for the TinyALU"""
ADD = 1
AND = 2
XOR = 3
MUL = 4
def alu_prediction(A, B, op, error=False):
"""Python model of the TinyALU"""
assert isinstance(op, Ops), "The tinyalu op must be of type Ops"
if op == Ops.ADD:
result = A + B
elif op == Ops.AND:
result = A & B
elif op == Ops.XOR:
result = A ^ B
elif op == Ops.MUL:
result = A * B
if error:
result = result + 1
return result
def get_int(signal):
try:
sig = int(signal.value)
except ValueError:
sig = 0
return sig
class TinyAluBfm(metaclass=Singleton):
def __init__(self):
self.dut = cocotb.top
self.driver_queue = Queue(maxsize=1)
self.cmd_mon_queue = Queue(maxsize=0)
self.result_mon_queue = Queue(maxsize=0)
async def send_op(self, aa, bb, op):
command_tuple = (aa, bb, op)
await self.driver_queue.put(command_tuple)
async def get_cmd(self):
cmd = await self.cmd_mon_queue.get()
return cmd
async def get_result(self):
result = await self.result_mon_queue.get()
return result
async def reset(self):
await FallingEdge(self.dut.clk)
self.dut.reset_n.value = 0
self.dut.A.value = 0
self.dut.B.value = 0
self.dut.op.value = 0
await FallingEdge(self.dut.clk)
self.dut.reset_n.value = 1
await FallingEdge(self.dut.clk)
async def driver_bfm(self):
self.dut.start.value = 0
self.dut.A.value = 0
self.dut.B.value = 0
self.dut.op.value = 0
while True:
await FallingEdge(self.dut.clk)
start = get_int(self.dut.start)
done = get_int(self.dut.done)
if start == 0 and done == 0:
try:
(aa, bb, op) = self.driver_queue.get_nowait()
self.dut.A.value = aa
self.dut.B.value = bb
self.dut.op.value = op
self.dut.start.value = 1
except QueueEmpty:
pass
elif start == 1:
if done == 1:
self.dut.start.value = 0
async def cmd_mon_bfm(self):
prev_start = 0
while True:
await FallingEdge(self.dut.clk)
start = get_int(self.dut.start)
if start == 1 and prev_start == 0:
cmd_tuple = (
get_int(self.dut.A),
get_int(self.dut.B),
get_int(self.dut.op),
)
self.cmd_mon_queue.put_nowait(cmd_tuple)
prev_start = start
async def result_mon_bfm(self):
prev_done = 0
while True:
await FallingEdge(self.dut.clk)
done = get_int(self.dut.done)
if prev_done == 0 and done == 1:
result = get_int(self.dut.result)
self.result_mon_queue.put_nowait(result)
prev_done = done
def start_bfm(self):
cocotb.start_soon(self.driver_bfm())
cocotb.start_soon(self.cmd_mon_bfm())
cocotb.start_soon(self.result_mon_bfm())
TinyAluBfm 关键点
- Singleton 模式:使用
pyuvm.Singleton元类,确保整个仿真中只有一个 BFM 实例。 - 三个队列:
driver_queue:Driver 写入命令,BFM 读取并驱动到 DUT。cmd_mon_queue:BFM 监控start上升沿,将命令放入队列供 Monitor 读取。result_mon_queue:BFM 监控done上升沿,将结果放入队列供 Monitor 读取。
- **
reset()**:在时钟下降沿复位 DUT,释放复位后再等待一个下降沿。 - **
start_bfm()**:启动三个后台协程(driver、cmd_mon、result_mon),它们会永远运行。
8.2 Testbench 文件:testbench.py
创建文件 testbench.py:
import logging
import random
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import Combine
import pyuvm
from pyuvm import *
from tinyalu_utils import Ops, TinyAluBfm, alu_prediction
# ============================================================
# 1. Sequence Items(事务)
# ============================================================
class AluSeqItem(uvm_sequence_item):
"""
ALU 事务:描述一次 ALU 操作的参数
"""
def __init__(self, name, aa, bb, op):
super().__init__(name)
self.A = aa
self.B = bb
self.op = Ops(op)
def randomize_operands(self):
self.A = random.randint(0, 255)
self.B = random.randint(0, 255)
def randomize(self):
self.randomize_operands()
self.op = random.choice(list(Ops))
def __eq__(self, other):
same = self.A == other.A and self.B == other.B and self.op == other.op
return same
__hash__: None # type: ignore
def __str__(self):
return f"{self.get_name()} : A: 0x{self.A:02x} \
OP: {self.op.name} ({self.op.value}) B: 0x{self.B:02x}"
# ============================================================
# 2. Sequences(序列)
# ============================================================
class RandomSeq(uvm_sequence):
"""随机操作数序列:对每种操作生成随机操作数"""
async def body(self):
for op in list(Ops):
cmd_tr = AluSeqItem("cmd_tr", None, None, op)
await self.start_item(cmd_tr)
cmd_tr.randomize_operands()
await self.finish_item(cmd_tr)
class MaxSeq(uvm_sequence):
"""最大值序列:对每种操作使用 0xFF 作为操作数"""
async def body(self):
for op in list(Ops):
cmd_tr = AluSeqItem("cmd_tr", 0xFF, 0xFF, op)
await self.start_item(cmd_tr)
await self.finish_item(cmd_tr)
class TestAllSeq(uvm_sequence):
"""虚拟序列:顺序执行 RandomSeq 和 MaxSeq"""
async def body(self):
seqr = ConfigDB().get(None, "", "SEQR")
random = RandomSeq("random")
max = MaxSeq("max")
await random.start(seqr)
await max.start(seqr)
class TestAllForkSeq(uvm_sequence):
"""并行序列:同时启动 RandomSeq 和 MaxSeq"""
async def body(self):
seqr = ConfigDB().get(None, "", "SEQR")
random = RandomSeq("random")
max = MaxSeq("max")
random_task = cocotb.start_soon(random.start(seqr))
max_task = cocotb.start_soon(max.start(seqr))
await Combine(random_task, max_task)
class OpSeq(uvm_sequence):
"""单次操作序列:执行指定的一次 ALU 操作"""
def __init__(self, name, aa, bb, op):
super().__init__(name)
self.aa = aa
self.bb = bb
self.op = Ops(op)
async def body(self):
seq_item = AluSeqItem("seq_item", self.aa, self.bb, self.op)
await self.start_item(seq_item)
await self.finish_item(seq_item)
self.result = seq_item.result
async def do_add(seqr, aa, bb):
seq = OpSeq("seq", aa, bb, Ops.ADD)
await seq.start(seqr)
return seq.result
async def do_and(seqr, aa, bb):
seq = OpSeq("seq", aa, bb, Ops.AND)
await seq.start(seqr)
return seq.result
async def do_xor(seqr, aa, bb):
seq = OpSeq("seq", aa, bb, Ops.XOR)
await seq.start(seqr)
return seq.result
async def do_mul(seqr, aa, bb):
seq = OpSeq("seq", aa, bb, Ops.MUL)
await seq.start(seqr)
return seq.result
class FibonacciSeq(uvm_sequence):
"""斐波那契序列:使用 ALU 的 ADD 操作计算斐波那契数列"""
def __init__(self, name):
super().__init__(name)
self.seqr = ConfigDB().get(None, "", "SEQR")
async def body(self):
prev_num = 0
cur_num = 1
fib_list = [prev_num, cur_num]
for _ in range(7):
sum = await do_add(self.seqr, prev_num, cur_num)
fib_list.append(sum)
prev_num = cur_num
cur_num = sum
uvm_root().logger.info("Fibonacci Sequence: " + str(fib_list))
uvm_root().set_logging_level_hier(logging.CRITICAL)
# ============================================================
# 3. Driver(驱动器)
# ============================================================
class Driver(uvm_driver):
"""
ALU Driver:从 Sequencer 获取事务,通过 BFM 驱动到 DUT
"""
def build_phase(self):
self.ap = uvm_analysis_port("ap", self)
def start_of_simulation_phase(self):
self.bfm = TinyAluBfm()
async def launch_tb(self):
await self.bfm.reset()
self.bfm.start_bfm()
async def run_phase(self):
await self.launch_tb()
while True:
cmd = await self.seq_item_port.get_next_item()
await self.bfm.send_op(cmd.A, cmd.B, cmd.op)
result = await self.bfm.get_result()
self.ap.write(result)
cmd.result = result
self.seq_item_port.item_done()
# ============================================================
# 4. Coverage(覆盖率)
# ============================================================
class Coverage(uvm_subscriber):
"""
功能覆盖率检查:确保所有操作类型都被执行过
"""
def end_of_elaboration_phase(self):
self.cvg = set()
def write(self, cmd):
(_, _, op) = cmd
self.cvg.add(op)
def report_phase(self):
try:
disable_errors = ConfigDB().get(self, "", "DISABLE_COVERAGE_ERRORS")
except UVMConfigItemNotFound:
disable_errors = False
if not disable_errors:
if len(set(Ops) - self.cvg) > 0:
self.logger.error(
f"Functional coverage error. Missed: {set(Ops) - self.cvg}"
)
assert False
else:
self.logger.info("Covered all operations")
assert True
# ============================================================
# 5. Scoreboard(记分板)
# ============================================================
class Scoreboard(uvm_component):
"""
记分板:收集命令和结果,对比预测值与实际值
"""
def build_phase(self):
self.cmd_fifo = uvm_tlm_analysis_fifo("cmd_fifo", self)
self.result_fifo = uvm_tlm_analysis_fifo("result_fifo", self)
self.cmd_get_port = uvm_get_port("cmd_get_port", self)
self.result_get_port = uvm_get_port("result_get_port", self)
self.cmd_export = self.cmd_fifo.analysis_export
self.result_export = self.result_fifo.analysis_export
def connect_phase(self):
self.cmd_get_port.connect(self.cmd_fifo.get_export)
self.result_get_port.connect(self.result_fifo.get_export)
def check_phase(self):
passed = True
try:
self.errors = ConfigDB().get(self, "", "CREATE_ERRORS")
except UVMConfigItemNotFound:
self.errors = False
while self.result_get_port.can_get():
_, actual_result = self.result_get_port.try_get()
cmd_success, cmd = self.cmd_get_port.try_get()
if not cmd_success:
self.logger.critical(f"result {actual_result} had no command")
else:
(A, B, op_numb) = cmd
op = Ops(op_numb)
predicted_result = alu_prediction(A, B, op, self.errors)
if predicted_result == actual_result:
self.logger.info(
f"PASSED: 0x{A:02x} {op.name} 0x{B:02x} = 0x{actual_result:04x}"
)
else:
self.logger.error(
f"FAILED: 0x{A:02x} {op.name} 0x{B:02x} "
f"= 0x{actual_result:04x} "
f"expected 0x{predicted_result:04x}"
)
passed = False
assert passed
# ============================================================
# 6. Monitor(监视器)
# ============================================================
class Monitor(uvm_component):
"""
监视器:通过 BFM 监控 DUT 的命令或结果
"""
def __init__(self, name, parent, method_name):
super().__init__(name, parent)
self.method_name = method_name
def build_phase(self):
self.ap = uvm_analysis_port("ap", self)
self.bfm = TinyAluBfm()
self.get_method = getattr(self.bfm, self.method_name)
async def run_phase(self):
while True:
datum = await self.get_method()
self.logger.debug(f"MONITORED {datum}")
self.ap.write(datum)
# ============================================================
# 7. Environment(环境)
# ============================================================
class AluEnv(uvm_env):
"""
ALU 验证环境:包含 Sequencer、Driver、Monitor、Coverage、Scoreboard
"""
def build_phase(self):
self.clk_drv = Clock(cocotb.top.clk, 2, "us")
cocotb.start_soon(self.clk_drv.start())
self.seqr = uvm_sequencer("seqr", self)
ConfigDB().set(None, "*", "SEQR", self.seqr)
self.driver = Driver.create("driver", self)
self.cmd_mon = Monitor("cmd_mon", self, "get_cmd")
self.coverage = Coverage("coverage", self)
self.scoreboard = Scoreboard("scoreboard", self)
def connect_phase(self):
self.driver.seq_item_port.connect(self.seqr.seq_item_export)
self.cmd_mon.ap.connect(self.scoreboard.cmd_export)
self.cmd_mon.ap.connect(self.coverage.analysis_export)
self.driver.ap.connect(self.scoreboard.result_export)
# ============================================================
# 8. Tests(测试)
# ============================================================
@pyuvm.test()
class AluTest(uvm_test):
"""基础测试:顺序执行 RandomSeq 和 MaxSeq"""
def build_phase(self):
self.env = AluEnv("env", self)
def end_of_elaboration_phase(self):
self.test_all = TestAllSeq.create("test_all")
async def run_phase(self):
self.raise_objection()
await self.test_all.start()
self.drop_objection()
@pyuvm.test()
class ParallelTest(AluTest):
"""并行测试:同时启动 RandomSeq 和 MaxSeq"""
def build_phase(self):
uvm_factory().set_type_override_by_type(TestAllSeq, TestAllForkSeq)
super().build_phase()
@pyuvm.test()
class FibonacciTest(AluTest):
"""斐波那契测试:使用 ALU 计算斐波那契数列"""
def build_phase(self):
ConfigDB().set(None, "*", "DISABLE_COVERAGE_ERRORS", True)
uvm_factory().set_type_override_by_type(TestAllSeq, FibonacciSeq)
return super().build_phase()
@pyuvm.test(expect_fail=True)
class AluTestErrors(AluTest):
"""错误注入测试:在所有操作中注入错误,预期失败"""
def start_of_simulation_phase(self):
ConfigDB().set(None, "*", "CREATE_ERRORS", True)
8.3 Testbench 整体架构
Testbench (Python + PyUVM)
├── AluSeqItem Sequence Item(事务)
├── RandomSeq 随机操作数序列
├── MaxSeq 最大值序列
├── TestAllSeq 虚拟序列(顺序执行)
├── TestAllForkSeq 虚拟序列(并行执行)
├── FibonacciSeq 斐波那契序列
├── Driver 驱动器
├── Coverage 覆盖率检查
├── Scoreboard 记分板
├── Monitor 监视器
├── AluEnv 环境容器
└── AluTest 基础测试类
├── ParallelTest 并行测试
├── FibonacciTest 斐波那契测试
└── AluTestErrors 错误注入测试(预期失败)
8.4 PyUVM 核心概念速查
| 概念 | 代码示例 | 说明 |
|---|---|---|
| 导入 pyuvm | from pyuvm import * |
导入所有 UVM 类名,类似 SystemVerilog 的 import uvm_pkg::* |
| 定义测试 | @pyuvm.test() |
装饰器标记这是一个 pyuvm 测试用例 |
| 事务基类 | class AluSeqItem(uvm_sequence_item) |
定义 Sequence 与 Driver 之间传递的数据结构 |
| Sequence | class RandomSeq(uvm_sequence) |
生成事务序列,通过 start_item/finish_item 发送 |
| Driver | class Driver(uvm_driver) |
从 Sequencer 拉取事务,驱动到 DUT |
| 连接 Port | self.driver.seq_item_port.connect(...) |
在 connect_phase 中完成 TLM 连接 |
| ConfigDB 写 | ConfigDB().set(None, "*", "SEQR", self.seqr) |
全局设置共享资源 |
| ConfigDB 读 | ConfigDB().get(self, "", "SEQR") |
在组件中读取共享资源 |
| Objection | self.raise_objection() / self.drop_objection() |
控制 run_phase 生命周期 |
| 工厂覆盖 | uvm_factory().set_type_override_by_type(A, B) |
运行时替换组件类型 |
| 启动时钟 | cocotb.start_soon(Clock(...).start()) |
自动生成时钟 |
8.5 各组件详解
AluSeqItem — Sequence Item
AluSeqItem 继承 uvm_sequence_item,是 Sequence 与 Driver 之间传递的数据结构。
randomize_operands():随机化操作数 A 和 B。__eq__():重载==运算符,用于比较两个事务是否相等。__str__():重载字符串转换,用于打印调试信息。
PyUVM 没有
convert2string()和do_compare(),而是直接使用 Python 的__str__()和__eq__()魔术方法。
Driver — 驱动器
Driver 继承 uvm_driver,是 UVM 架构中直接操作 DUT 的组件。
build_phase:创建uvm_analysis_port,用于将结果广播给 Scoreboard。start_of_simulation_phase:获取 BFM 实例。run_phase(async):主循环,不断从 Sequencer 获取事务并驱动到 DUT。
run_phase是async方法,因为它内部需要awaitBFM 的协程。
Monitor — 监视器
Monitor 继承 uvm_component,被动监控 DUT 的行为。
- 通过构造函数传入
method_name(如"get_cmd"或"get_result"),使用 Python 的getattr()动态获取 BFM 方法。 run_phase中无限循环,等待 BFM 返回数据后写入analysis_port。
这种通过字符串名称动态获取方法的能力是 Python 内省(Introspection) 的特性,SystemVerilog 不支持。
Scoreboard — 记分板
Scoreboard 继承 uvm_component,在 check_phase 中对比预期结果和实际结果。
build_phase:创建两个uvm_tlm_analysis_fifo分别缓存命令和结果。connect_phase:将 FIFO 的 get_export 连接到 get_port。check_phase:遍历所有结果,调用alu_prediction()计算预期值,与实际值对比。
check_phase在run_phase结束后执行,此时所有数据都已收集完毕。
Coverage — 覆盖率
Coverage 继承 uvm_subscriber,被动接收 Monitor 发送的事务。
uvm_subscriber本身就是一个uvm_analysis_export,可以直接连接到analysis_port。write()方法必须被重写,否则会报运行时错误。report_phase:检查是否覆盖了所有操作类型(ADD、AND、XOR、MUL)。
AluEnv — 环境
AluEnv 继承 uvm_env,是所有验证组件的容器。
build_phase(自顶向下):先创建时钟,然后依次创建 Sequencer、Driver、Monitor、Coverage、Scoreboard。connect_phase(自底向上):连接 Driver ↔ Sequencer、Monitor ↔ Scoreboard、Monitor ↔ Coverage、Driver ↔ Scoreboard。
AluTest — 测试
AluTest 继承 uvm_test,是 UVM 验证树的根节点。
build_phase:创建AluEnv。end_of_elaboration_phase:创建要运行的 Sequence。run_phase:通过raise_objection()/drop_objection()控制仿真生命周期。
@pyuvm.test()装饰器会自动调用uvm_root().run_test()来启动 UVM 测试树。
8.6 测试用例详解
AluTest — 基础功能测试
顺序执行 RandomSeq(随机操作数)和 MaxSeq(0xFF 操作数),共 8 个事务,覆盖所有四种操作。
ParallelTest — 并行测试
通过工厂机制将 TestAllSeq 替换为 TestAllForkSeq,同时启动 RandomSeq 和 MaxSeq。验证 Sequencer 的仲裁能力。
FibonacciTest — 斐波那契测试
使用 ALU 的 ADD 操作计算斐波那契数列 [0, 1, 1, 2, 3, 5, 8, 13, 21]。展示了如何在 Sequence 中嵌套调用其他 Sequence。
AluTestErrors — 错误注入测试(预期失败)
通过 ConfigDB 设置 CREATE_ERRORS = True,使 alu_prediction() 返回错误结果(结果 +1)。Scoreboard 会检测到不匹配并报告 FAILED。此测试使用 @pyuvm.test(expect_fail=True) 标记为”预期失败”。
9. 运行仿真
确保你的终端位于项目目录下,且已激活安装了 cocotb 和 pyuvm 的 Python 环境。
9.1 编译与仿真
make clean
make SIM=verilator
如果一切正常,你会看到如下输出:
** TESTS=4 PASS=4 FAIL=0 SKIP=0 183000.00 0.03 6149956.59 **
中间的日志会显示每个操作的具体结果:
49000.00ns INFO [uvm_test_top.env.scoreboard]: PASSED: 0x35 ADD 0xa7 = 0x00dc
49000.00ns INFO [uvm_test_top.env.scoreboard]: PASSED: 0x21 AND 0x1f = 0x0001
49000.00ns INFO [uvm_test_top.env.scoreboard]: PASSED: 0x4b XOR 0x18 = 0x0053
49000.00ns INFO [uvm_test_top.env.scoreboard]: PASSED: 0x99 MUL 0x55 = 0x32cd
9.2 使用 Icarus Verilog 仿真
如果你更习惯使用 Icarus Verilog(iverilog),只需修改 SIM 变量:
make clean
make SIM=icarus
Icarus 不需要
--timing参数,因此无需额外配置。
10. PyUVM 常见踩坑与教训
以下是在编写和运行本教程过程中遇到的真实踩坑记录,强烈建议仔细阅读。
10.1 raise_objection() 不能传参数
现象:
TypeError: uvm_component.raise_objection() takes 1 positional argument but 2 were given
原因:PyUVM 4.0.1 中的 raise_objection() 和 drop_objection() 方法不接受描述字符串参数,而原始文档和 SystemVerilog UVM 中是可以传的。
解决:调用时不传任何参数:
# 正确
self.raise_objection()
self.drop_objection()
# 错误(pyuvm 4.0.1 不支持)
self.raise_objection("Keep simulation alive")
self.drop_objection("Simulation may end now")
教训:PyUVM 的 API 虽然与 SystemVerilog UVM 类似,但在细节上可能有差异。遇到
TypeError时,优先查看 PyUVM 源码确认方法签名。
10.2 Sequence 中不能用 self 作为 ConfigDB 的 context
现象:
AssertionError: config_db context must be None or a uvm_component
原因:ConfigDB().get(self, "", "DUT") 的第一个参数 self 在 uvm_sequence 中是 uvm_object,而不是 uvm_component。ConfigDB 的实现要求 context 必须是 None 或 uvm_component 实例。
解决:在 Sequence 中访问 ConfigDB 时,第一个参数传 None:
# 正确
seqr = ConfigDB().get(None, "", "SEQR")
# 错误(只在 uvm_component 子类中可用)
seqr = ConfigDB().get(self, "", "SEQR")
教训:
ConfigDB().get(None, "*", "KEY")是最安全的跨组件访问方式;带self的写法只适用于uvm_component子类(如 Driver、Env、Test)。
10.3 cocotb.start_soon() 与 UVM phasing 的交互
现象:uvm_run_phase 完成后,后台的 Clock 协程和 Driver 的 while True 循环仍在运行,但不会影响测试结束。
说明:PyUVM 的 uvm_run_phase 通过 ObjectionHandler 管理生命周期。当 Test 中所有 objection 都被 drop 后,run_phase_complete() 返回,UVM 继续执行后续 phase(extract/check/report/final),然后 run_test() 结束。后台残留的 cocotb.start_soon() 协程(如 Clock、Driver while True)虽然还在事件循环中,但 cocotb 的 regression manager 会正常结束当前测试并进入下一个。
教训:PyUVM 测试的正常结束不依赖于”杀死所有后台协程”,而依赖于 Objection 机制。只要确保
raise_objection()和drop_objection()成对出现即可。
11. 扩展建议
- 增加随机测试:使用 Python
random模块生成随机操作数和操作类型,配合更多的 Sequence 变体。 - 接入 CI/CD:将
make clean && make SIM=verilator写入 GitHub Actions 或 GitLab CI,实现自动化回归。 - 使用 UVM Agent:将 Sequencer + Driver + Monitor 封装成
uvm_agent,更符合工业标准架构。 - 加入功能覆盖率:使用
cocotb-coverage库收集更详细的覆盖率数据(如操作数边界值)。 - 添加波形调试:在 Makefile 中添加
--trace和--trace-vcd参数,生成 VCD 波形文件用 GTKWave 查看。
12. 总结
本项目完整展示了:
- UVM 的核心概念:为什么需要 UVM、UVM 的标准组件(Sequence/Driver/Monitor/Scoreboard/Env/Test)。
- PyUVM 的优势:基于 Python 的简洁语法、无宏工厂注册、与 cocotb 的无缝集成。
- PyUVM 对 IEEE 1800.2 的实现:Base Classes、Factory、Phasing、TLM、ConfigDB、Reporting、Sequences。
- 如何用 Verilog 实现一个简单的 ALU:组合逻辑 + 流水线乘法器的设计。
- 如何用 PyUVM + cocotb 编写标准 UVM 验证平台:Sequence、Driver、Sequencer、Env、Test 的经典架构,ConfigDB 与 Objection 的使用。
- 如何用 Verilator 进行编译仿真。
所有代码均已在本教程中完整给出,复制到对应文件即可直接运行。
结语
第三百八十三篇博文写完,开心!!!!
今天,也是充满希望的一天。