UVM 验证平台骨架模板:env/agent/driver/monitor/scoreboard 五件套最小骨架
更新时间:2026-11-04。模板实战页,属于芯片设计入门系列。学完 SystemVerilog 的 class、虚接口和随机约束,第一次打开一个真实 UVM 工程,目录里往往躺着十几个文件:env、agent、driver、monitor、scoreboard、sequence、test……文件名都认识,合在一起不知道从哪读起。这篇 UVM 验证平台骨架模板不给你讲方法学全貌,只交付一份能照着抄的最小骨架:五件套各管什么、怎么连到 DUT、phase 按什么顺序跑、一笔 sequence item 怎么从产生走到比对。组件背后的原理见 UVM 验证平台骨架模板的组件与 phase 讲解,一笔事务的完整旅程见 同骨架的数据流实战。
本文要回答的问题
- env、agent、driver、monitor、scoreboard 五件套各自只负责什么?
- class 写的平台怎么碰到 module 里的真实引脚?
- 一份最小骨架按什么顺序写,phase 自动调度时谁先谁后?
- sequence item 怎么从 sequence 流到 DUT,再被抓回 scoreboard?
- 新手最常踩的四个坑(工厂注册、config_db 路径、objection)长什么样?
一、使用场景与五件套分工
这份 UVM 验证平台骨架模板适用于「块级验证平台从零起步」:DUT 是一个带时钟和简单握手接口的模块,需要随机激励、自动比对和后续挂覆盖率。它也是从手写 testbench 跨到 UVM 的过渡台阶——手写 TB 把激励、采样、比对全塞在一个 initial 块里,小模块够用,换个 DUT 就得重写;组件化之后每个类只干一件事,可以独立替换和复用。为什么要做这层拆分,见 验证与测试的区别。
读这张卡抓住一条分界线:driver 是唯一允许写引脚的组件,monitor 永远被动。很多新手让 monitor 去拉反压信号、让 driver 顺便判断对错,平台很快就没法复用。下面这张表把五件套压成一句话职责,写代码时可以贴在旁边。
| 组件 | 单一职责 | 不许干什么 |
|---|---|---|
| driver | 收事务,按时钟拍成引脚波形 | 不判断对错、不做期望模型 |
| sequencer | 把 sequence 的 item 派发给 driver | 自己不产生激励 |
| monitor | 被动采样引脚,还原事务并广播 | 不驱动任何信号 |
| scoreboard | 保存期望队列,与实际逐笔比对 | 不直接碰引脚 |
| env/agent | 纯容器:创建组件、完成连线 | 不写激励逻辑 |
二、完整骨架与连接关系
先看拓扑。一笔事务的完整路径是:sequence 随机出 item,经 sequencer 派发给 driver,driver 通过 virtual interface 驱动 DUT 引脚;monitor 在另一侧旁路采样 DUT 输出,还原成事务经 analysis port 广播,scoreboard 用输入侧算出的期望值逐笔比对。
橙色是主动激励路径,蓝色是被动观测路径,灰色是 class 世界和硬件世界的边界。注意 virtual interface 是这两个世界唯一的桥:class 不能例化硬件,只能握着 interface 的句柄读写信号。
下面给出最小可读骨架。为了让没有 UVM 仿真器的环境也能跑通,可运行版用普通 class 加 mailbox 复刻同样的职责切分;右侧注释标出真实 UVM 里对应的写法。先是事务与 sequencer:
class seq_item; // 对应 extends uvm_sequence_item
bit [7:0] data;
function new(bit [7:0] d); data = d; endfunction
endclass
class sequencer; // 对应 extends uvm_sequencer #(my_item)
mailbox #(seq_item) req_fifo; // UVM 里是 seq_item_port 握手
function new(mailbox #(seq_item) m); req_fifo = m; endfunction
task run(int n);
for (int i = 0; i < n; i++) begin
bit [7:0] d; seq_item it;
d = 8'($urandom() & 32'hFF);
it = new(d);
req_fifo.put(it); // 对应 start_item/finish_item
end
endtask
endclassdriver 是骨架里最核心的一段:取事务、在时钟沿驱动、再通知 sequencer 这笔干完了。它同时把输入事务丢一份给期望通道,相当于 UVM 平台里输入 monitor 加 predictor 的作用。
class driver;
virtual dut_if vif;
mailbox #(seq_item) req_fifo, exp_fifo;
task run(int n);
@(posedge vif.rst_n);
repeat (n) begin
seq_item it;
req_fifo.get(it); // UVM: get_next_item(req)
@(posedge vif.clk);
vif.in_valid <= 1'b1;
vif.in_data <= it.data;
exp_fifo.put(it); // 送参考模型
@(posedge vif.clk);
vif.in_valid <= 1'b0;
repeat ($urandom() % 3) @(posedge vif.clk); // 随机空闲
end
endtask
endclassmonitor 与 scoreboard 各守一个 mailbox:monitor 轮询输出有效信号,抓到一笔就广播;scoreboard 按顺序取出期望(本例参考模型是 data+1)和实际做比对,不匹配就打印 MISMATCH 并累计错误数。真实 UVM 里 mailbox 换成 analysis port,scoreboard 用 uvm_analysis_imp_decl 声明输入、输出两个 imp,完整写法在配套代码的 uvm/my_scoreboard.sv。
class monitor;
virtual dut_if vif;
mailbox #(seq_item) rsp_fifo;
task run(int n);
@(posedge vif.rst_n);
repeat (n) begin
do @(posedge vif.clk);
while (vif.out_valid !== 1'b1);
begin seq_item it; it = new(vif.out_data); rsp_fifo.put(it); end
end
endtask
endclass最后是 test 这个容器:build 阶段创建全部组件,connect 阶段连线,run 阶段并行启动四条生命周期,收尾阶段汇总报告。这正是 UVM 自动按 phase 顺序调度的内容,这里手工调用只是为了把顺序摊开给你看。
task run_phase;
$display("[PHASE] run_phase: raise_objection");
fork
sqr.run(n_items); drv.run(n_items);
mon.run(n_items); scb.run(n_items);
join
#200ns; // 等流水线排空
$display("[PHASE] run_phase: drop_objection");
endtask三、使用注意事项:四个必踩的坑
工厂注册漏宏。 每个 uvm_component 子类都要写 `uvm_component_utils,事务类写 `uvm_object_utils。漏了的症状不是编译报错,而是 test 里 type_id::create() 直接 fatal 报工厂里找不到这个类型。UVM 靠这张注册表实现「不改平台代码就替换组件」的多态重载。
phase 顺序别假设。 固定顺序是 build(自顶向下)→ connect(自底向上)→ run(所有组件并行,仿真时间在这里流逝)→ extract/check/report。config_db 的 set 必须在顶层 build_phase 完成、get 在子组件 build_phase 执行,时序天然满足;在 run_phase 里才去 set interface,子组件已经拿到空句柄,立刻 uvm_fatal("NOVIF")。
config_db 路径要对齐。 顶层写 `uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.agt.drv", "vif", if0),driver 里用 get(this, "", "vif", vif) 取。通配写法是第二个参数给 "*"。路径串里任何一级实例名拼错,get 返回 0,务必检查返回值并 fatal,不要让空虚接口拖到信号访问时才报晦涩的空指针。
objection 忘停与忘开。 两种死法:run_phase 里忘记 raise_objection,UVM 发现没人反对结束,仿真在 0 时刻直接退出,表现是平台「什么都没跑」;raise 了但所有分支都没 drop,仿真会挂到超时才被杀。纪律是 raise/drop 成对包在 sequence 启动外层,平台骨架里就是 phase.raise_objection(this); seq.start(sqr); phase.drop_objection(this);。
四、修改示范:加覆盖率与换接口
第一个常见改动是加功能覆盖率收集器:不动任何现有组件,新写一个继承 uvm_subscriber #(my_item) 的类,在 write 里 sample,env 的 connect_phase 再补一行连线即可。这正是 analysis port 广播模型的红利——monitor 对有几个订阅者一无所知。
class my_coverage extends uvm_subscriber #(my_item);
`uvm_component_utils(my_coverage)
bit [7:0] data;
covergroup cg_data;
cp_data: coverpoint data {
bins low = {[0:84]}; bins mid = {[85:169]}; bins high = {[170:255]};
}
endgroup
function new(string name, uvm_component parent);
super.new(name, parent); cg_data = new();
endfunction
virtual function void write(my_item t);
data = t.data; cg_data.sample();
endfunction
endclass
// env.connect_phase 追加:agt.mon.ap_out.connect(cov.analysis_export);覆盖率怎么看 bin、怎么定目标,见 功能覆盖率入门。第二个常见改动是换接口:DUT 升级为 valid/ready 反压接口时,改动只落在三处——interface 加 ready 信号、driver 的 drive 任务在握手处等 ready、monitor 按相同协议采样;sequencer、scoreboard、env 一行不动。接口与时钟同步的写法细节见 clocking block 与 testbench 同步。
五、真实跑一遍
UVM 库不是开源 Verilator 的自带件,真实工程要靠 VCS、Questa 等带 UVM 库的仿真器。为了让骨架行为可以被任何人直接验证,配套实验把同一份五件套结构写成不依赖任何 UVM 宏的普通 SystemVerilog,用 Verilator 5.020 真实编译运行;完整 UVM 五件套模板(工厂宏、config_db、analysis imp、objection 齐全)放在同目录 uvm/ 下,供有 UVM 仿真器时直接使用。
DUT 是一个一拍延迟的 data+1 模块。第一轮通过 +inject_bug 让它算成 data+2,scoreboard 立刻逐笔抓到期望值与实际值差 1,8 笔事务全部 MISMATCH:
实录里每一笔都能对上号:driver 驱动 196,monitor 一拍后采样到 198,scoreboard 算出期望 197,于是打印 MISMATCH #1 ... expected=197 actual=198。注意 phase 行的顺序——build、connect 在前,run_phase 打印了 raise_objection 之后数据流才开始,最后 drop 并出报告,和第三节讲的 phase 机制完全一致。去掉注入参数重跑,DUT 恢复 data+1:
这一轮没有任何 MISMATCH 行,monitor 采样值与驱动值严格差 1,报告打印 TEST PASS: 8 items matched。同一个平台代码一行没改,只改了 DUT 的行为,比对结果自动翻转——这就是五件套结构相对手写 TB 的价值。
代码获取:可运行的简化骨架、真实 UVM 五件套模板与复现说明都在公开仓 experiments/tpl-uvm-testbench-skeleton,克隆后执行 INJECT_BUG=1 ./run.sh && ./run.sh 即可复现上面两轮结果(需要 Verilator 5.020 以上,或使用仓库 README 给出的 geek-eda 容器)。
实验环境
- 机器/系统:Mac mini M4 / macOS(Darwin 25),实验在 geek-eda:0.4-flow 容器内执行
- 工具版本:Verilator 5.020(
verilator --version输出Verilator 5.020 2024-01-01 rev (Debian 5.020-1)) - 日期:2026-11-04
- 完整命令:
INJECT_BUG=1 ./run.sh;./run.sh
这一篇在知识体系里的位置
本篇是 UVM 入门的「抄得走的起点」:向上读 UVM 组件与 phase 理解组件化与工厂机制,顺着数据流读 UVM sequence/driver/scoreboard 实战 看 valid/ready 握手和乱序比对,验证平台之前的手写形态对照 SystemVerilog 自检 testbench 基础。
总结
UVM 验证平台骨架模板的核心是一条职责分界线加一条数据流:driver/sequencer/sequence 是主动侧,只负责把随机事务拍成引脚;monitor/scoreboard 是被动侧,只负责采样还原与逐笔比对;virtual interface 是 class 与硬件之间唯一的桥,env/agent 只做创建和连线。落地时盯住四件事:工厂宏不漏、phase 顺序不假设、config_db 路径与实例名严格对齐、objection 成对 raise/drop。骨架经 Verilator 两轮实跑验证,注入差异时 8/8 MISMATCH,修复后 8 笔全 PASS。