ARTICLE DETAIL

资讯详情

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

UVM树形结构详解:验证平台层次与建树机制

UVM树形结构详解:验证平台层次与建树机制 做数字IC验证的不管你是刚入门还是干了三五年UVM这套东西总归是绕不开的。很多人打开一个现成的UVM验证平台映入眼帘的是大量类定义——test、env、agent、driver、monitor、scoreboard、reference model一层套一层好像每个文件都在互相引用代码读起来不比追一部悬疑剧轻松。写UVM验证平台最让人困惑的往往不是某个类的实现细节而是整个平台的结构为什么长这样、各个组件之间怎么挂接、代码执行的先后又是怎么决定的。这些问题的答案全部集中在一个概念上UVM的Hierarchy树形结构。UVM的Hierarchy层次树形结构是整个验证平台的骨架它规定了每个组件在树上的位置、由谁创建、什么时候创建、phase怎么推进、config_db的资源往哪传。只要把这棵树的来龙去脉摸透了后面看代码、调环境、写测试用例都会顺畅很多。这篇文章我会从树的根、枝、叶入手结合我自己在实际调试中踩过的坑把这棵树掰开揉碎讲一遍。适合正在学UVM的验证小白、准备UVM验证面试的朋友也适合项目里被层次结构混乱折磨到没脾气的同学。1. Hierarchy树形结构到底是什么1.1 用公司组织架构来理解UVM的树这棵树并不是什么玄学概念它跟我们平时熟悉的公司组织架构非常像。一家公司有CEOCEO下面分研发部、产品部、测试部测试部下面又分若干个测试小组每个小组里有组长、组员。UVM的树形结构也是同样的逻辑最顶上有一个总根根下面挂不同层级的组件每个组件下面还能再挂自己的子组件一层层嵌套下去最终形成一棵倒挂的树。对应到UVM代码里这棵树的节点就是一个个继承自uvm_component的类实例。每个实例有两个关键身份一个是自己的名字name用来在树里唯一标识另一个是自己的父亲parent用来指明自己挂在哪个节点下面。只要这两个信息给对了这棵树就能被完整地构建出来。验证平台里所有需要参与phase调度、需要跨模块通信、需要在整个生命周期里稳定存在的对象都得在这棵树里有自己的位置。1.2 树上的节点uvm_component与uvm_object的区别很多新手学UVM的时候最蒙的就是uvm_component和uvm_object到底有什么区别。这俩名字长得太像连注册宏看起来都差不多一个uvm_component_utils一个uvm_object_utils。但它们在树形结构里的地位完全不同。uvm_component是树的节点它有parent有name有自己的生命周期由build_phase统一创建从仿真开始到最后结束一直存在例如driver、monitor、scoreboard、agent、env全是component。uvm_object则是没有“位置感”的游离对象它不挂在树上没有parent概念生命周期由使用者自己控制典型代表是transaction、sequence item、sequence。你可以把树想象成一栋楼的各种房间component是房间transaction是房间里跑来跑去的快递包裹。房间有固定的位置和编号包裹则是临时存在的送完就没了。这个概念没掰扯清楚后面经常会写出“在component里用new创建另一个component”这种让树直接长歪的代码。1.3 谁是根uvm_top聊树必然要有根。UVM里有一个全局唯一的根组件叫uvm_top。它是uvm_root类型的一个实例在UVM环境初始化时自动创建不需要你写任何代码。你调用run_test(my_test)的时候UVM其实就是在uvm_top下面通过工厂机制创建了一个my_test实例默认名字叫uvm_test_top。所以完整的树是从uvm_top长出uvm_test_top再往下长出env、agent这些枝叶的。uvm_top是棵树的起点也是整个验证平台所有全局操作的入口。调试的时候经常用到一句话uvm_top.print_topology()它打印出来的就是从uvm_top开始的整棵完整树。你可以不记得任何组件的具体路径但你一定要知道总根叫什么——后面所有关于树的操作追根溯源都会回到这里。2. 树是怎么长出来的2.1 从new()函数的两个参数说起uvm_component的构造函数长这样function new(string name, uvm_component parent); super.new(name, parent); endfunction这两个参数是整个树形结构的基石。name决定这个节点叫什么名字parent决定它挂在哪。注意这个parent不是随便传一个对象进去就完事了它必须是另一棵树上已有的节点。UVM在底层会把每个组件加入内部维护的树形索引里父子关系就是靠这个参数一点一点连接起来的。我见过不少新手写组件的时候图省事构造函数只写一个name参数或者直接不传parent。这样做会导致两个结果要么这个组件成了无父节点从树上脱掉要么编译直接报错找不到匹配的构造函数。UVM类库推荐的写法就是标准两个参数照抄就行。如果有特殊需求需要自己定义构造函数千万别把super.new(name, parent)丢了这是树能挂上的根。2.2 build_phase自上而下建树光有构造函数还不够树真正长出来靠的是build_phase。这个phase最核心的特点就是自上而下执行——从uvm_test_top开始先执行test的build_phase然后在test的build_phase里创建env接下来执行env的build_phase再在env的build_phase里创建agent和scoreboard一层层往下推直到整棵树全部创建完毕。拿一段典型的代码来说class my_env extends uvm_env; my_agent agent; my_scoreboard scb; function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); agent my_agent::type_id::create(agent, this); scb my_scoreboard::type_id::create(scb, this); endfunction endclass在这儿create第二个参数传的this就是把新建的agent挂到当前env节点下的关键。this就是当前组件自己。只要parent传对了建出来的东西位置就不会错。整个build_phase执行完后树的主干已经成型后面connect_phase只是把各个节点之间的通信端口连接起来不会再增加新的节点。2.3 为什么不能在其他地方用create乱建节点UVM对component的创建时机有严格规定普通component只能在build_phase里用uvm_xxx::type_id::create来创建不能在构造函数里new不能在其他phase里随便create一个component。原因很简单——build_phase是UVM规划好的建树窗口只有在这个窗口里创建的节点才能被正确挂到树上并参与后续的phase调度和资源传递。如果有人在run_phase里写了一句xxx my_driver::type_id::create(xxx, this)表面上看代码编译没问题运行也不立刻报错但等跑起来就会发现这个driver的phase永远不执行或者config_db里配给它的参数全都拿不到。这就是因为它错过了build_phaseUVM不会再为它安排建树的流程。实际项目里这种错误排查起来特别耗时因为你光看代码逻辑是看不出来问题的。那是不是树上的节点就不能动态增删了倒也不是UVM还有uvm_component的create_component等底层接口也支持用uvm_top动态挂一些组件但这些都是特殊场景下的用法普通验证平台用不着。老老实实遵循build_phase建树的规则是省心的最好办法。3. 树形结构如何驱动整个平台运行3.1 phase按树的顺序执行树搭好了接下来就要让平台跑起来。UVM的phase机制是挂在树形结构之上的它严格按照树的层次顺序来调度。以build_phase为例必须是从根到叶子逐层执行因为父节点没构建完子节点压根还不存在没法执行。而到了final_phase则是从叶子到根来执行因为子节点要先释放自己的资源、收尾自己的行为父节点才能做最终汇总。这种自顶向下、再自底向上的调度逻辑听起来有点像公司开年会先是CEO讲话然后各部门负责人讲话再然后各组长发言到了散场的时候各小组先清理会场部门确认人数最后CEO做总结。整个过程都是有明确先后顺序的不能乱来。UVM的phase调度器会遍历整棵树依次对每个节点调用对应phase的回调函数。所以树上任何一个节点的phase没有return后面的节点就会卡住。实际调试中遇到run_phase跑不完、结束不了仿真的情况十有八九是树上某个组件里的人在while循环里忘了退。3.2 config_db沿树传递资源环顾整棵树uvm_config_db的使用也深深依赖树形结构。uvm_config_db的操作分为set和get两边set的时候可以指定从哪个组件视角配置get的时候也是基于某个组件的层次路径去查找。如果你对树形结构不熟经常会出现set在agent层get在test层然后莫名其妙get不到的现象。我习惯把config_db理解成树上的“快递站”。set就是把快递放到某个节点对应的站点get就是去另一个站点取。快递能不能送到取决于两个站点的路径在不在同一棵树上取得到。代码里常见的写法是uvm_config_db#(virtual my_if)::set(this, env.agent.*, vif, this.vif);这里第二个参数是目标路径env.agent.*就是一条树上的路径表达式。它相对于当前组件的位置来定位。如果当前组件是test而这个test下面没有env.agent那这个快递就送不到。很多同学配置了半天virtual interface还是拿到null就是路径写错了。调试这类问题最直接的办法就是打印一下整棵树看看你期望的路径在树上到底存不存在。路径这种东西靠脑子想不如靠眼睛看。3.3 寄存器模型在树中的位置很多项目会在验证平台里加入寄存器模型uvm_reg_block、uvm_reg_map等用来做寄存器的前门访问、后门访问和镜像值比对。寄存器模型本身不直接挂在这棵树上它多数时候作为一个uvm_object或者被封装在某个component里。镜像值mirror value的比对依赖于寄存器模型与DUT寄存器的同步而register model的访问接口通常由env或者专门的reg_agent来驱动。在往register model里取值时最常踩的坑是镜像值与期望值对不上。这跟树的层次关系不大但跟建树的顺序有很大关系——如果register block的build没有在env的build里完成reg_model的配置路径就和adapter、sequencer的路径对不上后门访问就总有一步是空的。从树的视角去追踪很多这类问题的定位就变得特别快。4. 打印与分析树形结构4.1 print_topology一行命令看全树排查树的层次、路径、节点挂接问题时最实用的一招就是打印拓扑。在test的end_of_elaboration_phase里加一行virtual function void end_of_elaboration_phase(uvm_phase phase); super.end_of_elaboration_phase(phase); uvm_top.print_topology(); endfunction跑仿真的时候终端或者log文件里就会输出整棵树的层次结构。每行缩进代表一层每个节点的类型名称、实例路径一目了然。打印出来以后你就能很直观地看到一个test下面挂了多少个envagent下面有driver、monitor、sequencerenv下面还有scoreboard和coverage。我实际项目里会在test里保留一个开关用uvm_info或者if控制是否打印拓扑。默认打出来虽然日志比较长但是在环境初建和排查问题时特别有价值。你光靠代码去想象整个结构永远没有直接看打印来得快。4.2 树的输出长什么样打印出来的树大概长这样uvm_test_top my_env my_agent my_driver my_sequencer my_monitor my_scoreboard每层通过缩进区分节点名字就是创建时传给create的第一个字符串参数括号里跟的是这个实例对应的类类型。如果某个节点的名字在树里重复了UVM会自动在后面加上__1、__2之类的后缀来保证唯一性。看到这种后缀的时候就要注意了这往往说明你在某处无意中创建了多个同名组件可能是多重复制也可能是层次挂错了。4.3 树长歪了怎么排查树长歪的意思就是节点不在你期望的位置上。最常见的三种情况一是组件创建了但树上看不到二是组件挂在了别的env下面三是出现了多个重复实例。排查这类问题我有一套固定流程。先看打印出来的树找到目标节点实际在哪一层再看创建它的代码确认create的parent参数到底传了谁接着确认这个节点是否在期望的build_phase里创建最后确认是否被多次调用build造成重复创建。大多数情况都是parent传了null或者传错对象导致的。还有一种隐蔽情况是组件内部super.build_phase(phase)没调用父类初始化逻辑跳过了导致子组件的创建顺序被打乱。经验之谈树的问题用眼睛扫代码是最慢的先把树打出来再说。5. 常见问题与排错经验5.1 高频问题速查表这里我把日常问答里出现频率最高的一批问题整理成表格方便遇到对应现象时快速定位。现象可能原因排查方向树上找不到某个组件创建它的代码没进build_phasecreate参数传错parent打印拓扑搜索实例名组件出现两次build_phase被调用了两次代码里手动new了component检查父组件是否被重复创建同名字实例后带__1树中name重复UVM自动追加后缀检查是否有多个同层组件创建同名子组件phase不执行组件脱离树没挂到正确的父节点下打印树确认组件在树中的路径config_db get到nullset和get的路径不一致路径相对位置搞错用层次路径在树上对照检查final_phase不执行某个组件里一直有run_phase循环没退出打印phase执行信息逐个节点排查镜像值一直比对失败reg_model没被正确挂到adapter/sequencer路径上核查reg_model的路径与配置链这张表我建议直接收藏很多问题看着复杂最后查到底都是树的路径和挂接问题。UVM里没有那么多玄学绝大部分诡异现象最后都能从树的结构上找到解释。5.2 面试常问的几个Hierarchy问题面试中关于树形结构的提问频率很高。比较经典的有UVM的树形结构是怎么构建的为什么build_phase是自上而下uvm_top是什么由谁创建print_topology打印出来的是什么uvm_component和uvm_object有什么区别新建一个component时parent参数有什么作用config_db的路径怎么解析。我的回答思路是先讲树的根是uvm_toprun_test创建test挂上去每个component的构造函数里name和parent确定位置build_phase自上而下创建子组件最终形成以uvm_test_top为根的完整树phase调度和config_db都依赖这棵树。把这些讲清楚面试官基本就能确认你对UVM的整体结构有底了。还有一道容易被问到的题如果我在run_phase里create一个新的agent会有什么问题。这个问题就是在考树形结构的创建时机。答案要落在“错过了build_phase无法正确参与phase调度和config_db配置”这个点上。5.3 最终pass/fail醒目显示的小技巧很多人验证跑完了想在一堆log里快速看到结论UVM默认的打印信息不够醒目。我习惯在test的report_phase里做最终结果汇总根据UVM的report server统计信息在屏幕上用醒目的显示输出“PASS”或“FAIL”字样。核心思路是拿到整个仿真的uvm_report_server统计error和fatal的数量virtual function void report_phase(uvm_phase phase); uvm_report_server server; int err_count; super.report_phase(phase); server uvm_report_server::get_server(); err_count server.get_severity_count(UVM_ERROR) server.get_severity_count(UVM_FATAL); if (err_count 0) begin $display(); $display( TEST PASSED ); $display(); end else begin $display(); $display( TEST FAILED ); $display(); $display( total errors/fatals: %0d, err_count); $display(); end endfunction这段代码和树形结构没有直接关系但它是整个验证平台收尾的临门一脚。配合前面建好的树、跑完的phase最终判定结果一目了然。很多人问UVM里怎么显示非常醒目的pass和fail其实就是利用uvm_report_server的统计接口在report_phase阶段做一次汇总打印。6. 待更内容与实战心得这篇文章标着“待更”是因为Hierarchy树形结构延伸出来的东西确实不少。寄存器模型的镜像值比对逻辑、多个agent之间的层级隔离、subscriber在树里的挂接、以及UVM环境级的树形调度时序每一块都能单开一篇。特别是UVM寄存器模型和树的结合点上很多实际工程里的访问顺序、路径配置问题整理出来会是很有价值的实战资料。按我个人的习惯每接到一个新的验证模块第一件事不是急着写sequence而是先把环境里的树画出来。用print_topology跑一遍确认每个组件的层次、名字、parent都符合预期再动笔写用例。前期多花十分钟看树后期能省下好几个小时的调试时间。树形结构这个东西光看文档容易懵真正上手打印几次、建几个测试环境慢慢就摸出门道了。后面有机会我再把寄存器模型、phase调度这些topic一篇文章一篇篇文章补齐持续更新。
返回列表