O、代码分析
一、代码逐行详细注释
第一部分:comp1组件类
systemverilog
// 定义类comp1,继承uvm_component(UVM所有组件的基类,能参与phase机制、uvm树)
class comp1 extends uvm_component;
// 注册宏:UVM工厂注册,支持type_id::create创建实例,必须写在component类里
`uvm_component_utils(comp1)
int val1 = 1; // 定义int变量,默认初始值=1
string str1 = "null";//定义字符串变量,默认初始值"null"
// 构造函数,uvm_component固定new函数签名:名字+父节点
function new(string name, uvm_component parent);
super.new(name, parent); // 调用父类uvm_component的构造函数,挂载到UVM树
endfunction
// build_phase:UVM自上而下的构建阶段,用来获取config_db参数、创建子组件
function void build_phase(uvm_phase phase);
// uvm_info打印信息:ID=SETVAL,打印val1获取前的值,UVM_LOW是消息等级
`uvm_info("SETVAL", $sformatf("val1 is %d before get", val1), UVM_LOW)
`uvm_info("SETVAL", $sformatf("str1 is %s before get", str1), UVM_LOW)
// config_db get:从UVM配置数据库取出int类型,存入val1
// 参数说明:
// 1. this:当前组件comp1作为查找上下文
// 2. "":相对路径空字符串,代表在当前组件自身查找
// 3. "val1":key名字,必须和set的第三个参数完全一致
// 4. val1:ref引用,拿到的值会写入这个变量
uvm_config_db#(int)::get(this, "", "val1", val1);
uvm_config_db#(string)::get(this, "", "str1", str1);
// 打印get之后更新完的变量
`uvm_info("SETVAL", $sformatf("val1 is %d after get", val1), UVM_LOW)
`uvm_info("SETVAL", $sformatf("str1 is %s after get", str1), UVM_LOW)
endfunction
endclass
第二部分:test1测试用例类
systemverilog
// test1类,继承uvm_test,uvm_test是顶层测试类,也是uvm_component
class test1 extends uvm_test;
`uvm_component_utils(test1) // 工厂注册宏
comp1 c1; // 声明comp1类型句柄,用来存放子组件实例
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
// config_db set:往UVM全局配置数据库存入参数
// 参数:
// 1. this:当前test1作为上下文
// 2. "c1":目标组件的相对实例路径(相对于this,就是test下名字叫c1的组件)
// 3. "val1":key名字,comp1里get时匹配这个key
// 4. 100:要存入的值
uvm_config_db#(int)::set(this, "c1", "val1", 100);
uvm_config_db#(string)::set(this, "c1", "str1", "comp1");
// 工厂创建comp1实例,名字叫"c1",父节点是this(当前test1),挂载到UVM树
c1 = comp1::type_id::create("c1", this);
endfunction
endclass
// 模块顶层启动代码
initial begin
run_test("test1"); // 启动UVM,创建test1类的实例,开启UVM所有phase
end
二、执行流程拆解
- 执行
run_test("test1"),UVM自动创建test1类的实例 ,实例的名字固定叫uvm_test_top,不是test1! - 进入test1的
build_phase(自上而下phase,父组件先执行build)- 执行
uvm_config_db::set,把val1=100、str1="comp1"存入全局数据库;目标路径:uvm_test_top.c1 comp1::type_id::create("c1", this)创建子组件c1,父是uvm_test_top
- 执行
- 接下来执行子组件c1的
build_phase- 先打印默认初始值:
val1=1,str1="null" - 执行
uvm_config_db::get,在数据库中匹配路径+key,拿到刚才set的100和"comp1",覆盖成员变量 - 打印修改后的值:
val1=100,str1="comp1"
- 先打印默认初始值:
三、核心问题:为什么打印uvm_test_top.c1而不是test1.c1
类名是
test1,但是run_test创建出来的实例对象名字固定是uvm_test_top,这是UVM规定死的规则
-
test1是类名(class的名字,相当于图纸) -
uvm_test_top是实例对象的instance name (用图纸造出来的实体,UVM给这个顶层test实体强制命名为uvm_test_top) -
UVM打印层次路径,打印的是实例对象名字,不是类名
-
完整UVM树层次:
uvm_top(uvm_root全局根)
└── uvm_test_top (test1类的实例,名字叫uvm_test_top)
└── c1 (comp1实例)
所以打印路径:uvm_test_top.c1,不是test1.c1
一句话区分:
test1是类(图纸),uvm_test_top是造出来的实例(房子)。UVM报告打印的是房子名字,不是图纸名字。
四、uvm_config_db核心原理
uvm_config_db相当于全局共享的数据库,用【上下文+实例路径+field名字】三元组作为索引存取数据
set:存数据,父组件提前把参数放进数据库(test里set,发生在子组件build之前)get:取数据,子组件在build阶段从数据库检索取出,赋值给自己的成员变量
重点:build phase是自上而下执行 !父test的build先执行,先
set;再创建c1,再跑c1的build做get。所以get能读到set进去的值。
五、输出结果对应解释
UVM_INFO @ 0: uvm_test_top.c1 [SETVAL] val1 is 1 before get
// c1刚进入build,还没get,使用类定义默认值 val1=1
UVM_INFO @ 0: uvm_test_top.c1 [SETVAL] str1 is null before get
// str1默认初始值"null"
UVM_INFO @ 0: uvm_test_top.c1 [SETVAL] val1 is 100 after get
// get成功拿到test中set的100,覆盖val1
UVM_INFO @ 0: uvm_test_top.c1 [SETVAL] str1 is comp1 after get
// get拿到字符串"comp1",覆盖str1
补充小考点(面试常问)
uvm_config_db::get有返回bit:1=获取成功,0=没找到。工程中一般会加判断:
systemverilog
if(!uvm_config_db#(int)::get(this, "", "val1", val1)) begin
`uvm_warning("SETVAL", "val1 not found in config_db")
end
- set和get的路径匹配规则:
set(this, "c1", ...)等价于目标路径uvm_test_top.c1;c1内部get(this,"",...)代表在自身实例uvm_test_top.c1查找,刚好匹配。
一、UVM树简易结构图(原始代码版本)
uvm_top (uvm_root 全局根,UVM内置,自动生成)
└── uvm_test_top ← 【test1类的实例】,实例名字固定叫uvm_test_top
└── c1 ← comp1类实例,实例名字"c1"
日志打印层次:
uvm_test_top.c1问题根源:
run_test()创建的顶层test实例名固定是uvm_test_top,不是test1。
二、怎么让日志层次里出现 test1?
核心原理:
不能修改run_test生成的顶层uvm_test_top名字,但是可以把test1作为子组件,让uvm_test_top下面包含test1实例
新的UVM树:
uvm_top
└── uvm_test_top ← 我们新建一个顶层wrapper test(wrapper_test)
└── test1 ← test1类实例,实例名字"test1"
└── c1 ← comp1实例
✅ 此时c1打印层次:uvm_test_top.test1.c1,日志里就能看到test1了!
注意:
uvm_test_top这个顶层名字无法彻底抹除 ,UVM强制规定;我们只能增加一层子组件命名为test1。
完整修改后可仿真代码
systemverilog
// 原来的comp1组件不变
class comp1 extends uvm_component;
`uvm_component_utils(comp1)
int val1 = 1;
string str1 = "null";
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
`uvm_info("SETVAL", $sformatf("val1 is %d before get", val1), UVM_LOW)
`uvm_info("SETVAL", $sformatf("str1 is %s before get", str1), UVM_LOW)
uvm_config_db#(int)::get(this, "", "val1", val1);
uvm_config_db#(string)::get(this, "", "str1", str1);
`uvm_info("SETVAL", $sformatf("val1 is %d after get", val1), UVM_LOW)
`uvm_info("SETVAL", $sformatf("str1 is %s after get", str1), UVM_LOW)
endfunction
endclass
// 原来的test1,保持不变
class test1 extends uvm_test;
`uvm_component_utils(test1)
comp1 c1;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
uvm_config_db#(int)::set(this, "c1", "val1", 100);
uvm_config_db#(string)::set(this, "c1", "str1", "comp1");
c1 = comp1::type_id::create("c1", this);
endfunction
endclass
// 新增:包装顶层wrapper_test,作为run_test的入口
class wrapper_test extends uvm_test;
`uvm_component_utils(wrapper_test)
test1 t1; // 句柄,实例名字叫"test1"
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
// 创建test1实例,实例名字字符串为 "test1",父是当前wrapper_test(this)
t1 = test1::type_id::create("test1", this);
endfunction
endclass
// 顶层启动,现在run_test跑wrapper_test
initial begin
run_test("wrapper_test");
end
📜 运行后的打印输出
UVM_INFO @ 0: uvm_test_top.test1.c1 [SETVAL] val1 is 1 before get
UVM_INFO @ 0: uvm_test_top.test1.c1 [SETVAL] str1 is null before get
UVM_INFO @ 0: uvm_test_top.test1.c1 [SETVAL] val1 is 100 after get
UVM_INFO @ 0: uvm_test_top.test1.c1 [SETVAL] str1 is comp1 after get
✅ 层次变成 uvm_test_top.test1.c1,日志中能看到test1。
三、备选方案(不改动UVM树,只打印类名信息,面试更常用)
不想嵌套一层组件,只想额外打印当前test的类名,层次依旧是uvm_test_top.c1,但是额外输出一行标记是test1。
systemverilog
class test1 extends uvm_test;
`uvm_component_utils(test1)
comp1 c1;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void start_of_simulation_phase(uvm_phase phase);
// 打印当前test类名
`uvm_info("TEST_ID", $sformatf("===== Current test class: %s =====", this.get_type_name()), UVM_MEDIUM)
endfunction
function void build_phase(uvm_phase phase);
uvm_config_db#(int)::set(this, "c1", "val1", 100);
uvm_config_db#(string)::set(this, "c1", "str1", "comp1");
c1 = comp1::type_id::create("c1", this);
endfunction
endclass
输出会多一行:
UVM_INFO @ 0: uvm_test_top [TEST_ID] ===== Current test class: test1 =====
UVM_INFO @ 0: uvm_test_top.c1 [SETVAL] val1 is 1 before get
...
工程里绝大多数项目用这个方案,不会去嵌套一层wrapper,因为没必要改动UVM组件树结构。
四、补充说明(面试考点)
- ❌ 无法彻底把
uvm_test_top从层次名删掉
run_test()启动的顶层test实例,UVM内部固定命名为uvm_test_top,这是uvm_root的硬编码,不能修改。 - 区分:
type_id::create("test1", this)第一个字符串参数 = 实例名字(出现在层次路径)uvm_component_utils(test1)的test1 = 类名,用于工厂查找,不出现在层次路径
五、两种方案对比
| 方案 | 日志层次 | 优缺点 |
|---|---|---|
| Wrapper嵌套 | uvm_test_top.test1.c1 |
层次里带test1,但是多一层组件,一般项目很少用 |
打印get_type_name() |
uvm_test_top.c1,额外打印一行类名 |
工程主流,代码简单,不改变UVM树结构 |
问题:能定义多个 test 类吗?
✅ 结论:完全可以定义多个 test 类
UVM 支持在同一个工程里写好多个 uvm_test 的子类,每个 test 都是独立的测试用例。
systemverilog
// 测试用例1
class test1 extends uvm_test;
`uvm_component_utils(test1)
// ...代码
endclass
// 测试用例2
class test2 extends uvm_test;
`uvm_component_utils(test2)
// ...代码
endclass
// 测试用例3
class test3 extends uvm_test;
`uvm_component_utils(test3)
// ...代码
endclass
关键点:
所有 test 类都必须继承
uvm_test,并且加上uvm_component_utils工厂注册宏,才能被run_test()调用。
怎么选择运行哪一个test?
run_test("字符串") 里面的字符串就是test的类名,UVM工厂根据名字去找对应的test类创建实例。
方式1:代码硬写(最简单,适合调试)
systemverilog
initial begin
run_test("test1"); // 跑test1
// run_test("test2"); // 想换用例,改这里字符串
end
方式2:仿真命令行传参(项目最常用!推荐)
代码里不写死,让仿真命令控制跑哪个用例:
systemverilog
initial begin
run_test(); // 不带参数,从命令行获取test名字
end
编译仿真的时候(vcs/questa)加 +UVM_TESTNAME=xxx
bash
# 跑test1
simv +UVM_TESTNAME=test1
# 跑test2
simv +UVM_TESTNAME=test2
这是企业项目标准做法:不用改代码、不用重新编译,直接命令行切换测试用例。
多个test的复用思路(面试高频)
多个test一般不会全部重写一遍,通常:
- 一个DUT环境(env)只写一套
- 多个test继承同一个基础test,只修改激励/配置:
systemverilog
// 基础测试,放公共环境创建
class base_test extends uvm_test;
`uvm_component_utils(base_test)
my_env env;
virtual function void build_phase(uvm_phase phase);
env = my_env::type_id::create("env", this);
endfunction
endclass
// test1:继承base_test,只改config_db配置
class test1 extends base_test;
`uvm_component_utils(test1)
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase); // 调用父类build,创建env
uvm_config_db#(int)::set(this, "env.agent", "data", 100);
endfunction
endclass
// test2:同样继承base_test,换一套参数
class test2 extends base_test;
`uvm_component_utils(test2)
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
uvm_config_db#(int)::set(this, "env.agent", "data", 200);
endfunction
endclass
⚠️ 重要限制(坑点)
- 一次仿真只能运行 1 个 test
run_test()在一次仿真中只能调用一次,启动之后UVM仿真跑完就结束,不能连续跑多个test。
如果想一次性批量跑多个test,需要脚本多次启动仿真进程。
-
所有test类名不能重名,
uvm_component_utils注册的名字不能冲突。 -
不管你有多少个test类,最终被run_test实例化的那个test实例名字永远叫:uvm_test_top
哪怕你跑test2,打印层次依然是
uvm_test_top.xxx,不会变成test2.xxx,这个和前面例子原理一样!
简单一句话总结
✅ 可以写很多test类,每个代表不同测试场景;
✅ 通过 +UVM_TESTNAME 命令行选择本次仿真跑哪一个;
❌ 一次仿真只能执行其中一个test。
问题:上述示例代码,如果多个 test 类,且每个里面都有 c1,那么每个 test 都是输出 uvm_test_top.c1 吗?
✅ 结论:是的!只要当前跑的这个test里创建了名字叫 "c1" 的comp1实例,打印路径一定是 uvm_test_top.c1
下面拆开讲清楚原理,区分两个概念:
- 类名 :
test1/test2/test3(类,图纸) - 实例名(instance name) :
uvm_test_top(UVM给当前激活test实例强制固定的名字)
举例子
systemverilog
class test1 extends uvm_test;
`uvm_component_utils(test1)
comp1 c1;
virtual function void build_phase(uvm_phase phase);
uvm_config_db#(int)::set(this,"c1","val1",100);
c1 = comp1::type_id::create("c1", this); // 实例名字字符串是"c1"
endfunction
endclass
class test2 extends uvm_test;
`uvm_component_utils(test2)
comp1 c1;
virtual function void build_phase(uvm_phase phase);
uvm_config_db#(int)::set(this,"c1","val1",200);
c1 = comp1::type_id::create("c1", this); // 同样实例名字"c1"
endfunction
endclass
-
仿真执行
simv +UVM_TESTNAME=test1UVM树:
uvm_top -> uvm_test_top(test1类的实例) -> c1打印路径:
uvm_test_top.c1 -
仿真执行
simv +UVM_TESTNAME=test2UVM树:
uvm_top -> uvm_test_top(test2类的实例) -> c1打印路径:依然是
uvm_test_top.c1
关键点:
不管你跑的是test1还是test2,本次仿真的顶层test实例名字永远叫
uvm_test_top,不是类名。只要在这个顶层test内部,
type_id::create("c1", this),子组件的实例名字就是c1,组合起来就是uvm_test_top.c1。
容易混淆的点
-
多个test不会同时在一次仿真里出现 。
一次仿真只能激活其中一个test ,只会生成一棵UVM树,只会有一个uvm_test_top。
test1和test2是两份图纸;跑test1就只用test1图纸造uvm_test_top;跑test2就换test2图纸造uvm_test_top。 -
c1是实例字符串名字 ,不是变量名。systemverilogc1 = comp1::type_id::create("my_c1", this);如果这里改成
"my_c1",打印就变成uvm_test_top.my_c1。 第一个参数create("xxx",parent)就是实例名,决定层次打印字符串。
那怎么区分日志,知道当前跑的是哪个test?
日志路径没法直接看出,一般有两种方法:
- 在test的build/start_of_simulation里打印类名:
systemverilog
virtual function void start_of_simulation_phase(uvm_phase phase);
`uvm_info("TEST_NAME", $sformatf("Now running test: %s", this.get_type_name()), UVM_MEDIUM)
endfunction
get_type_name() 拿到类名 ,跑test1打印test1,跑test2打印test2。
- 日志加命令行参数标记,或者分开保存log文件(项目最常用)
bash
# 跑test1,日志存test1.log
simv +UVM_TESTNAME=test1 > test1.log
# 跑test2,日志存test2.log
simv +UVM_TESTNAME=test2 > test2.log
额外面试小坑
问:同一个仿真中,能不能同时存在
uvm_test_top.c1和uvm_test_top2.c1?答:不能 ,一次仿真只能有一个uvm_test_top,
run_test()只能启动1个顶层test实例。
一句话总结
每次仿真只会选一个test执行;只要这个test里用create("c1",this)创建子组件,打印层次就是uvm_test_top.c1,和你的test类名叫test1/test2无关。
问题:uvm_config_db#(int)::set (this, "c1", "val1", 100); 的 this 是不是就是指向所处 class 的名称?
一句话先给结论
this 不是类名,this 是「当前这个类实例的句柄」(对象指针)。
this 指向当前正在执行这个函数的那个对象实体,而不是类的名字。
我们拿你这行代码:
systemverilog
uvm_config_db#(int)::set(this, "c1", "val1", 100);
放在 test1 的 build_phase 里面来拆解。
1. 区分 类(class) vs 实例(instance)
test1:类名(图纸,模板,静态的)uvm_test_top:实例名(用test1这个图纸new出来的真实对象)this:在test1的build_phase执行时,this就等于uvm_test_top这个对象的句柄。
当仿真跑 test1:
build_phase 是
uvm_test_top(test1类的实例)在执行,所以这里this == uvm_test_top。
2. uvm_config_db::set 的4个参数完整含义
systemverilog
uvm_config_db#(类型)::set( cntxt, inst_name, field_name, value );
- cntxt(上下文,就是你写的this):uvm_component句柄
用来作为路径查找的基准起点。 - inst_name:相对于cntxt的子组件实例路径字符串
"c1"意思:相对于cntxt,找名字叫c1的子组件。 - field_name:配置key名字
"val1" - value:要存的值
100
👉 拼接完整路径公式:
完整路径 = cntxt的全路径 + "." + inst_name
本例中:
- cntxt =
this(也就是uvm_test_top) - inst_name =
"c1"
拼接后完整路径:uvm_test_top.c1
key是val1,数据库就记录:路径uvm_test_top.c1,key val1,值100
然后在c1组件内部:
systemverilog
uvm_config_db#(int)::get(this, "", "val1", val1);
c1里面的this是c1实例本身;第二个参数是空字符串"",代表就用当前this的完整路径 ,也就是uvm_test_top.c1,刚好匹配set存入的路径,所以get拿到100。
3. 容易踩坑的点(面试高频)
- ❌
this不是字符串名字 ,它是对象句柄,不能直接打印出"test1"。this.get_full_name()可以拿到实例的完整层次字符串,本例返回"uvm_test_top"this.get_type_name()拿到类名 ,本例返回"test1"
举个小例子放在test1的build里:
systemverilog
`uvm_info("DEBUG",$sformatf("this fullname = %s", this.get_full_name()),UVM_LOW)
`uvm_info("DEBUG",$sformatf("this classname = %s", this.get_type_name()),UVM_LOW)
输出:
this fullname = uvm_test_top
this classname = test1
- 如果你把set的第一个参数写成
null,就代表绝对路径:
systemverilog
uvm_config_db#(int)::set(null, "uvm_test_top.c1", "val1", 100);
效果和 set(this,"c1","val1",100) 等价,但工程里优先用this相对路径,更灵活。
4. 形象类比
test1= 房子图纸uvm_test_top= 按照图纸盖好的真实房子this= 你站在这个房子里,手里拿着这个房子的钥匙(句柄)"c1"= 这个房子里面一个叫c1的房间
set(this, "c1", "val1",100)
以【我当前所在房子(this)】为起点,找到里面叫c1的房间,往房间里放一个叫val1,值100的配置。
补充:如果是在test2里面写一模一样的set语句
systemverilog
class test2 extends uvm_test;
//...
function void build_phase(uvm_phase phase);
uvm_config_db#(int)::set(this, "c1", "val1", 200);
c1 = comp1::type_id::create("c1", this);
endfunction
endclass
跑test2时:
this指向test2类的实例,实例名叫uvm_test_top- set拼接路径依旧是
uvm_test_top.c1 - c1里面get,拿到200
再次强调:不管this属于test1还是test2类,实例名永远叫uvm_test_top。
一句话总结
this 是当前正在执行函数的组件实例句柄(对象指针) ,不是类名;
uvm_config_db用this作为路径的基准起点,再拼接后面的字符串"c1",生成完整层次路径。