UVM 基础

第 5 章 · 共 10 章

config_db 与虚接口传递

为什么 uvm_component 固定的 factory 构造函数签名排除了把虚接口作为构造函数参数传递的可能,以及 uvm_config_db::set()/get() 解决这个问题的完整机制——context、实例路径、通配符,还有为什么第 2 章 build_phase 的执行顺序正是它能生效的关键。

第 3 章已经用 uvm_config_db::set()/get()vif 从顶层 module 传进了 driver,并承诺以后会解释它为什么有效。这一章就是那个承诺的兑现——之所以放在第 4 章之后立刻讲,而不是拖到系列更后面,是因为它依赖的东西已经全部就位:第 2 章的组件树和 build_phase 执行顺序。这里完全不需要用到 factory(第 6 章)或 sequence(第 7 章),所以没有理由让第 3 章埋下的这个欠条一直等到那两章才还。

为什么构造函数参数这条路走不通

SV 基础系列 capstone 里的 driver 类有一个自定义的构造函数:new(virtual mux2_if.tb_mp vif, mailbox #(mux_txn) mbx)。这行得通,是因为它是一个纯 SystemVerilog 类,用一个完全由作者自己掌控的 new(...) 调用来构造。

uvm_component 没有这个自由。第 3 章用 my_driver::type_id::create("drv", this) 而不是 new(...) 来构造 driver——这正是 UVM 的 factory(完整解释在第 6 章),它内部的工作方式是对最终实际被构造出来的那个类型调用 new(name, parent),而且严格只有这两个参数。这个固定签名里没有空间再塞一个像 vif 这样的第三个参数。这不是风格偏好——而是一个硬性约束:任何想要支持 factory 替换(override)的 uvm_component,都必须保持标准的双参数构造函数,这就意味着像 virtual interface 这样的配置数据必须走别的路径。

uvm_config_db 就是这条别的路径:一张全局的、关联式的查找表,任何组件都可以往里发布,也可以从里查询,完全独立于构造函数之外。

set() 与 get():四个参数

两个调用的形状是一样的:

uvm_config_db#(T)::set(cntxt, inst_path, field_name, value);
uvm_config_db#(T)::get(cntxt, inst_path, field_name, value);  // get() 返回一个 bit
  • cntxt——一个组件,作为 inst_path 的参照点。在组件内部,这几乎总是 this。从一个纯 module 里调用(它根本不是一个组件——比如第 3 章顶层的 initial 块),没有组件可用,所以传 null,意思是"把 inst_path 当作从层次结构最顶端算起的绝对路径来解析"。
  • inst_path——一个字符串,描述这条记录适用于哪些组件,相对于 cntxt 而言。支持 */? 通配符。空字符串 "" 意味着"就是 cntxt 自己,别的都不算"。
  • field_name——一个你自己选的任意字符串键,比如 "vif"。只有当这个字符串在 set()get() 两边完全一致时才会匹配——这里如果打错字,效果和从没调用过 set() 完全一样分辨不出来,而这正是第 3 章那个 `uvm_fatal 检查专门用来抓住的失败情形。
  • value——数据本身(get() 通过引用写入它,成功返回 1,没有匹配上返回 0)。

解码第 3 章的调用

// 顶层 module -- 不是组件,所以 cntxt 是 null
uvm_config_db#(virtual mux2_if.tb_mp)::set(null, "*", "vif", bus_if);

null 意味着"从最顶端算起的绝对路径"。"*" 是一个通配符,匹配任意深度的任意组件路径——所以这一次调用就能覆盖任何现在或将来会查询字段名 "vif" 的组件,不管它在层次结构里有多深。

// my_driver 的 build_phase 内部 -- cntxt 就是这个组件自己
uvm_config_db#(virtual mux2_if.tb_mp)::get(this, "", "vif", vif);

this 加上一个空的 inst_path"")意思是"在我自己确切的路径上,有没有字段 "vif" 的值可见?"——因为 set() 调用用的是 "*" 通配符,答案是有,不管 my_driver 实际在树里的哪个位置。

为什么这样能扩展,而构造函数参数不能

假设 driver 不是 test 的直接子组件,而是深了三层——my_testmy_envmy_agentmy_driver。如果用构造函数参数,vif 就必须一层一层地穿过每个中间层的构造函数,即使 my_envmy_agent 自己根本用不到它——纯粹是管道传递,而且只要这些层里任何一层要在别处以不同的深度被复用,这套管道就会崩掉。用 config_db 的话,什么都不用变:顶层那条带 "*" 通配符的 set() 依然能到达 my_driver,中间那些组件完全不需要知道 vif 这个东西的存在。

这之所以安全,靠的是第 2 章的 phase 执行顺序:build_phase自顶向下执行的,所以顶层 module 里的 set()——它甚至发生在 run_test() 启动整个 phase 引擎之前——一定会在任何深度的任何组件的 build_phase 调用 get() 之前就已经存在。

再多知道一点,虽然这个系列的例子从来不会真正碰到它:如果同一个 get() 可能被不止一个 set() 匹配上,config_db 会优先选择更具体的 inst_path(精确路径胜过通配符),在同样具体的情况下,最后执行的那次 set() 胜出。读别人的 testbench 时知道这一点会有用;这里不需要专门为它设计什么。

小结

  • uvm_component 通过 factory 构造出来的构造函数固定是 (name, parent)——没有空间再塞额外的配置参数比如 virtual interface,这正是 config_db 存在的真正原因。
  • set(cntxt, inst_path, field_name, value) / get(cntxt, inst_path, field_name, value)cntxt 是参照点(组件内部用 this,纯 module 里用 null),inst_path 是相对 cntxt 的路径(支持 */? 通配符,"" 表示就是 cntxt 自己),field_name 是一个字符串键,set()get() 两边必须完全一致。
  • 顶层 module 里带 "*" 通配符的 set(),能覆盖任何深度、任何查询这个字段名的组件,完全不需要把这个值一层层穿过任何中间组件的构造函数。
  • 这一切都依赖第 2 章那条保证:build_phase 自顶向下执行——顶层的 set()(甚至在 run_test() 启动之前)一定会在任何组件的 build_phase 调用 get() 之前就已经存在。

为什么虚接口不能像 SV capstone 里那个纯类 driver 一样,直接作为 uvm_component 构造函数的第三个参数传进去?

在 uvm_config_db#(virtual mux2_if.tb_mp)::set(null, '*', 'vif', bus_if); 里,'*' 这个参数到底做了什么?

某个 driver 调用 uvm_config_db#(virtual mux2_if.tb_mp)::get(this, '', 'VIF', vif)(大写 VIF),但顶层 module 调用的是 set(null, '*', 'vif', bus_if)(小写 vif)。会发生什么?

为什么顶层 module 的 uvm_config_db::set() 调用必须在 run_test() 之前发生,具体是靠第 2 章的哪个 phase 机制保证的?