4. UVM工厂与配置机制在芯片级环境中的应用
在模块级验证中,factory override 往往只影响一两个 agent 或 sequence;config_db 的 set/get 也大多集中在两三层路径内。但到了芯片级环境,DUT 由多个子系统、多个 IP、多个时钟域和多种工作模式组成,验证环境本身也变成一棵庞大的组件树。此时工厂与配置机制不再只是“语法工具”,而是决定环境能否被复用、场景能否被快速切换、回归能否被统一管理的核心基础设施。本节围绕三个问题展开:factory override 如何跨模块传递、config_db 如何做层次化配置、芯片级多场景配置如何组织。
一、factory override 的跨模块传递
UVM factory 的本质是“按类型名创建对象,并在创建时用注册表替换实际类型”。在芯片级环境中,override 的传递遵循两条规则:按类型匹配和按路径匹配。按类型匹配(set_type_override_by_type)会作用于整个环境树中所有该类型的实例;按实例路径匹配(set_inst_override_by_type)只作用于指定层次路径下的实例。理解这两条规则,是控制 override 影响范围的关键。
考虑一个典型芯片级场景:SoC 中包含 CPU 子系统、DDR 控制器、PCIe 控制器和多个外设。基础环境为每个 IP 例化一个 ip_agent,其 driver 为 ip_driver。现在需要为 DDR 子系统注入错误激励,只替换 DDR 路径下的 driver,而不影响 PCIe 和外设。正确做法是:
// 只替换 DDR 路径下的 driver
ddr_driver_bad::type_id::set_inst_override(
ddr_driver_bad::get_type(),
"uvm_test_top.env.ddr_agent.drv"
);
// 若写成 type override,则所有 ip_agent 的 driver 都会被替换
// ip_driver_bad::type_id::set_type_override(ip_driver_bad::get_type());
这里有一个容易踩的坑:set_inst_override 的路径必须与组件在 build 阶段的实际层次路径完全一致,且 override 必须在目标组件创建之前执行。芯片级环境中组件创建顺序由 build 阶段自顶向下决定,因此 override 通常放在 test 的 build_phase 最前面,或放在更早的 uvm_config_db 配置阶段。若在 env 内部才设置 override,而 driver 已经在父组件 build 时创建,则 override 不会生效。
跨模块传递的另一个要点是override 的叠加与优先级。当同一类型同时存在 type override 和 inst override 时,inst override 优先;当存在多层继承关系时,factory 会沿继承链查找最匹配的替换类型。芯片级环境中常见做法是:在 base test 中设置全局默认 override,在具体场景 test 中通过 inst override 覆盖特定子系统,从而形成“全局默认 + 局部定制”的层次化替换策略。
二、config_db 的层次化配置
config_db 解决的是“值从哪里来、到哪里去”的问题。芯片级环境中,配置项数量多、来源杂、作用域不同,必须按层次组织。UVM 的 config_db 采用“路径 + 字段名”的键值模型,set 时指定目标路径,get 时按当前组件路径向上查找。理解这一点,就能设计出清晰的配置层次。
推荐的做法是按作用域分层设置:
- 全局配置:在 test 的 build_phase 中,用
"*"或"uvm_test_top.*"设置所有组件共享的参数,如超时时间、覆盖率开关、日志级别。 - 子系统配置:用
"uvm_test_top.env.ddr_agent.*"设置 DDR 子系统的专属参数,如地址映射、时序参数、错误注入使能。 - 组件级配置:用精确路径
"uvm_test_top.env.ddr_agent.drv"设置 driver 的细粒度参数,如驱动强度、延迟模式。
一个常见的芯片级配置示例如下:
// 全局:所有组件默认关闭详细日志
uvm_config_db#(int)::set(this, "*", "verbosity", UVM_LOW);
// 子系统:DDR 使用 4 通道、地址位宽 32
uvm_config_db#(int)::set(this, "env.ddr_agent.*", "num_channels", 4);
uvm_config_db#(int)::set(this, "env.ddr_agent.*", "addr_width", 32);
// 组件级:DDR driver 注入单比特错误
uvm_config_db#(bit)::set(this, "env.ddr_agent.drv", "inject_single_bit", 1);
// 在 driver 中获取
if (!uvm_config_db#(bit)::get(this, "", "inject_single_bit", inject_single_bit))
`uvm_fatal("CFG", "inject_single_bit not set")
这里的关键是get 的路径参数。当 driver 调用 get(this, "", "inject_single_bit", ...) 时,UVM 会从 this 的完整路径开始,逐级向上查找匹配的 set。因此,set 的路径越具体,优先级越高;set 的路径越宽泛,作为默认值兜底。芯片级环境中,建议在 base test 中设置全局默认值,在具体场景 test 中覆盖子系统或组件级配置,形成“默认 + 覆盖”的配置链。
三、芯片级多场景配置管理
芯片级回归通常需要覆盖多种工作模式:正常启动、低功耗模式、错误注入、性能压测、安全启动等。每种模式对应一组配置参数和一组 override。如果每个场景都写一个独立的 test,代码会迅速膨胀;更好的做法是用配置对象 + 场景枚举来组织。
推荐定义一个 chip_cfg 类,继承自 uvm_object,把所有可配置项集中管理:
class chip_cfg extends uvm_object;
`uvm_object_utils(chip_cfg)
rand int num_channels;
rand int addr_width;
rand bit inject_single_bit;
rand bit low_power_mode;
rand int timeout_cycles;
constraint c_default {
num_channels inside {[1:8]};
addr_width inside {32, 64};
timeout_cycles inside {[1000:100000]};
}
endclass
然后在 test 中根据场景名生成不同的 cfg 实例,并统一 set 到 config_db:
class chip_base_test extends uvm_test;
chip_cfg cfg;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
cfg = chip_cfg::type_id::create("cfg");
case (scenario_name)
"normal": cfg.randomize() with { low_power_mode == 0; inject_single_bit == 0; };
"low_power": cfg.randomize() with { low_power_mode == 1; num_channels == 1; };
"error_inj": cfg.randomize() with { inject_single_bit == 1; };
"stress": cfg.randomize() with { num_channels == 8; timeout_cycles == 100000; };
endcase
uvm_config_db#(chip_cfg)::set(this, "*", "chip_cfg", cfg);
endfunction
endclass
这样,所有组件只需要从 config_db 中 get 同一个 chip_cfg 对象,就能获得当前场景的全部配置。场景切换只需改变 scenario_name,无需修改任何组件代码。对于 override,也可以在 test 中根据场景动态设置:例如 error_inj 场景下,用 inst override 把 DDR driver 替换为错误注入版本;stress 场景下,用 type override 把所有 monitor 替换为高吞吐版本。
configure_environment() 函数中,按“全局 → 子系统 → 组件”的顺序设置,并在函数末尾打印一份配置摘要,便于回归日志中快速确认当前场景的实际配置。
最后需要强调的是,factory override 和 config_db 不是孤立的。override 决定“用哪个类”,config_db 决定“用什么参数”。在芯片级多场景管理中,两者通常配合使用:场景 test 先通过 config_db 下发配置对象,再根据配置对象中的字段决定是否设置 override。这样,场景定义、配置参数和类型替换三者形成一条清晰的链路,既保证了环境的可复用性,也让回归结果更容易追溯和复现。