摘要:UVM override 失效是静默的——不报错、不告警,测试照常跑完,错误注入却从未发生。本文剖析三种最常见的静默失效写法:用 new 建对象、override 写在 super.build_phase() 之后、set_inst_override 路径对不上,并给出正确写法与验证手段。
TL;DR 速览:
- 用 new 建对象,绕过工厂,override 失效。详见「失效写法一」。
- override 写在 super 之后,对象已建,失效。详见「失效写法二」。
- 实例路径对不上,匹配不到,静默失效。详见「失效写法三」。
- 正确做法:create 创建、先 override 后 super、路径传 this。
factory 是 UVM 复用性的基石。它解决的问题一句话能说完:在不动原始代码的前提下,把某个类替换成它的派生类。
但真正的坑不在“怎么用”,而在“为什么没生效”——override 失效是静默的:不报错、不告警,测试照常跑完,只是你注入的错误根本没出现。
工厂内部到底做了什么
UVM 工厂内部维护一张映射表:基类 → 实际要构造的类。
正常情况下这张表是恒等映射(dma_item → dma_item)。当你调用 set_type_override 或 set_inst_override,就把某一条映射改成了 dma_item → dma_err_item。
之后创建对象时,工厂查表得到实际类型,构造出派生类对象,再交给基类句柄持有,配合虚方法完成多态——“替换”就是这么实现的。
这也解释了为什么必须用 create:new 不查表。
四条前提,缺一不可
override 要生效,必须同时满足四条:
| # | 前提 | 漏掉的后果 |
|---|---|---|
| 1 | 类型用 `uvm_component_utils / `uvm_object_utils 注册过 | 工厂不认识这个类型,override 无从谈起 |
| 2 | 对象用 type_id::create() 创建 | override 静默失效 |
| 3 | override 调用发生在目标被 create 之前 | override 静默失效 |
| 4 | set_inst_override 的路径能匹配到创建者 | override 静默失效 |
后三条,是绝大多数“明明写了 override 却没生效”的真凶。
失效写法一:用 new 建对象
// ✗ 错误:绕开工厂
dma_item tr = new("tr");
// ✓ 正确:经过工厂
dma_item tr = dma_item::type_id::create("tr");
new 是 SystemVerilog 原生的构造调用,直接构造你写的那个类,与工厂毫无关系。工厂里的映射表根本没被查询,override 自然不生效。
这个错误不会报任何警告——测试跑得好好的,只是错误注入没有发生。
失效写法二:override 写在 super.build_phase() 之后
// ✗ 错误:env 已经把 item 造出来了
function void build_phase(uvm_phase phase);
super.build_phase(phase); // 这一步创建了 env 及其子组件
dma_item::type_id::set_type_override(dma_err_item::get_type()); // 太晚了
endfunction
UVM 的 build_phase 是自顶向下执行的:先 test,再 env,再 agent……super.build_phase() 一旦调用,下面的组件树就已经构建完毕,该创建的对象都创建完了。此时再改映射表,对已存在的对象无效。
正确顺序是先 override,后 super:
// ✓ 正确
function void build_phase(uvm_phase phase);
dma_item::type_id::set_type_override(dma_err_item::get_type());
super.build_phase(phase); // 现在创建的对象才会查新表
endfunction
失效写法三:set_inst_override 的路径对不上
路径这一条最容易踩,因为它有两个反直觉的地方。
第一,路径的起算点,取决于你有没有传第三个参数 parent。
- 传了
parent→inst_path被解释为相对该组件的层级路径 - 不传
parent→inst_path被解释为绝对实例路径(从uvm_top起算)
这一点在 UVM 参考手册里写得很明确。所以下面这种写法里,"env.agent.*" 会被当成从根算起的绝对路径,根本匹配不到 uvm_test_top.env.agent.*:
// ✗ 想当然:以为 "env.agent.*" 是相对于 test 的
dma_item::type_id::set_inst_override(dma_err_item::get_type(), "env.agent.*");
两种改法都对:
// ✓ 写法一:第三个参数传 this,路径就变成相对的
dma_item::type_id::set_inst_override(dma_err_item::get_type(), "env.agent.*", this);
// ✓ 写法二:不传 parent,那就老老实实写全路径
dma_item::type_id::set_inst_override(dma_err_item::get_type(), "uvm_test_top.env.agent.*");
第二,多条实例覆盖同时匹配时,先注册的那条赢。
UVM 参考手册的原话是:工厂处理实例覆盖时,实例队列按注册顺序处理,第一条匹配的生效;因此更具体的覆盖应当先注册,更通用的后注册。
注意这和 type override 的规则正好相反——type override 是后设的覆盖先设的(replace = 1 时)。所以下面这样写,具体那条永远轮不到:
// ✗ 顺序反了:通配写在前面,把后面更具体的那条吃掉了
dma_item::type_id::set_inst_override(dma_err_item::get_type(), "env.agent.*", this);
dma_item::type_id::set_inst_override(dma_delay_item::get_type(), "env.agent.sqr", this);
把两行顺序调过来即可——具体的先写,通用的后写。
这两条规则合在一起,解释了一个常见现象:为什么在子类 test 里加的实例覆盖,有时会“盖不过”父类 test 里的那条。
原因是父类通常也在 build_phase / end_of_elaboration_phase 里注册自己的覆盖,而这些方法的 super 调用由你决定放在哪里。如果子类把自己的覆盖写在 super.build_phase() 之后,它不仅要面对“晚于创建”的老问题,在注册顺序上也输给了父类(父类先注册,先匹配者生效)。
所以子类的实例覆盖应当写在 super 调用之前。这也是为什么很多验证环境的 test 里,override 语句总是 build_phase 的第一行。
一个不会静默的情况:替换类型没有继承关系
set_type_override 要求替换类型是原类型的派生类。道理很朴素:调用方手里拿的是基类句柄,装配进去的对象必须能赋值给它,也就是里氏替换。
// ✓ dma_err_item 必须 extends dma_item
class dma_err_item extends dma_item;
...
endclass
这不是静默失效——不同工具的表现形式不同,有的给告警、有的直接报错,总之跑一次就会报出来,不需要靠翻日志去发现。真正难查的,是上面那三种不声不响的情况。
type override 与 inst override
| 方法 | 作用范围 | 用法 |
|---|---|---|
set_type_override | 全局替换某类型 | dma_item::type_id::set_type_override(dma_err_item::get_type()) |
set_inst_override | 按层次路径替换某实例 | dma_item::type_id::set_inst_override(dma_err_item::get_type(), "env.agent.*", this) |
优先级规则:实例覆盖优先于类型覆盖,与两者的设置顺序无关。至于多条实例覆盖彼此之间的关系,见上一节——它反直觉。
一个完整例子——只对 env.agent 下的事务做错误注入:
class dma_err_test extends dma_smoke_test;
`uvm_component_utils(dma_err_test)
function void build_phase(uvm_phase phase);
// 第三个参数传 this,路径才是相对本组件的;
// 且必须早于 super.build_phase
dma_item::type_id::set_inst_override(
dma_err_item::get_type(),
"env.agent.*",
this);
super.build_phase(phase);
endfunction
endclass
第三个参数 this 不要省。省掉它,同一串 "env.agent.*" 会被当成绝对路径——匹配不上,而且悄无声息。
怎么确认 override 真的生效
因为失效是静默的,必须主动验证。三个手段:
一、打印工厂配置。 这是最直接的一个,能把每条 override 登记的路径原样打出来——路径写错时一眼就能认出来:
uvm_factory factory = uvm_factory::get();
factory.print(); // 输出当前所有 type / inst override 记录
uvm_top.print_topology(); // 顺带看实际建出来的类型对不对
二、打印实际类型。 在 end_of_elaboration_phase 里打印关键对象的 get_type_name(),确认拿到的已经是派生类名。
三、在派生类里加一条 uvm_info。 既然错误注入是故意的,就让它“出声”——跑一次就知道有没有生效。
另外,override 其实还能从命令行给,不必改代码:
+uvm_set_type_override=dma_item,dma_err_item
+uvm_set_inst_override=dma_item,dma_err_item,uvm_test_top.env.agent.*
下面给一个完整的回归脚本示例,把两个参数一起用上,并说明各自的格式与注意事项:
#!/bin/bash
# run_dma_err_regression.sh —— 用命令行 override 跑错误注入回归
1) 类型覆盖:把 dma_item 全局替换成 dma_err_item
格式:+uvm_set_type_override=<原类型>,<替换类型>[,replace]
- 原类型、替换类型都要用注册过的类型名(`uvm_object_utils 等)
- 可选第三段 replace:1 表示后设覆盖先设的,0 表示保留先设的
- 不写第三段时默认 replace=1
TYPE_OVERRIDE="+uvm_set_type_override=dma_item,dma_err_item"
2) 实例覆盖:只替换 uvm_test_top.env.agent 下的 dma_item
格式:+uvm_set_inst_override=<原类型>,<替换类型>,<实例路径>[,replace]
- 实例路径必须是绝对路径,从 uvm_top 起算,支持通配符 *
- 路径写错不会报错,override 会静默失效,务必用 factory.print() 核对
INST_OVERRIDE="+uvm_set_inst_override=dma_item,dma_err_item,uvm_test_top.env.agent.*"
3) 跑回归:把两个参数一起传给仿真器
vsim -c -do "run -all; quit"
+UVM_TESTNAME=dma_smoke_test
$TYPE_OVERRIDE
$INST_OVERRIDE
top_tb
注意事项:
- 类型名必须已注册:命令行里写的
dma_item、dma_err_item必须和代码里`uvm_object_utils/`uvm_component_utils注册的名字完全一致,否则工厂不认识,override 无从谈起。 - 实例路径是绝对路径:
+uvm_set_inst_override的路径从uvm_top起算,要写全uvm_test_top.env.agent.*,不能写相对路径env.agent.*——写错不报错,只会静默失效。 - 通配符用法:
*匹配任意一层或多层实例名,env.agent.*会匹配 agent 下所有子组件里创建的dma_item。 - 多条实例覆盖按注册顺序匹配:命令行参数按出现顺序注册,先匹配的那条生效;更具体的路径要写在更通用的前面。
- 回归后务必验证:命令行 override 不写进代码,光看代码看不出来。跑完用
factory.print()或uvm_top.print_topology()确认实际建出来的类型,再配合派生类里的uvm_info确认错误注入真的发生。
回归里换变体时很方便,代价是不看代码看不出来——所以线上环境慎用。
小结
factory 能做到“不改代码就替换”,靠的是查表 + 多态;而 override 生效需要四条前提同时成立:
注册宏 + type_id::create() + override 早于创建 + 实例路径能匹配
四条里缺任何一条,override 都可能静默失效。这是 UVM 里最典型的“测试没报错,但验证根本没做”的场景之一。

460

被折叠的 条评论
为什么被折叠?



