UVM env top test 装配:test/env/agent 三层结构、run_test 启动与配置下发全解
更新时间:2026-09-28。在 UVM 组件与 phase 里我们认识了 driver、monitor、scoreboard 这些零件,在 UVM config object 与 resource db 里见过配置怎么存取。但零件到手不等于机器能跑:谁把这些组件 new 出来、按什么次序、配置参数从顶层怎样流到最底层的 agent?这一篇就把 UVM env top test 装配这件事从头到尾讲透,目标是你读完能从零搭出一套
run_test一把启动的最小环境。
本文要回答的问题
- test、env、agent 三层各自负责什么,为什么不能把 scoreboard 直接挂在 test 下面?
run_test("my_test")这一行背后,UVM 到底做了哪些事?- top 模块里的 DUT 接口句柄,怎样穿过 test、env 到达 agent 里的 driver?
- 同一份 env 代码要支撑几十个测试用例,差异化配置从哪一层注入最干净?
- 装配阶段有哪些「编译能过、仿真起来就 fatal」的典型错误?
一、三层结构:为什么是 test / env / agent
UVM 的组件树不是平铺的一堆组件,而是一棵有明确层次语义的树。最经典的切法就是三层:test 在最顶,env 居中,agent 贴着 DUT 的每个接口各一个。
- test(uvm_test 的子类):一次仿真运行对应一个 test 实例。它负责「这一次要验什么」——选哪个 sequence、超时设多长、把 env 配置成什么模式。test 是整棵组件树的根之一,也是配置下发的起点。
- env(uvm_env 的子类):环境的容器,代表「围绕 DUT 的完整可复用验证环境」。env 本身不写激励、不做比对逻辑,只负责把若干个 agent、scoreboard、coverage、reference model 装配在一起并接好 TLM 连线。env 追求的是跨用例、甚至跨项目复用。
- agent(uvm_agent 的子类):一个 DUT 接口对应一个 agent,里面装 sequencer、driver、monitor 三件套。agent 是「协议执行单元」,AXI 口一个 agent、APB 口一个 agent,彼此独立。
这条分层背后的动机是复用边界的对齐。agent 复用的粒度是「协议」,换项目只要接口协议相同,agent 原封搬走;env 复用的粒度是「DUT 周边」,同一个 DUT 的所有用例共享同一个 env;test 则是完全不复用的那一层——每个用例一个 test 类,差异全部关在这里。如果把 scoreboard 直接挂到 test 下面,换用例时连接关系就散了,env 的整体复用也无从谈起。

这棵三层组件树图就是 UVM env top test 装配的骨架:test 只生一个 env,env 下面再铺开 agent 与比对组件。实际项目里 env 之下还可以嵌套 sub-env(比如一个子系统级 env 复用到 SoC 级),但每多一层,职责判据不变——容器只管装配,叶子才干活。
| 层级 | 基类 | 数量 | 复用粒度 | 典型内容 |
|---|---|---|---|---|
| test | uvm_test | 每用例一个 | 不复用 | 配置注入、sequence 选择、超时 |
| env | uvm_env | 每 DUT 一个 | 跨用例/跨项目 | agent、scoreboard、coverage、连线 |
| agent | uvm_agent | 每接口一个 | 跨项目(同协议) | sequencer、driver、monitor |
二、top 模块与 run_test:仿真是怎么被点着的
组件树不会自己长出来,一切从 Verilog 顶层模块开始。testbench 的顶层是一个普通 module,惯例叫 tb_top,它做三件 UVM 之外的事:产生时钟复位、例化 DUT、例化 interface。最后一行才是 UVM 世界的入口:
module tb_top;
logic clk, rst_n;
axi_if axi_vif(clk, rst_n);
apb_if apb_vif(clk, rst_n);
my_dut dut (
.clk(clk), .rst_n(rst_n),
.axi(axi_vif), .apb(apb_vif)
);
initial begin
uvm_config_db #(virtual axi_if)::set(null, "uvm_test_top.env.axi_agt", "vif", axi_vif);
uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top.env.apb_agt", "vif", apb_vif);
run_test("smoke_test");
end
endmodule先看 module 顶层的写法细节。时钟复位用 initial/always 产生,interface 例化时把时钟信号绑进去(axi_if axi_vif(clk, rst_n)),DUT 的端口直接接 interface 实例而不是逐信号连接——这就是 interface 存在的意义:top 层的连线从几十根信号缩成一句 .axi(axi_vif)。initial 块里先做两件 set,再调 run_test,次序不能反:set 必须在 run_test 创建组件树之前完成,否则 build_phase 里 get 不到。另一个常见疑问是为什么 set 的第一个参数写 null:top 是 module 不是 uvm_component,没有组件上下文,null 表示从组件树根部做绝对路径匹配,路径字符串因此必须带完整的 uvm_test_top 前缀。
run_test 是一个全局 task,它做的事情远比名字多。拆开看:先从命令行 +UVM_TESTNAME= 或参数里确定要跑哪个 test 类名;然后通过 factory 创建这个 test 的实例,并把它命名为固定的 uvm_test_top——这就是为什么配置路径都以 uvm_test_top 开头;接着 UVM 内核驱动 phase 机制自上而下运转:build_phase 自顶向下逐层创建子组件,connect_phase 自底向上完成 TLM 接线,然后进入 run_phase 跑激励,最后 extract/check/report 收尾。整个过程不需要你手写任何 new 的顶层调用。

