来源:qingxuanxx/sv_uvm_axi_notes · Copyright (c) 2026 qingxuanxx。按 MIT 许可 引用;内容版本:
aa3213b。本站调整标题、栏目导航与阅读链接。
返回 UVM 学习笔记目录 · 上一章 · 下一章
第 2 章 · 验证平台搭建与测试用例
核心结论:完整的 UVM 平台把“产生激励、驱动信号、监测接口、预测结果和自动比较”拆成独立组件,再通过 transaction、TLM 端口和 phase 连接起来。sequence 决定发送什么,driver 决定怎样发送,monitor 负责把信号还原为 transaction,scoreboard 负责判断 DUT 是否正确。
记忆主线:driver -> factory -> objection -> virtual interface -> transaction -> env -> monitor -> agent -> reference model -> scoreboard -> field automation -> sequencer/sequence -> base_test -> 命令行启动 test。
2.0 本章定位
第 1 章回答“为什么需要 UVM”,第 2 章开始回答“怎样搭建 UVM 验证平台”。 教材没有直接给出最终平台,而是从一个只有 driver 的环境开始,逐步加入新机制。 这种增量方式非常重要,因为每次增加的组件都对应一个明确问题。
| 增量步骤 | 解决的问题 |
|---|---|
| driver | 如何给 DUT 施加激励 |
| factory | 如何注册、创建和替换 UVM 类型 |
| objection | 如何控制任务 phase 的结束 |
| virtual interface | 类如何访问 HDL 接口信号 |
| transaction | 如何用事务代替零散信号传递信息 |
| env | 把多个组件放入统一容器 |
| monitor | 如何从接口采集并恢复 transaction |
| agent | 如何封装一套协议相关组件 |
| reference model | 如何产生期望结果 |
| scoreboard | 如何自动比较期望值和实际值 |
| field automation | 如何减少 copy/compare/print/pack 重复代码 |
| sequence/sequencer | 如何把激励生成与信号驱动分离 |
| base_test/test | 如何组织并从命令行选择测试用例 |
本章最终目标:看懂一棵完整 UVM 树,并能解释树中每个结点为什么存在。
零基础阅读提示
本章代码最多,第一次阅读建议按数据流看,不要一开始就背所有宏:
- sequence 产生 transaction。
- sequencer 把 transaction 交给 driver。
- driver 把 transaction 变成接口信号。
- monitor 再把信号恢复成 transaction。
- model 产生期望值,scoreboard 完成比较。
看到 type_id::create() 时先读成“创建对象”;看到 connect() 时读成“接线”;看到 raise/drop_objection() 时读成“测试还没结束/现在可以结束”。
2.1 验证平台的组成(🔴 高)
2.1.1 验证平台的四项基本能力(🔴 高)
验证平台用于找出 DUT 中的 bug。 一个最基本的验证平台需要具备四项能力。
| 能力 | 对应组件 | 核心问题 |
|---|---|---|
| 产生并施加激励 | sequence、driver | DUT 要经历哪些输入场景 |
| 采集 DUT 行为 | monitor | DUT 实际接收和输出了什么 |
| 计算期望结果 | reference model | 正确结果应该是什么 |
| 自动判断结果 | scoreboard | 实际结果与期望结果是否一致 |
激励既要包含正常场景,也要包含异常场景。 例如:
- 正常长度的数据包。
- 最小包和最大包。
- 连续数据包。
- 总线空闲。
- 协议允许的异常组合。
- 复位前后事务。
scoreboard 比较时需要两个输入:
- reference model 产生的期望 transaction。
- 输出 monitor 采集到的实际 transaction。
2.1.2 简单平台的数据流(🔴 高)
flowchart LR S[激励源] --> D[driver] D -->|引脚信号| DUT[DUT] D -->|输入事务| M[reference model] M -->|期望事务| SCB[scoreboard] DUT -->|输出信号| MON[output monitor] MON -->|实际事务| SCB
这个框图说明两条并行路径。 DUT 路径 DUT 路径是真实硬件路径:transaction 先被 driver 翻译成引脚信号,DUT 处理后再由 output monitor 重新采样成 transaction。 这条路用来回答:“DUT 实际做出了什么结果?”
flowchart LR A[输入 transaction] --> B[driver] B --> C[DUT 输入信号] C --> D[DUT 处理] D --> E[DUT 输出信号] E --> F[output monitor] F --> G[实际 transaction]
期望路径 期望路径是软件预测路径:同一个输入 transaction 不经过 DUT,而是交给 reference model 计算正确答案。 这条路用来回答:“理论上应该得到什么结果?”
flowchart LR A[输入 transaction] --> B[reference model] B --> C[期望 transaction] C --> D[scoreboard]
两条路径最后在 scoreboard 汇合。
关键:reference model 不是用来“再实现一个 RTL”,而是用较高抽象层次描述正确功能。
2.1.3 典型 UVM 平台(🔴 高)
完整平台通常包含输入 agent 和输出 agent。
| 组件 | 输入端 agent | 输出端 agent |
|---|---|---|
| sequencer | 有,需要发送激励 | 没有,不需要生成输出 |
| driver | 有,需要驱动输入 | 没有,输出由DUT驱动 |
| monitor | 有,监控实际输入 | 有,监控实际输出 |
| 工作模式 | active | passive |
输入 agent 既产生驱动,也监测真正送到 DUT 的内容。输出 agent 只监测 DUT 输出,不应主动驱动输出端。 下面这张图把完整平台中的激励、实际结果和期望结果放在一起:
flowchart TD SEQ["sequence<br/>产生 transaction"] --> SQR["sequencer<br/>管理并发送 transaction"] SQR --> DRV["driver<br/>转换为引脚信号"] DRV --> DUT["DUT<br/>处理输入信号"] DRV -. "输入端实际信号" .-> IMON["input monitor<br/>采集真实输入"] IMON --> RM["reference model<br/>计算正确结果"] RM --> EXP["expected<br/>期望 transaction"] DUT --> OMON["output monitor<br/>采集 DUT 输出"] OMON --> ACT["actual<br/>实际 transaction"] EXP --> SCB["scoreboard<br/>比较期望值和实际值"] ACT --> SCB SCB --> RESULT{"比较结果"} RESULT -->|"一致"| PASS["PASS"] RESULT -->|"不一致"| FAIL["FAIL"]
读图时可以抓住两条主线:上方是“激励如何进入 DUT”,下方是“期望结果和实际结果如何产生”。 scoreboard 自己不产生数据,它只负责接收 expected 与 actual,然后判断 DUT 的行为是否正确。
2.2 只有 driver 的验证平台(🟡 中)
2.2.1 最简单的验证平台(🟡 中)
教材 DUT
本章使用一个非常简单的环回 DUT。 输入有效时,DUT 把 rxd 延迟后送到 txd,并把 rx_dv 送到 tx_en。
module dut (
input logic clk, // 时钟
input logic rst_n, // 复位,低有效
input logic [7:0] rxd, // 接收数据(输入)
input logic rx_dv, // 接收数据有效标志
output logic [7:0] txd, // 发送数据(输出)
output logic tx_en // 发送数据有效标志
);
always_ff @(posedge clk) begin
if (!rst_n) begin
txd <= '0; // 复位时清空输出数据
tx_en <= 1'b0; // 复位时输出无效
end
else begin
txd <= rxd; // 把输入数据转发到输出
tx_en <= rx_dv; // 把输入有效信号转发到输出
end
end
endmodule这个 DUT 功能简单,因此验证重点不在算法,而在 UVM 平台结构。
always_ff与always的区别:always_ff是 SystemVerilog 引入的专用时序逻辑块,只能写触发器/寄存器,写错了(比如用阻塞赋值==、加#时延)编译器会直接报错。功能上等价于always @(posedge clk),纯属代码规范层面的保护,让工具帮你在编译阶段就拦住低级错误。SV 还有两个对应的:always_comb(组合逻辑专用)和always_latch(锁存器专用)。
| 信号 | 方向 | 含义 |
|---|---|---|
clk | 输入 | 工作时钟 |
rst_n | 输入 | 低有效复位 |
rxd[7:0] | 输入 | 接收数据 |
rx_dv | 输入 | 接收数据有效 |
txd[7:0] | 输出 | 发送数据 |
tx_en | 输出 | 发送数据有效 |
driver 的职责 driver 是信号级激励的执行者。 它负责:
- 从上层取得 transaction。
- 按协议时序拆解 transaction。
- 驱动 virtual interface。
- 在一个 transaction 驱动完成后通知 sequencer。
driver 不应该长期承担:
- 决定测试场景。
- 随机化所有 transaction。
- 计算期望结果。
- 检查 DUT 输出。
本节前半为了逐步学习,暂时在 driver 内部产生随机数据;加入 sequence 后会移除这部分职责。
举例:假设要发一个 AXI 写请求,driver 本该只管”把信号按时序拍出去”,但本节开头为了简单,让 driver 自己随机生成了地址和数据:
// ❌ 学习阶段的临时写法:driver 自己决定发什么
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
req.addr = $urandom; // 随机化由 driver 做了(职责越界)
req.data = $urandom;
vif.cb.addr <= req.addr; // driver 真正该做的:驱动信号
vif.cb.data <= req.data;
@(vif.cb);
seq_item_port.item_done();
end
endtask加入 sequence 之后,随机化移到了 sequence 的 body() 里,driver 回归纯粹:
// ✅ 分工正确后:sequence 决定发什么,driver 只管怎么发
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req); // req 已经被 sequence 随机化好了
vif.cb.addr <= req.addr; // driver 只做信号驱动
vif.cb.data <= req.data;
@(vif.cb);
seq_item_port.item_done();
end
endtask最简单的 driver
UVM 中 driver 应派生自 uvm_driver。
class my_driver extends uvm_driver;
function new(
string name = "my_driver",
uvm_component parent = null
);
super.new(name, parent); // 把实例名和父结点交给 uvm_component
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass
task my_driver::main_phase(uvm_phase phase);
top_tb.rxd <= '0; // 先初始化 DUT 输入
top_tb.rx_dv <= 1'b0;
while (!top_tb.rst_n)
@(posedge top_tb.clk); // 等待复位释放
for (int i = 0; i < 256; i++) begin
@(posedge top_tb.clk);
top_tb.rxd <= $urandom_range(0, 255);
top_tb.rx_dv <= 1'b1;
`uvm_info("DRV", "one byte driven", UVM_LOW)
end
@(posedge top_tb.clk);
top_tb.rx_dv <= 1'b0; // 所有数据发送完成后撤销 valid
endtask这个例子还不是规范平台,主要用于认识 UVM component 的基本外形。
component 的 new 函数
所有派生自 uvm_component 的类,其构造函数通常有两个参数。
| 参数 | 含义 |
|---|---|
name | 当前实例在 UVM 树中的名字 |
parent | 当前实例的父 component |
标准写法:
function new(string name, uvm_component parent);
super.new(name, parent); // 必须先完成父类构造
endfunctionname 不是类名,而是实例名。 同一个类可以创建多个实例,每个实例应有自己的 name。 parent 决定组件挂到 UVM 树的哪个位置。
main_phase
UVM 使用 phase 管理组件生命周期。 main_phase 是任务 phase,可以消耗仿真时间。 driver 的主要运行行为通常放在 main_phase 或其他 run-time phase 中。 典型耗时语句包括:
@(posedge clk)。#10。- 阻塞式 TLM get。
- 等待 semaphore、mailbox 或 event。
普通函数调用、变量赋值和日志打印本身不推进仿真时间。
UVM 日志宏
教材使用 uvm_info 代替普通 $display。
`uvm_info("DRV", "begin to drive packet", UVM_LOW)三个参数分别是:
| 参数 | 示例 | 含义 |
|---|---|---|
| ID | "DRV" | 日志分类标签 |
| message | "begin..." | 具体信息 |
| verbosity | UVM_LOW | 冗余度级别 |
常见 verbosity:
| 级别 | 使用建议 |
|---|---|
UVM_LOW | 默认希望看到的重要过程 |
UVM_MEDIUM | 普通调试信息 |
UVM_HIGH | 更详细的过程信息 |
UVM 日志通常自动包含:
- severity。
- 源文件和行号。
- 仿真时间。
- component 层次路径。
- 自定义 ID。
- 消息正文。
获取当前 component 完整路径:
`uvm_info("PATH", get_full_name(), UVM_LOW)与普通打印相比,UVM 日志更容易过滤、统计和定位。
2.2.2 加入 factory 机制(🟡 中)
component 注册 要让 UVM factory 认识一个 component,需要注册类型。
class my_driver extends uvm_driver;
`uvm_component_utils(my_driver) // 把 my_driver 注册到 factory
function new(string name = "my_driver",
uvm_component parent = null);
super.new(name, parent);
endfunction
endclass注册后,UVM 可以:
- 根据字符串找到类型。
- 通过 type_id 创建实例。
- 在后续使用 type override 或 instance override。
规则:派生自
uvm_component的自定义 component 通常使用uvm_component_utils。
run_test
最初的示例手工 new driver,并显式调用 main_phase。 这并没有真正使用 UVM 的生命周期管理。 加入 factory 注册后,可以写:
initial begin
run_test("my_driver"); // 按注册类型名创建测试顶层
endrun_test 会启动 UVM phase 机制。 本章初期把 driver 临时作为树根,仅用于展示自动创建和 phase 调用。 无论传给 run_test 的类型是什么,UVM 创建的根实例名通常是: uvm_test_top 需要区分:
| 名称 | 示例 | 作用 |
|---|---|---|
| 类型名 | my_driver | factory 查找哪个类 |
| 实例名 | uvm_test_top | UVM 树中根结点的名字 |
为什么要使用 type_id::create
普通 SystemVerilog 创建: drv = new("drv", this); UVM component 推荐创建: drv = my_driver::type_id::create("drv", this); 二者都能创建对象,但 type_id::create 会经过 factory。 只有经过 factory 创建,类型替换机制才有机会生效。
工程规则:UVM 组件注册用 utils 宏,创建用
type_id::create。
2.2.3 加入 objection 机制(🟡 中)
为什么 main_phase 刚开始就结束
UVM 不会因为某个 component 的 main_phase 仍有后续代码,就自动一直等待。 如果当前任务 phase 中没有 objection,UVM 可以结束该 phase,并终止仍在其中运行的进程。 因此,driver 可能只打印“main_phase is called”,还没来得及等待时钟就被结束。
raise 与 drop
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this); // 声明:当前组件还有工作未完成
// 所有需要消耗仿真时间的激励都放在二者之间
repeat (256) begin
@(posedge vif.clk);
vif.data <= $urandom_range(0, 255);
vif.valid <= 1'b1;
end
@(posedge vif.clk);
vif.valid <= 1'b0;
phase.drop_objection(this); // 声明:当前组件的工作已完成
endtaskobjection 必须成对。
| 操作 | 含义 |
|---|---|
raise_objection | 阻止当前 phase 立即结束 |
drop_objection | 撤销自己的反对票 |
只有当所有 objection 都被撤销后,phase 才能结束。 raise 必须足够早 错误写法:
task my_driver::main_phase(uvm_phase phase);
@(posedge vif.clk); // 已经先消耗仿真时间
phase.raise_objection(this); // 可能根本来不及执行
endtask正确原则: 在本 phase 第一个耗时语句之前 raise objection。 日志打印可以放在 raise 前,因为它通常不推进仿真时间;但统一把 raise 放在开头更清楚。 objection 应由谁控制 本章早期把 objection 放在 driver 中是为了教学。 加入 sequence 后,更合理的做法是让控制测试时长的 sequence 或 test 管理 objection。 原因:
- driver 是常驻组件,通常有
while (1)。 - driver 不知道本次测试计划发送多少事务。
- sequence 最清楚激励何时开始、何时结束。
记忆:谁决定“测试内容已经发完”,谁更适合控制 objection。
sequenceDiagram participant Uvm as UVM phase 调度 participant Drv as driver participant Sq as sequence Uvm->>Drv: main_phase 开始 Sq->>Uvm: raise_objection 计数器=1,不能结束 Sq->>Drv: 发完 10 个事务 Sq->>Uvm: drop_objection 计数器=0,可以结束 Uvm->>Drv: 结束 main_phase
2.2.4 加入 virtual interface(🔴 高)
为什么不能写绝对层次路径
下面的写法把 driver 与 top_tb 层次绑定:
top_tb.rxd <= data;
@(posedge top_tb.clk);一旦 testbench 层次变化,driver 就要修改。 例如时钟从 top_tb.clk 变成 top_tb.clk_gen.clk,所有引用都可能失效。 宏只能缓解单一路径变化,不能真正消除信号与层次结构的耦合。
定义 interface
interface my_if(input logic clk, input logic rst_n);
logic [7:0] data; // 协议数据
logic valid; // 数据有效
endinterface在 top_tb 中创建输入和输出 interface。
my_if input_if (clk, rst_n);
my_if output_if(clk, rst_n);
dut u_dut (
.clk (clk),
.rst_n (rst_n),
.rxd (input_if.data),
.rx_dv (input_if.valid),
.txd (output_if.data),
.tx_en (output_if.valid)
);类中使用 virtual interface interface 实例属于静态 HDL 层次。 class 对象属于动态面向对象世界。 class 中不能像 module 一样直接例化并持有普通 interface 实例,需要声明 virtual interface 句柄。
class my_driver extends uvm_driver;
virtual my_if vif; // 指向 top_tb 中真实 interface 实例
endclass之后 driver 只通过 vif 操作信号。
@(posedge vif.clk);
vif.data <= req.data;
vif.valid <= 1'b1;这样 driver 不再知道 interface 在 HDL 层次中的绝对路径。
config_db 的 set
在 top_tb 中把 interface 放入 config_db。
initial begin
uvm_config_db#(virtual my_if)::set(
null, // top_tb 不是 component,没有 this
"uvm_test_top.env.i_agt.drv",
"vif", // 字段名,必须与 get 保持一致
input_if // 真正传递的 interface 实例
);
end可以把 set 理解为“寄信”。 四个参数用于回答:
- 从哪个上下文出发。
- 发给哪个 component 路径。
- 数据使用什么字段名。
- 数据值是什么。
config_db 的 get
driver 在 build_phase 中取出 interface。
function void my_driver::build_phase(uvm_phase phase);
super.build_phase(phase); // 保留父类必要行为
if (!uvm_config_db#(virtual my_if)::get(
this, // 从当前 driver 上下文查找
"", // 当前实例自身
"vif", // 必须与 set 的字段名相同
vif // 获取结果写入成员变量
)) begin
`uvm_fatal("NO_VIF", "virtual interface vif was not configured")
end
endfunctionget 失败后不能继续驱动,因此使用 uvm_fatal。
uvm_fatal
`uvm_fatal("NO_VIF", "virtual interface must be set")uvm_fatal 只有 ID 和 message 两个主要参数。 它表示平台存在无法恢复的问题,报告后会结束仿真。 适用场景:
- virtual interface 未配置。
- 关键配置对象为空。
- 必要连接不存在。
- 平台结构与预期不一致。
uvm_fatal 参数详解:
| 参数 | 含义 | 类比 |
|---|---|---|
| ID | 错误分类标签(随便起,但要能看懂) | 错误码 |
| message | 具体描述 | 报错内容 |
它和 $display、$error 最大的区别:
| 语句 | 行为 |
|---|---|
$display | 打印完继续跑 |
$error | 记一笔错误,继续跑 |
uvm_fatal | 打印完直接结束仿真 ❌ |
为什么 get 失败要用 fatal 而不是 error? 拿不到 interface 后驱动全部会崩——与其在信号驱动时冒出难懂的 X 状态错误,不如在 build 阶段就”验尸”:明确指出”vif 没配置,后面没法玩,停吧”。 错误要早爆、爆得清楚。
config_db 类型必须匹配
config_db 是参数化类。 传递 int: uvm_config_db#(int)::set(null, "uvm_test_top", "packet_num", 100); 获取 int:
int packet_num;
if (!uvm_config_db#(int)::get(this, "", "packet_num", packet_num))
`uvm_fatal("NO_CFG", "packet_num was not configured")set 和 get 需要同时匹配(三把钥匙必须同时对上):
| 匹配项 | set 写什么 | get 写什么 | 对不上会怎样 |
|---|---|---|---|
| ① 参数化类型 | #(int) | #(int) | get 返回失败(类型不同就是另一个柜子) |
| ② 路径 | "uvm_test_top" | 从 this 出发 | 收不到(除非 set 是 get 的祖先前缀) |
| ③ 字段名 | "packet_num" | "packet_num" | get 返回失败 |
任何一个对不上,get 返回 0,然后 uvm_fatal 就炸了——所以报错信息写清楚 "xxx was not configured",一眼定位是哪个配置没传对。
build_phase
build_phase 是函数 phase,不消耗仿真时间。 主要用途:
- 创建子 component。
- 读取配置。
- 根据配置决定平台结构。
- 准备端口和成员对象。
执行顺序: 树根 -> 中间结点 -> 树叶 即 top-down。 build_phase 通常在仿真时间 0 完成。
2.3 为验证平台加入各个组件(🔴 高)
2.3.1 加入 transaction(🔴 高)
transaction 的意义 真实协议通常以帧、包或命令为单位交换信息,而不是孤立的 bit 或 byte。 transaction 用一个对象表示一次有意义的数据交换。 以太网 transaction 可以包含:
- 目的 MAC 地址。
- 源 MAC 地址。
- 类型字段。
- payload。
- CRC。
transaction 让组件之间传递“包”,driver/monitor 才处理“线”。 transaction 定义
class my_transaction extends uvm_sequence_item;
rand bit [47:0] dmac; // 目的 MAC 地址
rand bit [47:0] smac; // 源 MAC 地址
rand bit [15:0] ether_type; // 以太网类型
rand byte pload[]; // 可变长 payload
rand bit [31:0] crc; // 校验值
constraint payload_c {
pload.size() inside {[46:1500]};
}
function bit [31:0] calc_crc();
return 32'h0; // 教材示例省略真实 CRC 算法
endfunction
function void post_randomize();
crc = calc_crc(); // 随机化后根据其他字段重新计算 CRC
endfunction
`uvm_object_utils(my_transaction)
function new(string name = "my_transaction");
super.new(name); // object 没有 parent 参数
endfunction
endclasscomponent 与 object 的区别
| 对比项 | uvm_component | uvm_object |
|---|---|---|
| 典型类型 | driver、monitor、env、agent | transaction、sequence |
| 生命周期 | 通常贯穿整个仿真 | 按需创建和销毁 |
| 是否进入 UVM 树 | 是 | 否 |
| new 参数 | name、parent | 通常只有 name |
| 注册宏 | uvm_component_utils | uvm_object_utils |
| 创建方式 | type_id::create(name, parent) | type_id::create(name) |
uvm_sequence_item 最终继承自 uvm_object。 因此 transaction 不是 UVM 树结点。
driver 把 transaction 串行化 driver 要把一个 transaction 转换为 interface 上的 byte 流。
task my_driver::drive_one_pkt(my_transaction tr);
byte unsigned data_q[$]; // 字节队列:串行化缓冲
// 按协议规定顺序把字段压入队列
for (int i = 0; i < 6; i++)
data_q.push_back(tr.dmac[8*i +: 8]); // 压入 dmac(目的MAC,6字节)
for (int i = 0; i < 6; i++)
data_q.push_back(tr.smac[8*i +: 8]); // 压入 smac(源MAC,6字节)
data_q.push_back(tr.ether_type[7:0]); // 压入 ether_type(低字节在前)
data_q.push_back(tr.ether_type[15:8]); // 压入 ether_type(高字节)
foreach (tr.pload[i])
data_q.push_back(tr.pload[i]); // 压入 pload(载荷,长度随机)
for (int i = 0; i < 4; i++)
data_q.push_back(tr.crc[8*i +: 8]); // 压入 crc(校验,4字节)
`uvm_info("DRV", "begin to drive one packet", UVM_LOW)
while (data_q.size() != 0) begin // 队列非空就继续发
@(posedge vif.clk); // 每个时钟沿发 1 字节
vif.valid <= 1'b1; // 拉高有效标志
vif.data <= data_q.pop_front(); // 弹出队头驱动到 data
end
@(posedge vif.clk); // 再等一拍
vif.valid <= 1'b0; // 拉低有效标志,发送结束
endtask这一过程叫 serialization 或 packing。 monitor 执行相反的 deserialization 或 unpacking。
2.3.2 加入 env(🔴 高)
为什么需要 env
run_test 只创建一个测试顶层。 完整平台却有 driver、monitor、model 和 scoreboard 等多个组件。 因此需要一个容器统一创建这些组件。 UVM 中这个容器通常派生自 uvm_env。
class my_env extends uvm_env; // env 容器:负责统一创建平台组件
my_driver drv; // 声明 driver 句柄
`uvm_component_utils(my_env) // 注册到 factory
function new(string name = "my_env",
uvm_component parent = null);
super.new(name, parent); // 调用父类构造函数
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase); // 先执行父类 build
drv = my_driver::type_id::create("drv", this); // 创建 driver 并挂到 env 树下
endfunction
endclassparent 建立树形关系
创建 driver 时: drv = my_driver::type_id::create("drv", this); 这里:
"drv"是 driver 实例名。this指向当前 env。- driver 的 parent 是 env。
路径因此变成: uvm_test_top.drv 加入 base_test 后会继续变为: uvm_test_top.env.i_agt.drv 路径中的每一段都来自 create 时传入的实例名。
路径变化必须同步配置
如果 driver 从根结点移动到 env 之下:
原路径:uvm_test_top
新路径:uvm_test_top.drv那么 config_db set 的目标路径也必须更新。 这是初学时最常见的 virtual interface 获取失败原因之一。
调试方法:使用
get_full_name()打印真实组件路径,不要凭记忆猜路径。
2.3.3 加入 monitor(🔴 高)
monitor 的职责 monitor 与 driver 方向相反。
| driver | monitor |
|---|---|
| transaction -> signal | signal -> transaction |
| 主动驱动接口 | 被动观察接口 |
| 了解发送时序 | 了解采样时序 |
| 不能用于输出端主动驱动 | 可用于输入端和输出端 |
monitor 应该是非侵入式的,不能改变 DUT 信号。 monitor 骨架
class my_monitor extends uvm_monitor; // monitor:观察 DUT 接口,不驱动信号
virtual my_if vif; // 接口句柄(从 config_db 获取)
uvm_analysis_port #(my_transaction) ap; // 分析端口:把采集到的包广播出去
`uvm_component_utils(my_monitor) // 注册到 factory
function new(string name = "my_monitor",
uvm_component parent = null);
super.new(name, parent); // 调用父类构造函数
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase); // 先执行父类 build
if (!uvm_config_db#(virtual my_if)::get(
this, "", "vif", vif)) // 从 config_db 取 interface
`uvm_fatal("NO_VIF", "monitor vif was not configured") // 取不到直接终止仿真
ap = new("ap", this); // analysis_port 必须先创建
endfunction
task main_phase(uvm_phase phase);
my_transaction tr;
while (1) begin // monitor 是常驻组件
tr = my_transaction::type_id::create("tr"); // 创建空包
collect_one_pkt(tr); // 从接口恢复出一个完整包
ap.write(tr); // 广播给后续订阅者
end
endtask
endclassmonitor 不需要自己 raise objection。 它可以一直运行,phase 结束时由 UVM 终止其循环。 为什么输入端也需要 monitor driver 已经拥有输入 transaction,看起来可以直接送给 reference model。 教材仍推荐使用 input monitor,原因包括:
- monitor 看到的是 DUT 引脚上真正发生的数据。
- driver 与 monitor 可由不同人员实现,形成交叉检查。
- 能发现 driver 串行化或接口时序错误。
- 更适合把模块级平台复用到系统级。
- reference model 不必依赖 driver 内部实现。
原则:预测路径最好使用“实际送到 DUT 的事务”,而不是“本来打算送的事务”。
2.3.4 封装成 agent(🔴 高)
agent 封装协议组件 driver、monitor 和 sequencer 都围绕同一接口协议工作。 UVM 通常把它们封装成 agent。 不同 agent 通常代表不同协议或接口实例。
class my_agent extends uvm_agent;
my_sequencer sqr;
my_driver drv;
my_monitor mon;
uvm_analysis_port #(my_transaction) ap;
`uvm_component_utils(my_agent)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclassactive 与 passive
uvm_agent 自带 is_active。 其类型是 uvm_active_passive_enum。
| 模式 | sequencer | driver | monitor | 用途 |
|---|---|---|---|---|
UVM_ACTIVE | 创建 | 创建 | 创建 | 主动产生和驱动流量 |
UVM_PASSIVE | 不创建 | 不创建 | 创建 | 只观察接口 |
function void my_agent::build_phase(uvm_phase phase);
super.build_phase(phase);
if (is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver ::type_id::create("drv", this);
end
mon = my_monitor::type_id::create("mon", this);
endfunctionmonitor 无论 active 还是 passive 都应创建。 env 中创建两个 agent
function void my_env::build_phase(uvm_phase phase);
super.build_phase(phase);
i_agt = my_agent::type_id::create("i_agt", this);
o_agt = my_agent::type_id::create("o_agt", this);
i_agt.is_active = UVM_ACTIVE; // 输入端需要产生并驱动流量
o_agt.is_active = UVM_PASSIVE; // 输出端只允许观察
endfunction更完整的项目常通过 config_db 配置 is_active,减少 env 对 agent 内部的直接控制。
component 必须在 build 阶段完成
UVM 要求 component 树最晚在 build 阶段完成。 错误做法:
task my_env::main_phase(uvm_phase phase);
// 错误:build_phase 已结束,再创建 component 会触发 fatal
i_agt = my_agent::type_id::create("i_agt", this);
endtask推荐:
function void my_env::build_phase(uvm_phase phase);
super.build_phase(phase);
i_agt = my_agent::type_id::create("i_agt", this);
endfunction虽然理论上可在 new 中创建子组件,但会让配置读取和条件构建变得困难。
约定:component 统一在
build_phase中创建。
2.3.5 加入 reference model(🔴 高)
reference model 的作用 reference model 根据输入 transaction 计算期望 transaction。 本章 DUT 只是转发数据,因此 model 只复制输入事务。 复杂项目中的 model 可能:
- 执行协议变换。
- 运行算法模型。
- 维护寄存器或状态机状态。
- 调用 C/C++ DPI 模型。
- 根据配置产生不同期望结果。
model 的端口
class my_model extends uvm_component;
uvm_blocking_get_port #(my_transaction) port; // 获取输入事务
uvm_analysis_port #(my_transaction) ap; // 广播期望事务
`uvm_component_utils(my_model)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
port = new("port", this); // 创建进货口:从 input monitor 收输入事务
ap = new("ap", this); // 创建出货口:向 scoreboard 广播期望事务
endfunction
task main_phase(uvm_phase phase);
my_transaction in_tr;
my_transaction exp_tr;
while (1) begin
port.get(in_tr); // 阻塞,直到收到输入 monitor 的事务
exp_tr = my_transaction::type_id::create("exp_tr");
exp_tr.copy(in_tr); // 本章 DUT 是环回,所以期望值等于输入
ap.write(exp_tr); // 把期望事务送给 scoreboard
end
endtask
endclass复制 transaction 很重要。 如果直接把同一个句柄传给多个组件,任一组件修改对象都会影响其他组件看到的内容。 Transaction 级通信 为什么使用 transaction 级通信 组件之间不应通过层次化变量访问交换数据。 UVM 使用 TLM 端口传递 transaction。 优点:
- 组件边界清晰。
- 发送端不需要了解接收端实现。
- 容易插入 FIFO、覆盖率收集器或多个订阅者。
- 平台更容易复用和扩展。
analysis_port
monitor 使用 analysis_port 广播事务。 uvm_analysis_port #(my_transaction) ap; 创建: ap = new("ap", this); 发送: ap.write(tr); // 非阻塞广播,不等待接收方处理完成 analysis_port 适合:
- monitor 发布采集事务。
- model 发布期望事务。
- 一份数据同时送多个订阅者。
类比:analysis_port 是”广播喇叭”(推):喊一声就走,不等接收方处理。 一个货可以同时发给多个订阅者(scoreboard、覆盖率收集器都接同一个喇叭)。
blocking_get_port
消费者可使用 blocking get 取得事务。 uvm_blocking_get_port #(my_transaction) port; 接收: port.get(tr); // 没有数据时阻塞等待 发送端 write 非阻塞,而接收端 get 可能阻塞,因此二者之间通常需要 FIFO。
类比:blocking_get_port 是”收货口”(拉):没货就一直等,拿到一个处理一个。
两种端口的对比:
| 对比项 | analysis_port | blocking_get_port |
|---|---|---|
| 动作 | ap.write(tr) | port.get(tr) |
| 方式 | 推(广播,不等) | 拉(阻塞,等货) |
| 接收方 | 可有多个(订阅) | 通常一个 |
| 谁在用 | monitor、model(发布方) | model、scoreboard(消费方) |
analysis FIFO
uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo; 在 env 的 build_phase 中创建: agt_mdl_fifo = new("agt_mdl_fifo", this); FIFO 提供:
analysis_export,连接 analysis_port。blocking_get_export,连接 blocking_get_port。- transaction 暂存空间。
FIFO 的两个口对应:
| FIFO 的端口 | 类型 | 接谁 |
|---|---|---|
analysis_export | 收口(接广播) | monitor 的 ap |
blocking_get_export | 出口(供取货) | model 的 port |
为什么中间需要 FIFO:发(write)是推、不等接收方;收(get)是拉、可能处理得慢。 直接相连时,收货方还没取,下一批货又来了 → 丢货。 FIFO 当缓冲仓库:发方把货堆进仓库就走,收方有空再来取,速度不匹配被消化。
sequenceDiagram participant Mon as input monitor participant Fifo as FIFO(缓冲仓库) participant Mdl as reference model Mon->>Fifo: ap.write(tr) 推(广播,不等接收方) Fifo-->>Mdl: port.get(tr) 拉(没货就阻塞等待)
connect_phase
连接在 connect_phase 中完成。
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
// 输入 monitor -> FIFO
i_agt.ap.connect(agt_mdl_fifo.analysis_export);
// FIFO -> reference model
mdl.port.connect(agt_mdl_fifo.blocking_get_export);
endfunctionconnect_phase 的典型执行顺序是 bottom-up。 树叶 -> 中间结点 -> 树根 这使子组件先准备好端口,父组件再连接它们。
完整链路:monitor
ap.write(推)→ FIFO 暂存 → modelport.get(拉)→ 处理 → modelap.write→ scoreboard。
agent 暴露 monitor 端口
env 不必深入访问 i_agt.mon.ap。 agent 可以对外暴露统一的 ap。
function void my_agent::connect_phase(uvm_phase phase);
super.connect_phase(phase);
ap = mon.ap; // agent.ap 指向 monitor.ap
endfunction由于 agent 的 connect_phase 先于 env 的 connect_phase 执行,env 连接时 i_agt.ap 已有效。
意义:封装。 env 只需连
i_agt.ap,不必深入i_agt.mon.ap。 以后 agent 内部结构变化,外部连接不用改。
2.3.6 加入 scoreboard(🔴 高)
scoreboard 的两个输入 scoreboard 需要:
exp_port:接收 reference model 的期望事务。act_port:接收 output monitor 的实际事务。
class my_scoreboard extends uvm_scoreboard;
my_transaction expect_queue[$];
uvm_blocking_get_port #(my_transaction) exp_port;
uvm_blocking_get_port #(my_transaction) act_port;
`uvm_component_utils(my_scoreboard)
function void build_phase(uvm_phase phase);
super.build_phase(phase);
exp_port = new("exp_port", this);
act_port = new("act_port", this);
endfunction
endclass并行接收与比较
task my_scoreboard::main_phase(uvm_phase phase);
my_transaction get_expect; // 只由期望事务进程写入
my_transaction get_actual; // 只由实际事务进程写入
my_transaction expected; // 从期望队列弹出的待比较事务
fork
// 进程一:持续收集期望事务
forever begin
exp_port.get(get_expect);
expect_queue.push_back(get_expect);
end
// 进程二:持续收集 DUT 实际输出并比较
forever begin
act_port.get(get_actual);
if (expect_queue.size() == 0) begin
`uvm_error("SCB", "actual transaction arrived before expected")
end
else begin
expected = expect_queue.pop_front();
if (get_actual.compare(expected))
`uvm_info("SCB", "compare successfully", UVM_LOW)
else begin
`uvm_error("SCB", "compare failed")
expected.print(); // 打印期望值便于定位
get_actual.print(); // 打印实际值便于定位
end
end
end
join
endtask本章方案假设 reference model 比 DUT 更早产生结果。 因此期望事务先进入队列,实际事务到来后再取出比较。 真实项目还要考虑:
- DUT 乱序输出。
- transaction ID 匹配。
- 多通道数据。
- 丢包和重复包。
- 延迟超时。
- 仿真结束时队列是否清空。
uvm_error
`uvm_error("SCB", "compare failed")uvm_error 表示检查失败,但通常不会立即结束仿真。 这允许一次回归收集多个错误。
| 宏 | 用途 | 一般行为 |
|---|---|---|
uvm_info | 普通信息 | 受 verbosity 控制 |
uvm_warning | 可疑但可继续 | 继续仿真 |
uvm_error | 明确错误 | 计数并继续 |
uvm_fatal | 无法继续的平台错误 | 结束仿真 |
2.3.7 加入 field automation(🟡 中)
为什么需要字段自动化 没有自动化时,每个 transaction 都要手写:
- print。
- copy。
- compare。
- pack。
- unpack。
字段增加后,这些函数必须同步修改,容易遗漏。 UVM field 宏可以根据字段注册信息提供默认自动化操作。 注册字段
class my_transaction extends uvm_sequence_item;
rand bit [47:0] dmac; // 目的 MAC 地址
rand bit [47:0] smac; // 源 MAC 地址
rand bit [15:0] ether_type; // 以太网类型
rand byte pload[]; // 可变长 payload(数组)
rand bit [31:0] crc; // 校验值
`uvm_object_utils_begin(my_transaction) // 开启字段注册
`uvm_field_int (dmac, UVM_ALL_ON) // 登记整型字段(bit/logic/int 都用它)
`uvm_field_int (smac, UVM_ALL_ON)
`uvm_field_int (ether_type, UVM_ALL_ON)
`uvm_field_array_int(pload, UVM_ALL_ON) // 数组字段用 array 版(长度可变,处理方式不同)
`uvm_field_int (crc, UVM_ALL_ON)
`uvm_object_utils_end // 结束注册
function new(string name = "my_transaction");
super.new(name);
endfunction
endclass使用 begin/end 版本时,中间列出所有需要参与自动操作的字段。 字段类型不同,使用的宏也不同。
UVM_ALL_ON 的含义:标志位,表示该字段的所有自动操作都启用(compare/copy/print/pack 全要)。 字段类型与宏的对应:单值整型 →
uvm_field_int;数组 →uvm_field_array_int;字符串 →uvm_field_string;对象字段 →uvm_field_object。 自动方法 注册后可直接使用:
dst.copy(src); // 复制已注册字段
same = actual.compare(expected); // 比较已注册字段
tr.print(); // 打印已注册字段还可以 pack 成 byte 数组:
byte unsigned data_array[];
int bit_count;
bit_count = tr.pack_bytes(data_array);monitor 可反向 unpack:
tr.pload = new[payload_size]; // 动态数组必须先分配正确大小
bit_count = tr.unpack_bytes(data_array);字段顺序影响 pack 顺序
pack_bytes 按 field 宏注册顺序打包字段。 例如:
`uvm_field_int(dmac, UVM_ALL_ON)
`uvm_field_int(smac, UVM_ALL_ON)打包时先 dmac,再 smac。 如果交换宏顺序,byte 流顺序也可能改变。
警告:field automation 能减少代码,但协议线序仍必须认真确认。
举例:dmac = 48’hAABBCCDDEEFF,smac = 48’h112233445566,ether_type = 16’h0800,按上面注册顺序 pack:
data_array[0..5] = AA BB CC DD EE FF ← dmac(先注册,先打包)
data_array[6..11] = 11 22 33 44 55 66 ← smac
data_array[12..13]= 08 00 ← ether_type如果交换宏顺序(smac 在前),字节流就变成 11 22 ... 66 AA BB ... FF——同一台机器收发都用同一顺序自测没问题,但协议规定”先 dmac 后 smac”时,线上传输就会错乱。 unpack 按同一注册顺序反向还原,动态数组字段 unpack 前必须先 new[...] 分配大小。
field automation 的取舍
优点:
- 快速搭建平台。
- 减少机械性代码。
- 新字段容易加入 print/compare/copy。
需要注意:
- 宏展开可能增加编译和运行开销。
- 默认 compare 不一定符合协议语义。
- pack 顺序和大小端必须核对。
- 深拷贝需求复杂时应检查默认 copy 行为。
- 大型项目可能选择手写关键方法。
本章重点是理解自动化机制,不是要求所有项目一律使用。
2.4 UVM 的终极大作:sequence(🔴 高)
2.4.1 在验证平台中加入 sequencer(🔴 高)
为什么把激励生成移出 driver 规范平台中:
- sequence 决定产生什么 transaction。
- sequencer 负责仲裁和转发。
- driver 只负责把 transaction 驱动到接口。
如果 driver 同时随机化和驱动:
- 不同测试必须修改 driver。
- 协议时序和测试场景耦合。
- driver 难以复用。
- 多个 sequence 无法仲裁。
sequencer 定义
class my_sequencer extends uvm_sequencer #(my_transaction);
`uvm_component_utils(my_sequencer)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclasssequencer 是 parameterized component。 参数 my_transaction 表示它传递哪种 sequence item。
driver 也要参数化
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver)
// uvm_driver 中的 req 因参数化而成为 my_transaction 类型
endclass参数化后,driver 内建的 req 句柄就是 my_transaction 类型。
2.4.2 sequence 机制(🔴 高)
sequence 定义
class my_sequence extends uvm_sequence #(my_transaction);
my_transaction m_trans;
`uvm_object_utils(my_sequence)
function new(string name = "my_sequence");
super.new(name);
endfunction
virtual task body();
repeat (10) begin
`uvm_do(m_trans) // 创建、随机化并发送 transaction
end
endtask
endclasssequence 是 object,不是 component。 因此:
- 不进入 UVM 树。
- 使用
uvm_object_utils。 - 有自己的生命周期。
- 启动后执行
body。
uvm_do 做了什么
初学阶段可把 uvm_do(m_trans) 理解为:
- 创建
m_trans。 - 等待 sequencer 授权。
- 随机化 transaction。
- 把 transaction 交给 sequencer。
- 等待 driver 完成该 transaction。
后续章节会学习 start_item 和 finish_item 的展开写法。 带临时约束:
`uvm_do_with(m_trans, {
m_trans.pload.size() == 60;
})连接 driver 与 sequencer active agent 在 build_phase 中同时创建二者。
if (is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver ::type_id::create("drv", this);
end在 connect_phase 中连接:
if (is_active == UVM_ACTIVE) begin
drv.seq_item_port.connect(sqr.seq_item_export);
endpassive agent 没有 driver 和 sequencer,因此连接前必须判断 is_active。
driver 获取 item
task my_driver::main_phase(uvm_phase phase);
vif.data <= '0;
vif.valid <= 1'b0;
while (!vif.rst_n)
@(posedge vif.clk);
forever begin
seq_item_port.get_next_item(req); // 阻塞等待下一个事务
drive_one_pkt(req); // 按接口协议完成驱动
seq_item_port.item_done(); // 确认该事务已经处理完成
end
endtaskget_next_item 与 item_done 必须正确配对。 如果漏掉 item_done:
- sequence 可能一直等待。
- 下一次
uvm_do不会继续。 - 仿真表现为“卡住”。
sequence-driver 握手
uvm_do 内部的完整握手分两段:先”请求-授权”(start_item),再”交付-完成”(finish_item)。
sequenceDiagram participant Seq as sequence participant Sqr as sequencer participant Drv as driver Note over Seq,Sqr: 第一段:请求与授权(start_item) Seq->>Sqr: 请求发送 item Drv->>Sqr: get_next_item(driver 来取货) Sqr-->>Seq: 授权(grant) Note over Seq,Sqr: 第二段:交付与完成(finish_item) Seq->>Sqr: 随机化后交付 item Sqr-->>Drv: 转发 req Drv->>Drv: 驱动接口信号(drive) Drv->>Sqr: item_done(这单做完了) Sqr-->>Seq: uvm_do 返回
uvm_do 通常要等 driver 调用 item_done 后才完成。 这保证 sequence 产生事务的节奏与 driver 实际处理能力同步。
get_next_item 与 try_next_item
| 方法 | 类型 | 没有 transaction 时 |
|---|---|---|
get_next_item | 阻塞 | 一直等待 |
try_next_item | 非阻塞 | 立即返回,req 可能为 null |
forever begin
seq_item_port.try_next_item(req);
if (req == null) begin
@(posedge vif.clk); // 没有数据时保持总线空闲
end
else begin
drive_one_pkt(req);
seq_item_port.item_done();
end
end启动 sequence 手工 start 可以在 env 的 main_phase 中创建并启动 sequence。
task my_env::main_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this); // 举手:我要开始干活(env 知道何时发完)
seq = my_sequence::type_id::create("seq"); // 创建 sequence(无 parent!它是 object 不上树)
seq.start(i_agt.sqr); // 启动,指定目标 sequencer
phase.drop_objection(this); // 干完收工
endtaskstart 的参数告诉 sequence 把 transaction 发给哪个 sequencer。
2.4.3 default_sequence 的使用(🔴 高)
本书使用 config_db 设置 sequencer 某个 phase 的 default sequence。 default_sequence 是”预约剧本”:提前用 config_db 告诉 sequencer”你的 main_phase 一到,自动跑这个 sequence”,不用手工 start。
uvm_config_db#(uvm_object_wrapper)::set(
this, // (1)谁放的(env 自己)
"i_agt.sqr.main_phase", // (2)发给谁(路径:sequencer + phase 名!)
"default_sequence", // (3)字段名:固定写法,表示"设默认序列"
my_sequence::type_id::get() // (4)值:sequence 的类型句柄(不是实例!)
);路径中必须包含目标 phase: i_agt.sqr.main_phase 原因是 sequencer 可在不同任务 phase 启动不同 default sequence。
为什么路径带 phase 名:同一个 sequencer 可在不同 phase 启动不同序列——
i_agt.sqr.main_phase挂主测试序列、i_agt.sqr.reset_phase挂复位序列。 相对路径与绝对路径 在 env 内 set:
uvm_config_db#(uvm_object_wrapper)::set(
this,
"i_agt.sqr.main_phase",
"default_sequence",
my_sequence::type_id::get()
);第二个参数是相对 env 的路径。 在 top_tb 内 set:
uvm_config_db#(uvm_object_wrapper)::set(
null,
"uvm_test_top.env.i_agt.sqr.main_phase",
"default_sequence",
my_sequence::type_id::get()
);因为 top_tb 不是 component,使用 null 和完整路径。
相对 vs 绝对路径:本质是”从谁的视角写地址”
| 在哪 set | 第二参数 | 为什么 |
|---|---|---|
env 内(component,有 this) | "i_agt.sqr.main_phase" | 相对路径:从 env 自己的位置往下找 |
top_tb 内(module,无 this) | "uvm_test_top.env.i_agt.sqr.main_phase" | 完整路径:从树根一路指下去 |
核心规则:
- 有
this的组件(env/test)→ 用相对路径(相对自己) - 没有
this的模块(top_tb)→ 用null+ 从uvm_test_top开始的完整路径 default_sequence 的 objection 教材 UVM 1.1d 示例使用starting_phase。
virtual task body();
if (starting_phase != null)
starting_phase.raise_objection(this);
repeat (10)
`uvm_do(m_trans)
#1000; // 给 DUT 和 scoreboard 留出处理时间
if (starting_phase != null)
starting_phase.drop_objection(this);
endtask先判断 null,是因为 sequence 也可能通过手工 start 启动,此时 starting_phase 不一定有效。
版本提醒:教材基于 UVM 1.1d;其他 UVM 版本对 default sequence 和 starting_phase 的推荐用法可能不同。
2.5 建造测试用例(🔴 高)
2.5.1 加入 base_test(🔴 高)
为什么 test 才是树根
教学过程中的树根依次是 driver、env。 实际平台通常让 uvm_test 派生类作为根类型。 base_test 负责:
- 创建 env。
- 设置公共配置。
- 设置默认 sequence 或测试基础策略。
- 配置超时。
- 在 report_phase 汇总测试结果。
base_test 示例 base_test 是所有测试的公共基类:固定平台结构(建 env)+ 挂默认序列 + 结束时汇报结果。 具体测试继承它,只换序列。
class base_test extends uvm_test; // test 基类(组件)
my_env env; // 持有 env 句柄(平台容器)
`uvm_component_utils(base_test) // 注册进 factory
function new(string name = "base_test",
uvm_component parent = null);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this); // 创建 env(机箱),挂到自己树下
uvm_config_db#(uvm_object_wrapper)::set( // 给 sequencer 挂默认序列
this, // test 自己放
"env.i_agt.sqr.main_phase", // 路径:env 下 sequencer 的 main_phase
"default_sequence", // 固定字段名
my_sequence::type_id::get() // 挂哪个序列(类句柄)
);
endfunction
function void report_phase(uvm_phase phase); // 仿真结束时汇报
uvm_report_server server;
int error_num;
super.report_phase(phase);
server = get_report_server(); // 拿到 UVM 的"成绩单服务器"
error_num = server.get_severity_count(UVM_ERROR); // 数整个仿真的 ERROR 数
if (error_num == 0)
$display("TEST CASE PASSED"); // 0 错误 = 通过
else
$display("TEST CASE FAILED"); // 有错误 = 失败
endfunction
endclass为什么用 ERROR 数判断结果:scoreboard 对比失败、断言失败都会报
uvm_error——ERROR 总数 == 0 意味着所有检查都通过。 report_phasereport_phase在运行 phase 结束后执行。 适合:
- 统计 UVM_ERROR 数量。
- 输出 PASS/FAIL 标志。
- 检查 scoreboard 未匹配队列。
- 输出覆盖率或事务统计。
- 给日志分析脚本提供统一结果。
注意: 只检查 UVM_ERROR 数量可能不够,项目还应考虑 UVM_FATAL、scoreboard 残留和协议超时。 加入 base_test 后的 UVM 树
uvm_test_top : my_caseN / base_test ← 树根(run_test 创建,动态替换 test)
└── env : my_env ← 机箱:平台容器
├── i_agt : my_agent [ACTIVE] ← 输入 agent:主动发激励
│ ├── sqr : my_sequencer ← 调度台
│ ├── drv : my_driver ← 发信号
│ └── mon : my_monitor ← 采输入(active 也带 monitor!)
├── mdl : my_model ← 参考模型:算期望
├── scb : my_scoreboard ← 计分板:对比
└── o_agt : my_agent [PASSIVE] ← 输出 agent:只看
└── mon : my_monitor ← 采输出(passive 只有 monitor)transaction、sequence 和 FIFO 中暂存的对象不是这棵 component 树的普通结点。
2.5.2 UVM 中测试用例的启动(🔴 高)
测试用例不应修改公共平台 随着验证推进,test 数量会不断增加。 新增 test 不应破坏已存在 test。 推荐结构:
uvm_test
|
base_test
|-- my_case0
|-- my_case1
|-- my_case2
...公共平台放在 base_test 和 env。 每个具体 test 只改变本场景需要的 sequence 和配置。 case0
class case0_sequence extends uvm_sequence #(my_transaction); // 测试0 的剧本(object)
my_transaction m_trans;
`uvm_object_utils(case0_sequence) // object 注册宏(sequence 不上树)
function new(string name = "case0_sequence");
super.new(name);
endfunction
task body(); // 剧本正文:phase 到时自动执行
if (starting_phase != null)
starting_phase.raise_objection(this); // 举手:我还没发完
repeat (10)
`uvm_do(m_trans) // 使用 transaction 原有约束(发 10 个默认约束的包)
if (starting_phase != null)
starting_phase.drop_objection(this); // 收工
endtask
endclass
class my_case0 extends base_test; // 具体测试:继承 base_test(平台已搭好)
`uvm_component_utils(my_case0) // 注册进 factory(+UVM_TEST_NAME 靠它查找)
function new(string name = "my_case0",
uvm_component parent = null);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase); // 先创建 base_test 中的 env(调用父类 build!)
uvm_config_db#(uvm_object_wrapper)::set( // 只做一件事:换默认序列
this,
"env.i_agt.sqr.main_phase", // 还是那个 sequencer 的 main_phase
"default_sequence",
case0_sequence::type_id::get() // 换成 case0 的序列
);
endfunction
endclassbase_test vs 具体 test 的分工:base_test 建平台 + 挂默认序列;my_case0 只覆盖”挂哪个序列”——平台不动,换剧本即可。 case1 与 uvm_do_with
class case1_sequence extends uvm_sequence #(my_transaction); // 测试1 的剧本
my_transaction m_trans;
`uvm_object_utils(case1_sequence) // object 注册宏
task body();
if (starting_phase != null)
starting_phase.raise_objection(this); // 举手
repeat (10) begin
`uvm_do_with(m_trans, { // 带临时约束发包
m_trans.pload.size() == 60; // 本 test 固定 payload 长度(只这次生效)
})
end
if (starting_phase != null)
starting_phase.drop_objection(this); // 收工
endtask
endclassuvm_do_with 在 transaction 原约束基础上加入本次随机化的临时约束。 它适合把“测试场景要求”放在 sequence 中,而不是修改 transaction 的通用合法约束。
命令行选择 test
top_tb 只保留:
initial begin
run_test(); // 不在源码中写死具体 test 类型(空参 = 从命令行读)
end运行 case0: <sim_command> ... +UVM_TEST_NAME=my_case0 运行 case1: <sim_command> ... +UVM_TEST_NAME=my_case1 这样切换 test 不需要修改和重新编译顶层源码。 前提是目标 test 已:
- 被编译。
- 使用 factory 宏注册。
- 名字拼写正确。
原理:
run_test()空参时读取命令行+UVM_TEST_NAME=xxx的字符串 → 去 factory 登记簿查找 → 创建对应 test。 所以切换用例 = 改命令行参数,不改代码、不重新编译。
一次 test 的完整执行链 从命令行启动到比较结果,可以按下面的顺序理解:
+UVM_TEST_NAME=my_case0
|
v
run_test() 通过 factory 创建 uvm_test_top
|
v
my_case0.build_phase
|
+--> base_test.build_phase 创建 env
| |
| +--> env 创建 input/output agent、model、scoreboard、FIFO
| |
| +--> active agent 创建 sqr/driver/monitor
| +--> passive agent 只创建 monitor
|
+--> my_case0 设置 default_sequence
|
v
connect_phase 连接 vif、seq_item_port 和 TLM 端口
|
v
main_phase 启动 sequence
|
v
sequence -> sequencer -> driver -> DUT
|
+--> input monitor -> model -> expected FIFO
+--> output monitor ---------> actual FIFO
|
v
scoreboard各阶段应看到的关键现象:
| 阶段 | 关键检查 |
|---|---|
| run_test | test 类型已注册,根实例为 uvm_test_top |
| build_phase | UVM 树完整,active/passive 配置正确 |
| config_db | driver/monitor 均取得正确 virtual interface |
| connect_phase | driver-sqr、monitor-FIFO、FIFO-scoreboard 已连接 |
| sequence | objection 已提起,item 能够持续产生 |
| driver | get_next_item 与 item_done 成对 |
| monitor | 每个完整协议包只发布一次 |
| model | 输入事务转换为独立的期望事务 |
| scoreboard | expected/actual 数量一致且最终队列清空 |
| report_phase | ERROR/FATAL 数量和残留事务共同决定 PASS/FAIL |
推荐在初次搭建时加入以下结构日志:
function void base_test::end_of_elaboration_phase( // build+connect 之后自动执行
uvm_phase phase
);
super.end_of_elaboration_phase(phase);
uvm_top.print_topology(); // 打印整棵 UVM 树:检查实例名、父子关系和 active 结构
endfunction在连接完成后检查端口:
function void my_env::connect_phase(uvm_phase phase);
super.connect_phase(phase);
// 所有 connect 完成后再打印关键路径,便于和 config_db 对照
`uvm_info("PATH", i_agt.drv.get_full_name(), UVM_LOW) // 打印 driver 完整路径
`uvm_info("PATH", o_agt.mon.get_full_name(), UVM_LOW) // 打印 monitor 完整路径
endfunction这两段是调试神器:树打不出来 = 组件没建/没挂对;路径和你 config_db 里写的对不上 = set 路径写错(最常见 bug)。
如果仿真卡住,按数据流方向排查:
- sequence 是否真的启动。
- sequencer 是否把 item 交给 driver。
- driver 是否完成 item_done。
- monitor 是否采到 valid 和完整包。
- analysis_port 是否写入 FIFO。
- model/scoreboard 的 blocking_get 是否返回。
- objection 是否因未 drop 而保持非零。
本章总结(2.1-2.5)
学习重点排序
| 优先级 | 必须掌握 |
|---|---|
| 🔴 高 | component/object 的区别及两类注册宏 |
| 🔴 高 | build、connect、main、report phase 的职责 |
| 🔴 高 | virtual interface 与 config_db 的传递方式 |
| 🔴 高 | sequence、sequencer、driver 的 item 握手 |
| 🟡 中 | agent、model、scoreboard 和 TLM 数据流 |
返回 UVM 学习笔记目录 · 上一章 · 下一章