UVM 面试高频:uvm_test_top 与 uvm_config_db 路径机制全解析

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

二、执行流程拆解

  1. 执行run_test("test1"),UVM自动创建test1类的实例 ,实例的名字固定叫uvm_test_top,不是test1!
  2. 进入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
  3. 接下来执行子组件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

补充小考点(面试常问)

  1. 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
  1. 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组件树结构。

四、补充说明(面试考点)

  1. ❌ 无法彻底把uvm_test_top从层次名删掉
    run_test()启动的顶层test实例,UVM内部固定命名为uvm_test_top,这是uvm_root的硬编码,不能修改。
  2. 区分:
  • 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一般不会全部重写一遍,通常:

  1. 一个DUT环境(env)只写一套
  2. 多个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. 一次仿真只能运行 1 个 test
    run_test() 在一次仿真中只能调用一次,启动之后UVM仿真跑完就结束,不能连续跑多个test。

如果想一次性批量跑多个test,需要脚本多次启动仿真进程。

  1. 所有test类名不能重名,uvm_component_utils 注册的名字不能冲突。

  2. 不管你有多少个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

下面拆开讲清楚原理,区分两个概念:

  1. 类名 :test1 / test2 / test3(类,图纸)
  2. 实例名(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=test1

    UVM树:uvm_top -> uvm_test_top(test1类的实例) -> c1

    打印路径:uvm_test_top.c1

  • 仿真执行 simv +UVM_TESTNAME=test2

    UVM树: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。

容易混淆的点

  1. 多个test不会同时在一次仿真里出现 。
    一次仿真只能激活其中一个test ,只会生成一棵UVM树,只会有一个uvm_test_top。
    test1和test2是两份图纸;跑test1就只用test1图纸造uvm_test_top;跑test2就换test2图纸造uvm_test_top。

  2. c1 是实例字符串名字 ,不是变量名。

    systemverilog 复制代码
    c1 = comp1::type_id::create("my_c1", this);

    如果这里改成 "my_c1",打印就变成 uvm_test_top.my_c1。 第一个参数create("xxx",parent)就是实例名,决定层次打印字符串。

那怎么区分日志,知道当前跑的是哪个test?

日志路径没法直接看出,一般有两种方法:

  1. 在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。

  1. 日志加命令行参数标记,或者分开保存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 );
  1. cntxt(上下文,就是你写的this):uvm_component句柄
    用来作为路径查找的基准起点。
  2. inst_name:相对于cntxt的子组件实例路径字符串
    "c1" 意思:相对于cntxt,找名字叫c1的子组件。
  3. field_name:配置key名字 "val1"
  4. 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. 容易踩坑的点(面试高频)

  1. ❌ 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
  1. 如果你把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",生成完整层次路径。

相关推荐
极小狐1 小时前
MR 里测试摘要空白、覆盖率不显示?JUnit 与覆盖率配置 9 步排查
数据库·ci/cd·junit·单元测试·mr·devops·代码覆盖率
silvamux_1 小时前
MiniMax API 调用教程:兼容 OpenAI 写法
java·数据库·elasticsearch
zh73141 小时前
sqlforge一个SQL 格式化与方言转换工具
数据库·sql
染指11101 小时前
145.Agent-LangGraph基础概念和安装和简单代码示例
java·服务器·数据库·langchain·agents
太理摆烂哥2 小时前
MySQL表的操作
数据库
lbb 小魔仙2 小时前
时序数据库是什么?2026 年五款工业时序数据库横评与场景解析
数据库·时序数据库
零基础1232 小时前
权限管理怎么做?从“资格”与“空间”双维度设计一套可落地的权限体系
java·服务器·数据库
小小龙学IT2 小时前
PostgreSQL 服务端机制与 SQL 实战深度解析:从 MVCC 到查询优化器
数据库·sql·postgresql
l1t2 小时前
DeepSeek总结的DuckDB 从 JSON 字符串到类型化列
数据库·sql·json