这张启动流程图里有一个值得记住的细节:run_test 的参数其实可以被命令行 +UVM_TESTNAME=xxx 覆盖。工程上的惯例是 run_test() 括号里给一个默认冒烟用例名,回归脚本里一律用 plusarg 指定用例,这样同一份编译产物可以跑任意用例,不用为用例切换重新编译——回归规模上来以后,这一条省下的编译时间非常可观。
三、build_phase:组件树自顶向下的生长次序
phase 机制在 UVM 组件与 phase 里已经讲过,这里只聚焦与装配直接相关的次序问题。build_phase 是 function phase,自顶向下执行:先执行 test 的 build_phase,再执行 env 的,最后是 agent 的。这意味着父组件的 build_phase 运行时,子组件还不存在;而子组件的 build_phase 运行时,父组件已经完成创建。
这个次序直接决定了三件事的写法。第一,子组件一律在父组件的 build_phase 里创建,且必须用 factory:
class my_env extends uvm_env;
`uvm_component_utils(my_env)
axi_agent axi_agt;
apb_agent apb_agt;
my_scoreboard sb;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
axi_agt = axi_agent::type_id::create("axi_agt", this);
apb_agt = apb_agent::type_id::create("apb_agt", this);
sb = my_scoreboard::type_id::create("sb", this);
endfunction
endclasscreate 的第二个参数 this 把新组件挂到当前组件名下,组件的 full name 自动变成 uvm_test_top.env.axi_agt——这个名字既是 config_db 路径匹配的依据,也是报错信息里的定位坐标,实例名取得含糊,后面 debug 时满屏 uvm_test_top.env.a1.a2 会很难受。第二,配置下发必须先于创建:test 想把某个参数给 agent,要在自己的 build_phase 里先 set 再(由 env 去)create,次序反了子组件取到的就是默认值。第三,绝不能把 new 子组件写进 connect_phase 或 run_phase——build_phase 结束时组件树就冻结了,后面再创建的组件不会被 phase 机制管理,也不会被 config_db 正确遍历。

这张 build 次序图看着简单,违反它的后果却隐蔽:父组件如果在 connect_phase 才 create 子组件,仿真不会立刻报错,但子组件的 build/configure 逻辑根本没跑,往往在 run 阶段以「句柄为 null」或「配置没生效」的形式爆出来,定位成本极高。
四、配置下发:virtual interface 与参数怎么流到 agent
top 模块里的 set 只是把东西放进了 config_db 这个全局仓库,真正的接力发生在每一层的 build_phase 里。路径 "uvm_test_top.env.axi_agt" 用的是实例名点分层,通配符也支持;agent 内部用 get 取回:
class axi_agent extends uvm_agent;
`uvm_component_utils(axi_agent)
virtual axi_if vif;
axi_driver drv;
axi_monitor mon;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(virtual axi_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "axi vif not set")
drv = axi_driver::type_id::create("drv", this);
mon = axi_monitor::type_id::create("mon", this);
uvm_config_db #(virtual axi_if)::set(this, "drv", "vif", vif);
endfunction
endclass注意这个「取到再下发」的模式:config_db 的 set 只在设置那一刻匹配当时已存在的组件并缓存结果——更准确地说,get 时的查找沿组件树向上进行,先 set 后 create 能命中,是因为查找发生在子组件 get 的时刻。但 driver 在 agent build 时才被创建,top 模块当初 set 的路径只到 agent 一层,所以 agent 必须显式把 vif 再往 drv 送一层。这一手接力是 UVM env top test 装配里最容易漏的环节:漏了的症状是 driver 里 vif 为 null,uvm_fatal 一报,仿真零时刻退出。
除了 virtual interface,test 层下发的典型配置还有:agent 的 active/passive 模式(is_active 枚举,passive 时只建 monitor 不建 driver/sequencer)、覆盖率开关、超时阈值、协议参数(地址位宽、outstanding 上限)。做法统一封装成一个 config object,test 里 new 出来、填好字段、一次 set 下去,env 和 agent 各取所需。这套做法的细节在 UVM config object 与 resource db 里有完整展开,这里只强调装配视角的原则:配置的产生在 test,配置的消费在叶子,中间的 env 只做透传与裁剪。
| 配置项 | set 的位置 | get 的位置 | 路径写法 |
|---|---|---|---|
| virtual interface | tb_top 的 initial | agent 的 build | uvm_test_top.env.axi_agt |
| env 级 config object | test 的 build | env/agent 的 build | uvm_test_top.env* |
| is_active 模式 | test 的 build | agent 的 build | uvm_test_top.env.*_agt |
| plusarg 临时开关 | 任意(建议 test) | 任意 | 命令行直达 |
一个与路径相关的工程建议:env 里的实例名尽量与 agent 类型保持可读对应(axi_agt、apb_agt),不要用 agt0、agt1。配置路径、报错信息、波形里的 hierarchy 全都用这个名字,语义化的名字在回归 triage 时每天都在替你省时间。
把整条配置流再串一遍时序:tb_top 零时刻 set vif(路径指到 agent 层)→ test 的 build_phase set env_cfg(路径指到 env 层)并 create env → env 的 build_phase get cfg、按 cfg 决定建哪些 agent、可选地把裁剪后的子配置继续 set 给各 agent → agent 的 build_phase get vif 与 cfg、create driver/monitor/sequencer、把 vif 续传给 driver。每一层都是「先 get 自己的、再决定建什么、最后为下一层 set」,三步走完组件树恰好自顶向下生长一层。env 在这一链条里是中转站而非终点:test 给的 cfg 字段未必全部属于 env 自己,env 按字段归属拆开,agent 相关的部分原样或改写后继续下发,这样 test 不必知道 env 内部有几个 agent、叫什么名字,层与层之间保持松散。
多 env 拼装时这套次序同样成立,多出来的只有命名与连接的组织问题。SoC 级平台常有 pcie_env、ddr_env、cpu_env 等多个子 env,chip_test 下面并行创建它们。命名上给每个 env 实例起协议语义名(pcie_env0 这种带序号的留给数组化例化场景,配 for 循环里用 $sformatf 拼实例名与配置路径);连接上,子 env 之间的 TLM 连线一律收在 chip 级 env 的 connect_phase,子 env 各自只暴露 export,不越层直接连对方内部组件——否则复用某个子 env 到别的项目时,会把一条指向兄弟 env 的硬连线一起带走,接线当场断掉。虚拟 sequence 的路由表(virtual sequencer 里指向各子 env sequencer 的句柄数组)也在 chip env 的 connect 里按名字赋好,test 层只起 virtual sequence,不关心底层有几个 sequencer。
五、test 层:用例差异化的唯一入口
同一个 env 要支撑冒烟、随机压力、错误注入等几十上百个用例,装配纪律是:所有差异都在 test 层注入,env 和 agent 的代码一个 if 都不为用例而加。一个典型 test 长这样:
class smoke_test extends uvm_test;
`uvm_component_utils(smoke_test)
my_env env;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env_cfg cfg = env_cfg::type_id::create("cfg");
cfg.axi_active = UVM_ACTIVE;
cfg.apb_active = UVM_PASSIVE;
cfg.timeout_ns = 2_000_000;
uvm_config_db #(env_cfg)::set(this, "env", "cfg", cfg);
env = my_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
smoke_seq seq = smoke_seq::type_id::create("seq");
phase.raise_objection(this);
seq.start(env.axi_agt.sqr);
phase.drop_objection(this);
endtask
endclass新用例 = 新 test 类:换一个 sequence、改几个 cfg 字段、或者把某个 agent 调成 passive,就构成了一个新用例,全部通过 factory 注册后由 +UVM_TESTNAME 选择。想要更强的差异化——比如错误注入场景要把 driver 换成一个会注错的变体——用 factory override,在 test 的 build_phase 里 set_type_override_by_type,env 里的 create 语句一行不改,创建出来的就是替换后的类型。factory 覆写的更多花样见 UVM factory 与 env 派生。

上面这张图表达的是 UVM env top test 装配的终极目标:env 是被复用的常量,test 是排列组合的变量。objection 的提起与落下决定用例何时结束,这块语义(包括多层 objection、drain time)在 UVM phase 与 objection 结束机制 里单独成篇,装配层面只需要记住:run_phase 里不 raise_objection,用例会在激励发出前就结束,是新手最常见的「仿真秒退、日志空空」根因。
六、从零搭一套最小环境:装配清单
把前面五节压缩成可照做的清单,一个新 block 的 UVM 环境从零到跑通按这个次序走:
- 写 interface 与 tb_top:时钟复位、DUT 例化、vif 的
set、run_test()给默认用例名。 - 写 transaction 与 agent 三件套:driver 从 sequencer 取 item 驱动 vif,monitor 采样 vif 经 analysis_port 广播;TLM 端口细节见 SystemVerilog TLM 通信。
- 写 env:build 里 create 各 agent 与 scoreboard,connect 里接 analysis 连线,模式开关走 config。
- 写 base_test 与 smoke_test:base_test 只做公共的 cfg 构造与 env 创建,具体用例继承它只改差异部分——这能让几十个 test 类保持干净。
- 冒烟验证:编译、
+UVM_TESTNAME=smoke_test跑几笔定向事务,确认 config_db 无CFG类 warning、vif 不为 null、scoreboard 有比对输出。 - 再叠随机用例与覆盖率,接入回归脚本用 plusarg 选用例。
每一步都有「编译能过但仿真起不来」的对应坑,装配期高频问题集中在这张表里:
| 症状 | 根因 | 处理 |
|---|---|---|
| 零时刻 fatal「vif not set」 | top 的 set 路径与 agent 实例名不符,或 agent 漏向 driver 续传 | 对照 print_topology 输出逐段核对路径 |
| get 返回 0 但仿真照跑 | get 失败被 if 吞掉、用了默认值 | 配置项 get 失败一律报 uvm_fatal,不许沉默 |
| 组件树里没有预期组件 | 子组件写在 connect_phase 才 create | 创建动作全部挪回 build_phase |
| 仿真秒退、无激励日志 | run_phase 未 raise_objection | test 的 run_phase 起手先 raise |
| override 不生效 | set_type_override 写在 create 之后 | 覆写语句放 test build_phase 开头 |
| 多 env 场景连线断裂 | 子 env 之间越层直连内部组件 | 连线收拢到父 env 的 connect_phase |
| +UVM_TESTNAME 报类不存在 | test 类忘了注册宏或包没 import | 查 factory 注册与编译文件列表 |
装配阶段的报错九成发生在零时刻,把启动日志里 uvm_test_top 开头的组件树打印(+UVM_VERBOSITY=UVM_HIGH 或调 uvm_top.print_topology())看一遍,结构与预期对不对一目了然。更稳妥的做法是在 end_of_elaboration_phase 里主动调一次拓扑打印:此时组件树已经冻结、连线也全部完成,打印出来的就是本次仿真真实的最终结构。持续集成脚本可以把这段拓扑输出从日志里抓出来存成工件,回归失败时先比对拓扑与基线的差异——组件缺失、实例名漂移、agent 被误配成 passive 这类问题,看拓扑比翻几千行日志快得多。团队里还可以约定每个新用例合入时附带一份预期拓扑片段,评审时对照检查,装配错误在进回归之前就被拦下。还有一个容易忽视的检查点:build_phase 结束时 dump 一次 config_db 的访问记录(uvm_config_db_options::turn_on_tracing() 或各厂商等价开关),凡是 set 了却从未被 get 的条目,多半是路径写歪了的死配置——它们不报错,但意味着某个组件正拿着默认值在跑,用例行为与预期悄悄分叉。回归稳定后建议把 tracing 关掉,日志量会明显干扰正常排查。
走完这六步,一套环境通常两三个工作日就能从空目录跑到第一笔比对通过。前期的快不代表可以省纪律:装配阶段偷的每一次懒(漏一个 fatal、少一层续传、图省事把配置写死),都会在用例数量上量之后以十倍的排查成本要回来。装配是平台类代码里修改频率最低、被复用次数最多的部分,值得一次写对。
这一篇在知识体系里的位置
- 向上承接:UVM 组件与 phase 给出组件与 phase 的基本概念,本篇把它们组织成可运行的三层结构。
- 横向相关:UVM config object 与 resource db 深入配置数据库的匹配规则;SystemVerilog TLM 通信 负责 env 内部连线的那一半。
- 落地延伸:UVM factory 与 env 派生 讲覆写与派生玩法;分层 testbench 架构与复用 把三层结构推广到子系统与 SoC 级;AMBA AXI UVM 环境搭建 是这套装配流程在真实总线上的完整实例。
总结
UVM env top test 装配就是一棵树加两条流:test/env/agent 三层组件树在 build_phase 里自顶向下由 factory 创建,配置与 virtual interface 沿 config_db 自顶向下逐层接力下发,run_test 点火后差异全部关在 test 层、复用全部沉淀在 env 与 agent——守住「父 build 里先 set 后 create、env 只装配不写激励、vif 层层显式续传」三条纪律,零时刻 fatal 就追不上你。