ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

uvm_pool实战指南:UVM跨组件高频数据共享与全局池避坑

uvm_pool实战指南:UVM跨组件高频数据共享与全局池避坑 以前搭验证环境的时候最让我头疼的一件事就是参考模型算出来的中间结果记分板要用覆盖率模型要用有时候两个sequence还要读一眼。你们一般怎么传要么从test一层层往下传句柄要么在某个类里塞个static变量再要么就是把数据灌进config_db。前两种到后期都很难维护config_db本来也不是为高频读写设计的。后来我发现UVM里有个不起眼的类叫uvm_pool专门干这个事。这篇内容适合两类人一类是刚接触UVM、想知道除了config_db还有什么办法做数据共享的同学另一类是已经在用pool但被一些隐藏规则坑过、想搞清楚底层机制的工程师。我会把源码、全局池机制、实际用法和踩过的坑一起讲透。1. uvm_pool到底解决了什么共享句柄的归属问题1.1 表面上它是一个关联数组实际上是一个公共储物柜uvm_pool的定义非常简单它是一个参数化类内部核心就是一个关联数组class uvm_pool #(type Tuvm_object) extends uvm_object; protected T pool[string]; // ... endclass就这么点东西。pool[string]是一个以字符串为索引、以类型T对象为值的关联数组。你可以把它想象成一个公共储物柜每个格子有一个字符串标签里面存放一个对象句柄。任何人只要拿到这个柜子的访问权限就能按标签取放东西。但它的价值不在数据结构本身而在于UVM给它配了一套全局访问的机制。UVM里很多类的实例都必须通过create和build_phase才能被大家拿到uvm_pool则绕过了这套繁琐流程提供了一个静态方法让你在任何组件里直接拿到同一个池子实例。这就解决了验证环境里最现实的问题一个对象到底归谁管参考模型生成的数据test要配置scoreboard要校验agent里的monitor可能也要看一眼如果每个组件都自己存一份句柄副本代码里到处都是传递依赖后期改一个参数要找遍全工程。1.2 核心API只有五六个别被文档绕晕uvm_pool的接口非常精简常用的就这几个方法作用注意点get(string key)按key取对象key不存在时返回nullset(string key, T item)放入或覆盖对象覆盖时旧对象失去引用deallocate(string key)删除指定key的对象删除后get会返回nullexists(string key)检查key是否存在返回1/0num()返回池中条目数调试时很好用get_global_pool()获取全局池池未创建时会报fatalget_global_pool_alloc()获取全局池不存在则创建推荐使用这个看到这里你应该能理解它的定位了uvm_pool本质上就是给验证环境提供一个按字符串索引的对象仓库。它解决的痛点是对象句柄的归属和传递而不是配置参数的层级覆盖这跟uvm_config_db有本质区别。1.3 和uvm_config_db的边界一个传配置一个传对象很多人会问uvm_config_db不也能共享数据吗确实能80%的跨组件数据传递用config_db就足够了。但config_db的设计目标是配置参数下发它带有一套复杂的层次路径匹配机制set和get之间要经过层层查找在高频读写场景下性能和便利性都不理想。我的判断标准很简单如果是配置项int、string、bit、interface、或者一个只读的配置对象走config_db如果是需要被多个组件同时读写、频繁更新内容的对象走uvm_pool。比如参考模型每个周期更新一次的预测数据你不可能往config_db里每秒set一万次再get一万次这种场景就是pool的主场。2. 源码里的全局池机制get_global_pool与get_global_pool_alloc的差别2.1 全局池靠静态变量实现每个类型组合只有一个uvm_pool之所以能做到全局共享秘密在于它内部有一个静态变量class uvm_pool #(type Tuvm_object) extends uvm_object; local static this_type m_global_pool; // ... endclass这个m_global_pool是静态的意味着无论你在环境的哪个角落调用uvm_pool#(my_type)::get_global_pool()拿到的都是同一个对象。这里有个关键点它是按参数类型T分别存储的。也就是说uvm_pool#(string)的全局池和uvm_pool#(int)的全局池是两个完全独立的对象各自有各自的m_global_pool静态变量。这种设计的好处是类型隔离。不同类型的数据互不干扰你不用手动加互斥锁语言层面就帮你隔开了。2.2 两个获取全局池的方法一个保守一个激进源码里有这样两个静态方法很多人会用混static function this_type get_global_pool(); if (m_global_pool null) uvm_report_fatal(POOL, Global pool not allocated, UVM_NONE); return m_global_pool; endfunction static function this_type get_global_pool_alloc(); if (m_global_pool null) m_global_pool new(global_pool); return m_global_pool; endfunction注意看区别get_global_pool()拿到的是必须已经存在的池子如果还没有任何代码创建过它直接调用会触发fatal报错。而get_global_pool_alloc()会在池子不存在时自动创建一个名字里的alloc就是分配的意思。所以在实际工程里除非你能保证在此之前一定有人创建过这个类型的池子否则一律用get_global_pool_alloc()就对了。我见过同事因为在某个组件里调了get_global_pool()而环境直接崩掉的案例排查半天发现是调用顺序问题——别的组件还没来得及先创建池子。这个坑很简单但也很容易踩。2.3 局部池是全局池的隔离备选除了全局池还有一个get_local_pool()方法static function this_type get_local_pool(); this_type pool new(local_pool); return pool; endfunction这个方法每次调用都会返回一个全新的池子实例不会和任何人共享。它的价值在于隔离你可以创建一个局部池然后通过config_db把它传给需要共享的组件既保留了pool的便利性又限定了共享范围不会污染全局状态。这在多测试用例并行跑的场景里尤其重要后面实战部分我会详细展开。3. 类型参数决定池身份一个容易忽略的全局池隔离规则3.1 同样是poolstring池和自定义类池互不相通这是我在实际项目中踩过的一个很隐蔽的坑。当时我在环境里放了一个全局池存配置对象代码大致是这样class env_cfg extends uvm_object; bit have_coverage; int max_packets; // ... endclass // 在test里 uvm_pool#(env_cfg)::get_global_pool_alloc().set(cfg, cfg); // 在env里 uvm_pool#(uvm_object)::get_global_pool_alloc().get(cfg);第二行代码在编译和仿真阶段都不会报错但get返回的一直是null。我当时花了半天时间检查是不是set没执行到最后才意识到uvm_pool#(env_cfg)和uvm_pool#(uvm_object)是两个完全不同的池子。虽然env_cfg继承自uvm_object但泛型类型不同静态变量m_global_pool就不一样彼此之间根本看不到对方的数据。这个规则的本质是全局池的身份由完整参数类型组合决定而不只是类名或者池的名字。uvm_pool#(A)和uvm_pool#(B)只要A和B不是同一个类型就是两个池子哪怕A继承自B也一样。3.2 三个常见参数类型的池互不干扰实测确认用一个最小实验可以很直观地验证class ref_data extends uvm_object; int value; endclass class ref_data_ext extends ref_data; int extra; endclass module test; initial begin uvm_pool#(ref_data)::get_global_pool_alloc().set(key, new()); uvm_pool#(ref_data_ext)::get_global_pool_alloc().set(key, new()); $display(%0d, uvm_pool#(ref_data)::get_global_pool().num()); // 输出1 $display(%0d, uvm_pool#(ref_data_ext)::get_global_pool().num()); // 输出1 end endmodule两个池子里各有一个条目互不影响。这个特性既是优点也是陷阱优点是类型之间天然隔离陷阱是如果你在不同地方用了不同类型的池子就必须确保所有调用方都统一类型否则数据就像丢进了平行宇宙。3.3 如果非要跨类型取数据只能统一池的类型有一种做法是统一用uvm_pool#(uvm_object)这种基类池往里塞什么对象都行取出来再用$cast转成实际类型。这种做法在极端情况下可行但我不推荐作为常规手段因为类型安全就完全靠自觉了编译器帮不上忙。如果哪天塞错了一个类型$cast失败时只有一句报错排查成本比直接用参数化类型高得多。正确做法是在设计阶段就定好每一种需要共享的数据类型单独声明一个对应的uvm_pool#(T)并且保证set和get两边使用完全相同的类型参数。这是用pool的基本素养。4. 三个真实场景下的使用姿势从参考模型到对象缓存4.1 场景一参考模型和记分板共享中间预测数据这是uvm_pool最典型的应用场景。假设参考模型每个时钟周期都会算出一份黄金预测值记分板需要在同一个周期拿到这份数据做比对class ref_model extends uvm_component; uvm_pool#(golden_data) data_pool; function void build_phase(uvm_phase phase); super.build_phase(phase); data_pool uvm_pool#(golden_data)::get_global_pool_alloc(); endfunction task run_phase(uvm_phase phase); forever begin (posedge vif.clk); golden_data gd new(); gd.value calc_expected_value(); data_pool.set(current_golden, gd); end endtask endclass class scoreboard extends uvm_scoreboard; uvm_pool#(golden_data) data_pool; function void build_phase(uvm_phase phase); super.build_phase(phase); // 类型必须完全一致 data_pool uvm_pool#(golden_data)::get_global_pool_alloc(); endfunction task run_phase(uvm_phase phase); forever begin (posedge vif.clk); golden_data gd data_pool.get(current_golden); if (gd null) uvm_error(SCB, golden data not found) else compare_with_dut_output(gd); end endtask endclass这里有个小细节get之前最好判断一下是否为null。虽然理论上参考模型先跑、记分板后采样就不会出问题但验证环境里的时序偶尔会因为复位、同步延迟等原因乱掉null判断既是保护也是排查手段。4.2 场景二测试用例之间复用同一套环境配置有些环境配置对象特别大包含几十个字段如果每个test都重新构造一份再传给环境代码冗余严重。用pool可以做一个配置的默认值仓库class base_test extends uvm_test; virtual function void build_phase(uvm_phase phase); super.build_phase(phase); env_cfg cfg new(default_cfg); cfg.have_coverage 1; cfg.max_packets 1000; cfg.sequence_count 100; uvm_pool#(env_cfg)::get_global_pool_alloc().set(default_cfg, cfg); endfunction endclass class test_small_packets extends base_test; virtual function void build_phase(uvm_phase phase); super.build_phase(phase); // 取出来修改再放回去其他组件拿到的是修改后的版本 env_cfg cfg uvm_pool#(env_cfg)::get_global_pool().get(default_cfg); cfg.max_packets 10; uvm_pool#(env_cfg)::get_global_pool().set(default_cfg, cfg); endfunction endclass注意这里get返回的是句柄你修改cfg内容时池子里存的那个对象也会同步变化因为它们是同一个对象。要是想保留原始配置不被改动就得先拷贝一份再改。这是句柄语义不是值语义必须心里有数。4.3 场景三对象缓存避免高频创建大对象在跑长时间回归时频繁new大对象会带来不小的内存和性能开销。用pool做对象缓存是很自然的思路class packet_cache; uvm_pool#(packet) pool; int max_cached 64; function void build_phase(uvm_phase phase); super.build_phase(phase); pool uvm_pool#(packet)::get_global_pool_alloc(); endfunction function packet acquire(string key); packet p pool.get(key); if (p null) begin p packet::type_id::create(cached_packet); pool.set(key, p); end p.clean(); // 复用前清空字段 return p; endfunction function void release(string key); // 实际开发中可以根据需要决定是否真要释放 pool.deallocate(key); endfunction endclass这种用法的核心是控制对象的生命周期。acquire时如果池子里有就复用没有就新建并放进去release时直接删掉。如果你希望对象长期驻留、只是被多个组件轮流借用那release可以不实际deallocate而是用一个空闲标志来管理。这取决于你的业务逻辑pool本身不关心这些。4.4 局部池隔离多测试用例的共享数据再说一个我比较推崇的用法。直接用全局池有个隐患不同testcase之间共享同一个池前一个caseset的数据可能会污染后一个case。规避方法是每个test创建自己的局部池通过config_db往下传class base_test extends uvm_test; uvm_pool#(ref_data) local_pool; function void build_phase(uvm_phase phase); super.build_phase(phase); local_pool uvm_pool#(ref_data)::get_local_pool(); uvm_config_db#(uvm_pool#(ref_data))::set(this, *, ref_pool, local_pool); endfunction endclass这样每个test拿到的池子都是初始状态互不干扰又保留了pool的灵活接口。如果你问我全局池到底能不能用我的回答是能用但要明确知道它是一次性的、还是常驻的。常驻数据比如环境拓扑固定的共享对象用全局池没问题凡是跟具体testcase相关的数据尽量走局部池。5. 五个我在实战中反复踩的坑5.1 全局池被多个testcase复用造成数据污染这是最隐蔽的坑。第一次跑testcase A向某个全局池里set了一堆数据第二次跑testcase BB没有set相同key却get到了A留下的旧数据。仿真结果看起来像灵异事件其实是全局池跨case存活了。我的建议是如果确实要用全局池就要在环境搭建时约定好清理机制。但uvm_pool没有提供clear()方法你只能自己遍历所有key再逐个deallocate可你又不知道有哪些key。所以最干净的办法还是回到4.4的做法——用局部池配合config_db传递从根上避开跨case共享问题。5.2 get返回的是句柄不是副本修改对象会波及所有人看这个例子ref_data d1 pool.get(key); d1.value 100; ref_data d2 pool.get(key); $display(%0d, d2.value); // 结果也是100d1和d2指向同一个对象改一处四处变。很多刚用pool的同事以为pool像关联数组一样存了一份其实存的是句柄。如果你需要每个调用方独立修改而不互相影响就一定要在取出来后自己拷贝一份。UVM里可以用copy()方法但前提是你定义了do_copy没有的话就老老实实逐字段复制。5.3 get返回null问题可能不在时间点而在类型前面第3节讲过全局池是按类型隔离的。如果get一直返回null先别急着怀疑时序花两分钟检查一下set和get两边的泛型参数是否一字不差。我在项目里遇到过set用的是uvm_pool#(uvm_object)get用的是uvm_pool#(my_type)这俩永远不可能互通排查过程完全是在浪费生命。检查顺序建议是先看类型再看key再看调用时序。这三个检查点能覆盖90%以上的null问题。5.4 deallocate之后立刻get返回null容易误判为数据没产生deallocate(key)执行的是删除操作删完之后exists(key)返回0get(key)返回null。这本身没问题但如果你在代码里有类似数据未产生就报错误的逻辑就要注意到底是真没set过还是刚刚被人deallocate了。这两种情况的修复方向完全不同。一个实用的调试技巧在get失败时同时打印num()和exists(key)的结果。如果池里还有其他条目但指定的key不存在说明这个key从未被写入或已删除如果池里条目数也是0说明整个池子可能都没正常工作。5.5 phase顺序问题build_phase里get不到run_phase才set的数据UVM的phase执行有严格顺序先build_phase再connect_phase最后run_phase。如果你在某个组件的build_phase里尝试get另一个组件在run_phase里才会set的数据结果必然是null。这不算bug而是UVM执行模型天然决定的。正确做法是build_phase只负责创建和获取池子本身真正的数据读写都放在run_phase或更高层级的phase里。如果你确实需要提前拿到数据就得把set提前到更早的phase比如test类的build_phase里并且保证set先于get执行。6. 什么时候不该用uvm_pool替代方案与团队规范6.1 一张表看清四种共享方式的差异方案作用范围线程安全典型场景最大缺点uvm_config_db按层次路径定向传递安全配置参数下发不适合高频对象读写uvm_pool全局/局部按类型共享按类型隔离跨组件共享对象句柄全局池跨case污染风险静态变量模块内全局无隔离简单数据共享无法复用、无法隔离句柄逐层传递编译期绑定安全静态拓扑改动代价高、依赖多从这个表能看出来uvm_pool最适合的场景是跨组件、高频、需要按key存取对象。如果你只是为某个组件传一个配置参数用config_db更合规矩如果你连key都懒得设计就纯粹想搞一个全局变量那静态变量反而更简单直接。6.2 团队协作时的几个约定能省掉大量排查时间pool用起来简单但用多了会让环境的数据流变得隐晦。我在团队里推行了几条约定效果很好全局池只放常驻共享对象凡是跟testcase相关的数据一律用局部池。这从源头上杜绝了case间污染。key命名必须带前缀和所属模块比如ref_model.golden、env.cfg.default。别用data这种泛泛的名字不然后期根本分不清是哪个模块在写。每个池的类型和使用目的要在头文件里注释清楚。因为类型不一样池就不一样如果不统一两个模块各写各的类型等于各用各的池数据流完全断裂。注释里写明这个池由ref_model写、由scoreboard读能避免很多低级错误。set和get必须成对出现在同一个功能上下文中。如果你set了数据但没有地方get或者get的地方找不到set说明设计有问题要么删掉要么补全。6.3 我的最终选型经验说到底uvm_pool不是一个高级特性它是UVM提供的一个基础工具。我用它的原则就三句话配置走config_db对象共享走pool。能用局部池就不用全局池全局池只放铁打不动的常驻数据。拿不准的时候先画数据流图标清楚谁写、谁读、哪个phase生效再决定用哪种共享方式。最后分享一个小技巧调试pool问题时别靠眼睛看代码直接在关键点打印num()配合exists(key)和get(key)null三个信号很快就能定位数据是没写进去还是读错了地方。这个方法帮我在不少复杂环境里省下了几个小时的排查时间希望你用不上但万一遇到了记得回来翻翻这篇内容。
返回列表