UVM中的uvm_config_db机制是一种用于在验证平台中传递配置参数和共享数据的标准化方法。它基于类型参数化的静态数据库,允许组件在不同层次之间安全、灵活地传递信息,而无需直接引用或依赖层次结构。
一、核心概念
uvm_config_db是一个参数化的静态类(模板类),用于存储和检索类型化的资源(如配置对象、虚拟接口等)。它构建在更通用的uvm_resource_db之上,但增加了层次化上下文的支持,使其更适合基于组件的配置。
二、使用场景
-
传递虚拟接口(virtual interface) :将模块(module)中实例化的物理接口传递给UVM类(如driver、monitor)。
cpp// 在top_tb中设置 initial begin uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.i_agt.*", "vif", input_if); end // 在driver中获取 function void build_phase(uvm_phase phase); if (!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif)) `uvm_fatal("NO_VIF", "Virtual interface not set") endfunction -
传递配置对象 :将配置参数(如地址宽度、使能标志等)封装在对象中,在组件间传递。
这里uvm_config_db传递的是一个句柄,#(T),括号内的T是句柄的类型
cpp// 定义配置类 class agent_config extends uvm_object; int master_id; bit is_active = UVM_ACTIVE; endclass // 在test中设置 function void build_phase(uvm_phase phase); agent_config cfg = new("cfg"); cfg.master_id = 0; uvm_config_db#(agent_config)::set(this, "env.i_agt", "cfg", cfg); endfunction // 在agent中获取 function void build_phase(uvm_phase phase); if (!uvm_config_db#(agent_config)::get(this, "", "cfg", cfg)) `uvm_fatal("CFGERR", "Config not found") endfunction -
控制组件行为 :例如使能/禁用检查(checks_enable)、覆盖率收集(coverage_enable)等。
cpp// 禁用特定monitor的检查 uvm_config_db#(int)::set(this, "masters[0].monitor", "checks_enable", 0); // 在monitor中使用 if (checks_enable) perform_transfer_checks(); -
设置默认序列(default_sequence) :为sequencer指定在特定phase启动的默认序列。
cppuvm_config_db#(uvm_object_wrapper)::set(this, "env.i_agt.sqr.main_phase", "default_sequence", my_sequence::type_id::get() );
三、uvm_config_db的四个参数
以 set 为例
cpp
static function void set(uvm_component cntxt,
string inst_name,
string field_name,
T value
);
1. uvm_component cntxt(上下文/起点)
第一个参数是一个uvm_component类型,决定了 set 或 get 动作在 UVM 层次结构中的参考起点。
- 在 Component 中:通常传入 this。这表示以当前组件为基准。
- 在顶层(如 Top module):由于不在类中,没有 this,通常传入 null。此时 UVM 会默认起点为 uvm_root::get()(即 uvm_test_top 的上一层), null等价于uvm_root::get()
- 还可以传入任何一个component的句柄
- 作用:它与第二个参数 inst_name 拼接在一起,组成完整的查找路径。
2. string inst_name(实例路径)
第二个参数是string字符串类型,这是相对于第一个参数 cntxt 的目标组件路径。
- 相对路径:如果 cntxt 是 this,且当前组件是 env,那么 inst_name 设为 "agent",则完整路径指向 env.agent。
- 绝对路径:如果 cntxt 是 null,第二个就必须使用绝对路径,必须以uvm_test_top作为开头,比如 "uvm_test_top.env.agent" 这样的完整路径。
- 通配符:支持 * 和 ?。例如 "uvm_test_top.*" 表示匹配顶层测试下的所有组件。
- 注意:在 get 时,inst_name 通常传空字符串 "",因为 cntxt 传入 this 已经精确代表了当前组件。
路径的名字是组件的句柄名还是调用new()函数时设置的实例名?
这是一个非常关键的细节:uvm_config_db 的第二个参数(inst_name)既不是类名,也不是你在代码里声明的变量名(句柄名),而是该组件在 new 函数中传递的那个字符串名字。
在 UVM 中,每个组件(component)都有两个身份:
- 句柄名(Handle Name):类声明的句柄的名字,例如 my_agent_h。
- 实例名(Instance Name):在 new(name, parent) 或 type_id::create(name, parent) 中传入的字符串 name。
uvm_config_db 查找路径时,认且只认"实例名"。
cpp
class my_env extends uvm_env;
my_agent my_agent_h; // 这里是变量名(句柄名)
virtual function void build_phase(uvm_phase phase);
// 这里第一个参数 "m_agt" 才是实例名,也是 config_db 认的名字
my_agent_h = my_agent::type_id::create("m_agt", this);
// 正确写法:使用实例名 "m_agt"
uvm_config_db#(int)::set(this, "m_agt", "is_active", 1);
// 错误写法:使用变量名 "my_agent_h"(除非实例名也叫这个)
// uvm_config_db#(int)::set(this, "my_agent_h", "is_active", 1);
endfunction
endclass
为什么要用"实例名"?
UVM 这样设计是为了实现层次结构的解耦:
- 变量名(Handle)只在当前的类作用域内有效。
- 实例名(Instance Name)构成了 UVM 树状结构(例如 uvm_test_top.env.m_agt)。
- config_db 通过这个全局可见的树状路径来查找目标,这样即使在没有对象指针的地方(比如顶层 Module),也能通过字符串路径准确地把配置"投递"给目标。
3. string field_name(字段名称)
第三个参数是string字符串类型,这是你在数据库中给存放的数据起的"名字"或"标签"。
- 匹配规则:set 和 get 时,这个字符串必须完全一致(大小写敏感)。
- 建议:为了避免拼写错误,大型项目中常使用宏或字符串常量来代替硬编码的字符串。
4. T value(数据值/句柄)
这是你真正想要传递的内容。
T (类型参数):这个 T 必须与 uvm_config_db#(T) 中声明的类型完全匹配。如果是变量,该变量的类型必须和T完全一致,如果是句柄,该句柄可以是T的子类,或者和T完全一致。
传递内容:
- 可以是 Virtual Interface(用于传递物理接口)。
- 可以是 Configuration Object 句柄(用于传递参数类)。
- 可以是 原始类型(int, string 等)。
参数匹配机制示意图
为了让 get 成功拿到值,必须满足以下四个条件的匹配:
| 条件 | 匹配要求 |
|---|---|
| 类型 (Type) | #(T) 在 set 和 get 时必须完全相同。 |
| 路径 (Path) | cntxt + inst_name 的拼接结果必须覆盖 get 调用的位置。 |
| 名称 (Name) | field_name 字符串必须完全一致。 |
| 时间 (Time) | set 必须在 get 执行之前完成(通常都在 build_phase)。 |
四、uvm_config_db优先级规则
uvm的uvm_config_db在使用时,如果有多个组件同时向同一个组件发起set,将会怎样?
当多个组件同时对同一个目标组件(同一变量)进行 uvm_config_db::set 操作时,最终生效的值取决于 UVM 树的层级结构(Hierarchical Precedence) 以及 执行时间的先后顺序。
1. 层级优先级规则(最重要)
UVM 规定,高层级(靠近 uvm_test_top)的组件优先级高于低层级组件。
情况一,test和env都在build_phase对driver进行了config_db设置
结果:uvm_test 设置的值会覆盖 uvm_env 的设置。
这背后的逻辑是:Test 层应该拥有对验证环境配置的最高控制权,能够覆盖环境内部默认的配置。
cpp
// --------------------------------------------------
// 1. 在低层级组件中 (env)
// --------------------------------------------------
class my_env extends uvm_env;
`uvm_component_utils(my_env)
...
function void build_phase(uvm_phase phase);
// env 尝试设置 is_active 为 ACTIVE
uvm_config_db#(uvm_active_passive_enum)::set(this, "agent_h*", "is_active", UVM_ACTIVE);
agent_h = my_agent::type_id::create("agent_h", this);
endfunction
endclass
// --------------------------------------------------
// 2. 在高层级组件中 (test)
// --------------------------------------------------
class my_test extends uvm_test;
`uvm_component_utils(my_test)
...
function void build_phase(uvm_phase phase);
// test 尝试设置同一个 agent 的 is_active 为 PASSIVE
// 注意:这里的 context 是 'this' (即 uvm_test_top)
uvm_config_db#(uvm_active_passive_enum)::set(this, "env_h.agent_h*", "is_active", UVM_PASSIVE);
env_h = my_env::type_id::create("env_h", this);
endfunction
endclass
情况二、test在较晚的main_phase进行了set,env在build_phase进行了set
结果:uvm_test 设置的值也会覆盖 uvm_env 的设置。
UVM 的 config_db 并不是简单的"覆盖",它在内部维护了一个资源列表,并根据 "Precedence" (优先级) 属性进行排序。
- 层级高的组件(Context 深度浅):拥有更高的 Precedence 值。
- 层级低的组件(Context 深度深):拥有较低的 Precedence 值。
当一个组件(如 Driver)调用 get 时,UVM 会在数据库中搜索匹配的条目。如果存在多个匹配项,它会忽略时间顺序,优先选择 Precedence 值最高(即层级最高)的那一个。
如果 uvm_test 和 uvm_env 同时给某个 driver 设置同一个变量,即便 uvm_env 的 set 动作在时间上更晚,uvm_test 设置的值也会覆盖 uvm_env 的设置。
2. 同一层级的先后顺序规则
如果发起 set 的多个组件处于 同一层级(例如两个不同的 Agent),或者是在同一个组件内多次 set:
- 后执行的 set 会覆盖先执行的 set。
- 在 UVM 的 build_phase 中,组件的创建顺序通常由代码书写顺序决定,因此时间上的"最后一次"操作将决定最终存入数据库的值。
cpp
class my_test extends uvm_test;
`uvm_component_utils(my_test)
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 第一次设置:尝试将 Agent 设为 PASSIVE
uvm_config_db#(uvm_active_passive_enum)::set(this, "env.agent*", "is_active", UVM_PASSIVE);
// 第二次设置:由于某种逻辑判断,再次将其设为 ACTIVE
// 在同一层级(都在 my_test 中),这次设置会覆盖上面的设置
uvm_config_db#(uvm_active_passive_enum)::set(this, "env.agent*", "is_active", UVM_ACTIVE);
endfunction
endclass
3. 配置数据库的内部机制
| 场景 | 结果 | 理由 |
|---|---|---|
| 高层 vs 低层 | 高层胜出 | UVM 设计初衷是让 Test 能够重载(Override)环境配置。 |
| 同层 A 晚于 B | A 胜出 | 同层遵循标准覆盖逻辑,即"后来者居上"。 |
| Context 为 null | 视为最高层 | 使用 null 作为 uvm_config_db::set 的第一个参数通常等同于在顶级发起。 |
五、uvm_config_db使用的注意事项
- set和get #(T)中的T类型必须完全一致
- 在实际工程中,极其不推荐在 build_phase 之外使用uvm_config_db。这会导致验证环境的行为变得难以预测(Race Condition),增加调试难度。建议set和get都在build_phase中完成
- 避免过度使用通配符,以提高可读性和可维护性。
- set和get必须成对出现,而且必须保证set在get之前完成
- 极其不建议让平级组件之间进行set和get,一般是高层级向低层级进行set
六、uvm_config_db出现错误后的调试方法
1. 常见问题
- 路径错误:inst_name拼写错误导致获取失败,使用check_config_usage()检测。
- 类型不匹配:set()和get()的类型参数不一致。
- 时机问题:在build_phase之后设置,目标组件可能已获取初始值。
2. 调试函数
- check_config_usage():检查是否有设置但未被获取的参数。
- print_config(1):递归打印所有可见配置资源。
- 添加命令行参数 +UVM_CONFIG_DB_TRACE
七、uvm_config_db与uvm_resource_db的关系
uvm_config_db基于uvm_resource_db,但增加了层次化语义。
使用原则:
- 需要层次化上下文时(如配置特定agent的子组件),使用uvm_config_db。
- 全局共享资源(无层次依赖)时,可使用uvm_resource_db。
八、与过时方法的对比
set_config_int/get_config_int等函数在UVM 1.2中已过时,推荐使用uvm_config_db。
uvm_config_db支持更广泛的数据类型(包括用户自定义对象)。
问题1
uvm_config_db中,如果传递的是一个句柄,uvm_config_db#(T),括号内T所指代的类型必须和句柄的类型完全一致吗?可以是它的父类吗?
在 UVM 中,uvm_config_db#(T)::set 和 uvm_config_db#(T)::get 中的 #(T) 是一个参数化类。为了让 set 和 get 成功匹配,**T 的类型必须在发送端和接收端完全一致。**但传递的实际对象(Instance)可以是子类句柄。
1. 为什么类型参数 #(T) 必须一致?
uvm_config_db 实际上是一个基于类型的查找表。当调用 set 时,UVM 会根据"类型 + 路径 + 名字"作为索引来存储值。如果在 set 时使用 uvm_config_db#(Parent)::set(...),但在get 时使用 uvm_config_db#(Child)::get(...)。即使 Child 继承自 Parent,UVM 也会认为这是两个完全不同的查找表,导致 get 失败。
2. 句柄(实例)可以是父类或子类吗?
虽然 #(T) 的类型必须一致,但由于 SystemVerilog 支持多态(Polymorphism),你可以利用这一点:
情况 A:传递子类实例给父类容器(常用)
这是合法的,也是推荐的做法。你可以定义配置数据库的类型为"父类",但实际传递一个"子类"的句柄。
cpp
// 假设类定义:class MyChild extends MyParent;
// 发送端:类型参数用父类,但传入子类实例 child_inst
uvm_config_db#(MyParent)::set(this, "*", "cfg", child_inst);
// 接收端:类型参数必须也是父类
MyParent p_inst;
if (uvm_config_db#(MyParent)::get(this, "", "cfg", p_inst)) begin
// p_inst 实际指向的是子类对象
end
情况 B:传递父类实例给子类容器(不合法)
这违反了 SystemVerilog 的基本语法。你不能将一个父类句柄赋值给子类变量(除非进行 $cast 检查,但在 config_db 的接口层面直接这样做会报错)。
3. 核心结论与建议
| 项目 | 要求 |
|---|---|
#(T) 类型参数 |
必须完全一致 。发送端是 base_cfg,接收端也必须是 base_cfg。 |
| 实际传递的句柄 | 可以是 T 的子类。这利用了 OOP 的多态性,增强了代码的灵活性。 |
通常建议将 uvm_config_db#(T) 中的 T 设置为基类(Base Class)。这样,以后如果派生出不同的子类配置,无需修改 get 的代码,依然可以成功传递。
注意: 如果你通过父类类型 get 到了子类句柄,在接收端需要使用子类特有的成员时,记得使用 $cast 进行类型转换。
问题2
uvm_config_db可以在哪里使用,哪里不能使用?
1. 哪里可以使用?
uvm_config_db 本质上是一个静态查找表,只要在 UVM 环境启动后(甚至在 run_test 之前),几乎所有 SV 作用域都可以调用它,但用法略有不同:
A. 在 uvm_component 中(最常见)
- 用途:在 build_phase 中传递配置对象(Config Object)或接口。
- 特点:可以使用 this 作为第一个参数(context),利用 UVM 的树状层级进行过滤。
- 示例:uvm_config_db#(cfg)::get(this, "", "my_cfg", m_cfg);
B. 在 Top Module (HDL 顶层) 中
- 用途:主要用于将 Virtual Interface (vif) 传递给 UVM 世界。
- 注意:由于 Module 不是类,不能使用 this,必须使用 null 或 uvm_root::get() 作为 context。
- 示例:uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.*", "vif", if_inst);
C. 在 uvm_sequence 中
- 用途:Sequence 需要根据配置(如发包数量、模式)动态调整行为时。
- 注意:Sequence 没有层级(它不是 component),get 时建议使用 p_sequencer 或 null 作为 context。
2. 哪里不建议/不能使用?
A. 严禁在"热点"循环(Inner Loops)中使用
- 原因:config_db::get 涉及大量的字符串匹配和底层资源池搜索,开销非常大。
- 后果:如果在 Driver 的每发一个包的循环里都调一次 get,仿真速度会大幅下降。
- 正确做法:在 build_phase 或 start_of_simulation_phase 中 get 一次,存入类成员变量。
B. 不建议在 uvm_object (非 Sequence) 中随意使用
原因:像 uvm_transaction(数据包)这种短命的、大量的对象,如果每个都去查数据库,性能会爆炸。而且 uvm_object 没有 path,很难进行精确的层级匹配。
C. 避免跨级"透传" (Skip-level Access)
- 现象:Test 直接给底层的 Driver 设置参数,跳过了 Env 和 Agent。
- 建议:虽然功能可行,但破坏了组件的封装性。推荐做法是:Test -> Env -> Agent -> Driver 逐级传递或传递一个统一的 Config Object。
3. 使用场景总结表
| 场景 | 是否推荐 | 典型用途 | Context 参数 (第一个) |
|---|---|---|---|
| Top Module | ✅ 必须 | 传递 virtual interface |
null |
| uvm_test | ✅ 推荐 | 下发全局配置、覆盖子组件配置 | this |
| uvm_component | ✅ 推荐 | 获取配置、向下传参 | this |
| uvm_sequence | ⚠️ 慎用 | 获取 Sequencer 相关的配置 | p_sequencer 或 null |
| Transaction | ❌ 不推荐 | 传递属性值(开销大) | N/A |
| High-speed loop | 🚫 禁止 | 实时修改驱动行为 | N/A |
问题3
当env在其build_phase通过uvm_config_db向agent传递一个类的句柄(比如cfg时),然后env在start_of_simulation_phase,修改了这个cfg,那么agent拿到的是哪个cfg?
这是一个非常经典的关于 "引用传递(Pass by Reference)" 的问题。
简短的答案是:Agent 拿到的是修改后的内容。
这是因为当你通过 uvm_config_db 传递一个类(class)的句柄时,你传递的是指向内存中那个对象的"地址",而不是对象的副本。
1. 为什么是修改后的内容?
在 SystemVerilog 中,类是引用类型。我们可以用下面的逻辑来理解这个过程:
- Build Phase (env):env 创建了一个 cfg 对象(假设地址在 0x1234),并把这个地址放进了 config_db 盒子里。
- Build Phase (agent):agent 从盒子拿到了地址 0x1234,并让自己的 m_cfg 句柄也指向这个地址。
- Start of Simulation Phase (env):env 通过自己的句柄修改了地址 0x1234 里的数据。
- Run Phase (agent):当 agent 使用 m_cfg 读取数据时,它直接访问地址 0x1234,此时拿到的自然是 env 修改后的新值。
2. 关键点:句柄与对象的区别
这里有一个非常重要的细节:你是在修改"对象的内容",还是在让句柄指向"新的对象"?
情况 A:修改成员变量(Agent 能看到更新)
cpp
// env.sv
function void start_of_simulation_phase(uvm_phase phase);
cfg.enable = 1; // 修改了同一个对象的内容
endfunction
情况 B:重新 new 了一个对象(Agent 看不到更新!)
cpp
// env.sv
function void start_of_simulation_phase(uvm_phase phase);
cfg = new(); // 此时 env 的句柄指向了新地址 0x5555
cfg.enable = 1;
endfunction
这种情况下,agent 手里的句柄仍然指向旧的地址 0x1234。由于 agent 的 get 动作发生在 build_phase(已经结束了),它不会自动去同步 env 后来指向的新地址。
3. UVM 仿真相序(Phasing)的影响
在 UVM 中,build_phase 是**自顶向下(Top-down)**执行的:
- env::build_phase 先执行,完成 set。
- agent::build_phase 后执行,完成 get。
而 start_of_simulation_phase 是在所有组件的 build、connect 等阶段都完成后才执行的。因此,Agent 绝对有足够的时间在开始工作前拿到那个句柄。
4. 最佳实践建议
虽然这种"动态修改"在技术上是可行的,但为了代码的健壮性,建议:
- 配置锁定:如果某些配置不希望在运行中被修改,可以在 cfg 对象中增加一个 is_locked 标志位。
- 避免后期修改:尽量在 build_phase 结束前完成所有关键配置。如果必须在运行中修改,请确保 agent 的逻辑是实时读取句柄内容的,而不是在 build_phase 拿出来后存到了一个本地的普通变量(如 int)里。
5. 如果传递的是一个变量,而不是句柄呢?
如果传递的不是类句柄(Class Handle),而是原始数据类型(如 int, bit, logic, struct 等),情况会完全不同。
简短答案:Agent 拿到的是修改前的值(旧值)。
这是因为原始数据类型在 SystemVerilog 中是**按值传递(Pass by Value)**的。一旦 get 完成,env 和 agent 之间就没有任何联系了。
1. 为什么是旧值?(值的拷贝机制)
当你使用 uvm_config_db#(int)::set 时,UVM 会在内部的数据库里开辟一块空间,复制一份该变量当前的值存进去。
- Build Phase (env):执行 set。此时 int a = 10。UVM 数据库里存入了一个数字 10。
- Build Phase (agent):执行 get。agent 声明一个变量 b,从数据库里把数字 10 拿出来赋值给 b。此时 b = 10。
- Start of Simulation Phase (env):修改变量 a = 20。
- 结果:agent 里的 b 依然是 10。
2. 核心区别对比
我们可以把 uvm_config_db 想象成一个寄存物柜:
| 数据类型 | 行为类比 | 修改后的影响 |
|---|---|---|
| 类句柄 (Class Handle) | 你把柜子的钥匙给了 Agent。你随时可以进柜子换里面的东西,Agent 再次打开柜子时看到的是新东西。 | Agent 同步更新 |
| 原始变量 (int, bit等) | 你把文件的复印件放进了柜子。Agent 拿走后,你在家撕掉原件重新写,Agent 手里的复印件不会变。 | Agent 保持旧值 |