尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

UVM验证平台核心:Hierarchy树形结构全面解析

UVM验证平台核心:Hierarchy树形结构全面解析 很多做验证的同学一开始接触UVM注意力全放在sequence、driver、scoreboard这些“干活”的组件上天天debug波形、调时序忙得不亦乐乎。但等平台搭到一定规模你会发现真正决定平台好不好维护、好不好排查问题的往往不是某个组件本身写得多漂亮而是这些组件在UVM Hierarchy树形结构里挂得对不对。项目标题“UVM-验证平台——Hierarchy树形结构”点到的恰恰就是这套被很多人忽视、却在面试和实战里反复被问到的底层骨架。这篇文章我想从“这棵树到底是怎么长出来的”讲起把uvm_component的挂载关系、build_phase的执行顺序、层次路径的生成规则这些概念拆开揉碎然后带大家从零搭一个最小的UVM树形平台跑一遍仿真看看它怎么组织、怎么运行、出了错怎么排查。无论你是刚入门UVM的新手还是已经写过一些验证组件但总觉得“差口气”的初级工程师这篇内容都适合你。理解了Hierarchy树形结构再看phase机制、config_db的路径匹配、寄存器模型的镜像值更新都会顺畅很多。1. 内容整体设计与思路拆解1.1 为什么验证平台需要一个“组织架构”先做一个思想实验。假设你现在要用UVM搭一个稍微像样点的验证平台里面至少要有一个test、一个env、一个agentagent下面有driver、monitor、sequencer再配一个scoreboard做数据对比。如果是多协议、多接口的项目可能还有多个env、多个agent每个agent下面又是一套driver、monitor、sequencer。组件数量一下就到了十几个甚至几十个。这么多组件如果大家都是“平铺”的关系你打算怎么管理phase怎么调度config_db的set和get路径怎么写环境要重建时哪些组件该创建、哪些该销毁靠脑内记忆吗显然不现实。这时候就需要一个统一、可递归遍历的组织结构把每个组件都挂到一个明确的父节点下形成一棵自顶向下的树。UVM做这件事的手段就是Hierarchy树形结构它在UVM里不是一个虚的概念而是由uvm_component基类通过parent参数在new函数里显式建立起来的。每个uvm_component在创建时都必须知道自己的“爸爸”是谁这个爸爸关系决定了它在树上的位置也决定了它以后从哪个路径被访问、被配置。有了这棵树整个验证平台就有了清晰的组织架构test在最顶部下面是envenv下面是agent和scoreboardagent下面再挂driver、monitor、sequencer。UVM的phase机制沿着这棵树递归执行消息报告机制也按这棵树的路径区分来源我调config_db时也是通过树的路径精确命中目标组件。可以说没有Hierarchy树形结构UVM的一切特性都无从谈起。1.2 UVM树形结构的基本组成从uvm_root到叶子节点UVM的树形结构并不是从你的test才开始长的。在test的最顶层还有一个隐形的“总根”叫uvm_root。uvm_root是UVM内部自动创建的一个顶层节点全局只有一个实例可以用uvm_top来访问它。你的test_top就是挂在uvm_root下面作为它的child存在的。这就形成一个很有意思的关系uvm_root全局唯一顶层根→ uvm_test_top我们自己写的test→ uvm_env环境→ uvm_agent代理→ driver/monitor/sequencer具体干活的小兵。这个结构从上到下一层套一层。组件的类型上UVM把对象分成了两大派系一类是uvm_object比如sequence、sequence_item、寄存器模型里的某些类它们不参与树的挂载生命周期短、随用随建没有parent的概念另一类是uvm_component比如driver、monitor、agent、env、test它们必须有parent必须挂到树上和testbench同寿命phase机制也只作用在uvm_component上。这个区分极其关键。很多新手在写sequence时也习惯性地想给它加个parent却发现传不进去因为uvm_object的new函数根本没有parent参数。反过来如果自己写了一个driver却忘了在new函数里调用super.new(name, parent)编译能过但跑到phase调度时就会出各种稀奇古怪的问题比如组件不执行run_phase或者get_full_name()打出来是空的。理解了树形结构这些问题就都成了“可预测的坑”。1.3 用一张“家族族谱”看懂组件之间的层级关系为了让大家更直观地记住UVM树的形态我经常把它类比成一个家族族谱。uvm_root相当于这个家族的“老祖宗”一般不直接露面但所有分支都从它那里延续出来。uvm_test_top是当代的“家主”整个平台的运行从家主一声令下开始。env是家里的各个“部门”比如采购部、生产部、质检部agent就是部门下面的“小组”小组里有负责干活的“组员”比如driver负责往总线发送激励monitor负责盯着总线把数据采回来sequencer负责把sequence产生的transaction分发给driver。scoreboard呢它更像是“质检部”独立于agent之外但同属env这个层级。它接收monitor从DUT接口上采到的数据再和reference model或者预期数据做比对。在树形结构里scoreboard通常也挂在env下面和agent平级。我整理了一张组件层级对照表对应关系一目了然层级UVM组件家族类比0uvm_root (uvm_top)老祖宗全局唯一1uvm_test_top (test)家主整个测试的入口2uvm_env (env)部门负责组装各种验证组件3uvm_agent / uvm_scoreboard小组/质检部负责具体协议和比对4driver / monitor / sequencer组员实际干活的执行者这个树形结构一旦建立起来UVM的很多特性就变得非常自然了build_phase从上到下递归执行connect_phase在同一层之间建立连接run_phase所有component并行跑消息报告带上每个组件的层次路径。所以Hierarchy不只是“摆设”它是UVM一切运行机制的骨架。2. 树形结构是怎么搭起来的深入component的诞生与挂载2.1 所有component都必须显式指定parent否则就直接挂掉代码层面一棵树是怎么建立的呢关键在new函数。UVM里所有组件类都要求实例化时同时指定实例名name和父节点parent。这个设计贯穿了uvm_component的全部生命周期。我举一个最典型的反例。很多初学UVM的朋友写driver时是这样的function new(string name driver); super.new(name); endfunction看着没什么问题编译也过仿真也能跑但当你把它挂到agent下面时grandmaster级别的UVM会告诉你一个扎心的结果这个driver不是agent的孩子它成了uvm_root的孩子或者严格说它成了一个“没有爸爸的孤儿组件”。为什么因为super.new(name)默认把parent设成了null。当你没有主动传入parent时UVM在uvm_component的new里会把parent为null的组件自动挂到uvm_topuvm_root的实例下面。这会导致agent想通过get_child(driver)找到它时返回空指针config_db的路径完全对不上phase调度顺序也不再是agent先执行再执行driver。正确写法其实很无脑function new(string name, uvm_component parent); super.new(name, parent); endfunction然后在agent里这样创建driver uvm_driver::type_id::create(driver, this);这里的第二个参数this就是当前agent自己。create内部会调用driver的构造函数并且把this传给parent参数。这一传driver这棵小树苗就成功挂到了agent这根枝条上。2.2 build_phase的展开顺序为什么必须先父后子组件挂到树上之后UVM怎么知道先创建谁、后创建谁这里就要提到UVM的build_phase机制。build_phase是整个仿真过程中最先执行的phase之一它在uvm_root里被触发然后从上到下、按树的深度逐层递归。理解这个顺序太重要了。以我们的最小平台为例run_test会先去创建test_toptest的build_phase执行在build_phase里创建envenv被创建并挂到test下面后UVM接着执行env的build_phaseenv在里面创建agent和scoreboardagent的build_phase再创建driver、monitor、sequencer。整个过程是“先父后子、深度优先”的。为什么必须这样道理很简单一个父亲组件在创建孩子之前它自己必须已经存在。如果env想创建agent但env自己还没被build出来那agent挂到哪里去没有父节点就没法成为树的一部分。所以UVM强行规定了build_phase的顺序是从root开始逐层向下不能乱。这个顺序还有个隐蔽的影响父组件在build_phase里创建的配置比如set到config_db的参数子组件在build_phase里get的时候理论上应该能拿到相同的内容。因为config_db的set/get机制也是按层次路径来匹配的父节点的路径前缀天然会被子节点继承。理解了树的顺序config_db里很多“为什么有时get不到”的问题就能迎刃而解。2.3 super.build_phase()这行代码到底能不能省自定义UVM组件的build_phase末尾总会看到一行super.build_phase()。很多教程会说“要记得调用”但没说为什么。这里我结合树形结构的机制解释一下。uvm_component里定义的build_phase包含了UVM内部处理component层次匹配、config_db初始化的很多默认逻辑。如果子类重写了build_phase却没有调用super.build_phase()那么这些默认逻辑就不会执行。最典型的表现就是在某些复杂场景下组件的层次信息不完整或者config_db里set出去的参数在子组件里get不到又或者用uvm_top.print_topology()打印层次时某些节点异常。我自己在实际项目中就见过一个同事因为漏写了super.build_phase()导致一个monitor在函数里get_config_string(vif)总是返回空整个平台的接口配置全部失效。查了两天最后发现就是少了这一行。这种bug有时候甚至不影响编译也不报错就是行为诡异特别坑人。所以凡是重写build_phase第一行就必须写上super.build_phase()这是铁律没有例外。2.4 create的name参数就是组件在树上的“门牌号”在UVM里每个component实例都有两个“名字”格外重要。一个是get_name()就是创建组件时传入的name参数比如driver、env另一个是get_full_name()是从uvm_root开始到当前组件的完整路径用点号连接比如uvm_test_top.env.agent.driver。get_full_name()就是组件在树上的门牌号也是config_db的set和get能够精确找到目标的根本依据。这里要特别提醒一个坑同一个父节点下的两个子组件名字不能重复。如果你在env里创建了两个agent一个叫agent另一个还叫agentUVM并不会直接给你报编译错误但在运行时config_db的路径匹配就会出现严重的歧义。update相关操作也可能落到错误的组件上。我记得有次在项目里调试寄存器模型的镜像值mirror value更新混乱最后发现原因就是两个agent重名导致寄存器模型通过uvm_reg_block的路径访问时数据被写到了错误的hdl_path上镜像值自然对不上。后来把agent名字改为agent0和agent1问题立刻消失。所以给组件起名字一定不要偷懒best practice是在名字里体现出组件在环境中的角色和序号比如mst_agent、slv_agent、mst_driver这种。好名字就是好门牌后续排查问题会省很多力气。3. 实操从零搭建一棵最小化UVM树形验证平台并验证结构3.1 环境结构设计我们这棵树准备怎么长理论讲了半天肯定有人要问到底怎么亲眼看到这棵树长出来下面我带着大家从零搭一个最小的UVM平台跑仿真亲眼看Hierarchy树形结构是什么样子。我们的平台规模控制在最小可用级别一个test一个env一个agentagent下面有driver、monitor、sequencer再加一个scoreboard用来和agent平级。这个规模虽然简单但已经能完整展示树形结构的所有关键节点。设计如下uvm_top (uvm_root) └── test_top (my_test) └── env (my_env) ├── agent (my_agent) │ ├── driver (my_driver) │ ├── monitor (my_monitor) │ └── sequencer (my_sequencer) └── scoreboard (my_scoreboard)需要提醒的是这里我们只需要高速展示结构所以内部的connect、sequence传输等内容适度简化重点放在树的组织和phase执行顺序上。3.2 写代码test、env、agent、driver等组件的完整实现先从最底层的叶子组件写起。为了让你看清效果我在每个组件的build_phase和run_phase里都打印信息仿真一跑就能看到phase是如何沿着树展开的。my_driver.svclass my_driver extends uvm_driver #(my_transaction); uvm_component_utils(my_driver) function new(string name my_driver, uvm_component parent null); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); uvm_info(HIER, $sformatf(build_phase: %s, get_full_name()), UVM_LOW) endfunction task run_phase(uvm_phase phase); uvm_info(HIER, $sformatf(run_phase: %s, get_full_name()), UVM_LOW) // 实际项目里在这里循环向DUT发送transaction endtask endclassmy_monitor.svclass my_monitor extends uvm_monitor; uvm_component_utils(my_monitor) function new(string name my_monitor, uvm_component parent null); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); uvm_info(HIER, $sformatf(build_phase: %s, get_full_name()), UVM_LOW) endfunction task run_phase(uvm_phase phase); uvm_info(HIER, $sformatf(run_phase: %s, get_full_name()), UVM_LOW) // 实际项目里在这里采集DUT接口数据 endtask endclassmy_sequencer.svclass my_sequencer extends uvm_sequencer #(my_transaction); uvm_component_utils(my_sequencer) function new(string name my_sequencer, uvm_component parent null); super.new(name, parent); endfunction endclassmy_agent.svclass my_agent extends uvm_agent; uvm_component_utils(my_agent) my_driver driver; my_monitor monitor; my_sequencer sequencer; function new(string name my_agent, uvm_component parent null); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); driver my_driver::type_id::create(driver, this); monitor my_monitor::type_id::create(monitor, this); sequencer my_sequencer::type_id::create(sequencer, this); uvm_info(HIER, $sformatf(build_phase: %s, get_full_name()), UVM_LOW) endfunction virtual function void connect_phase(uvm_phase phase); super.connect_phase(phase); driver.seq_item_port.connect(sequencer.seq_item_export); endfunction endclassmy_scoreboard.svclass my_scoreboard extends uvm_scoreboard; uvm_component_utils(my_scoreboard) function new(string name my_scoreboard, uvm_component parent null); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); uvm_info(HIER, $sformatf(build_phase: %s, get_full_name()), UVM_LOW) endfunction virtual function void connect_phase(uvm_phase phase); super.connect_phase(phase); uvm_info(HIER, $sformatf(connect_phase: %s, get_full_name()), UVM_LOW) endfunction endclassmy_env.svclass my_env extends uvm_env; uvm_component_utils(my_env) my_agent agent; my_scoreboard scoreboard; function new(string name my_env, uvm_component parent null); 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); scoreboard my_scoreboard::type_id::create(scoreboard, this); uvm_info(HIER, $sformatf(build_phase: %s, get_full_name()), UVM_LOW) endfunction virtual function void connect_phase(uvm_phase phase); super.connect_phase(phase); // 实际项目里在这里把monitor的分析端口连到scoreboard endfunction endclassmy_test.svclass my_test extends uvm_test; uvm_component_utils(my_test) my_env env; function new(string name my_test, uvm_component parent null); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); env my_env::type_id::create(env, this); uvm_info(HIER, $sformatf(build_phase: %s, get_full_name()), UVM_LOW) endfunction virtual function void end_of_elaboration_phase(uvm_phase phase); super.end_of_elaboration_phase(phase); uvm_top.print_topology(); endfunction task run_phase(uvm_phase phase); uvm_info(HIER, $sformatf(run_phase: %s, get_full_name()), UVM_LOW) endtask endclasstb_top.svmodule tb_top; import uvm_pkg::*; include uvm_macros.svh include my_transaction.sv include my_driver.sv include my_monitor.sv include my_sequencer.sv include my_agent.sv include my_scoreboard.sv include my_env.sv include my_test.sv initial begin run_test(my_test); end endmodule这里在test的end_of_elaboration_phase里调用了uvm_top.print_topology()这个函数就是UVM官方提供的“树形结构打印工具”它会用文本方式把整棵Hierarchy树原原本本地画出来。对你理解树的组织方式非常有帮助。3.3 跑仿真亲眼看树形结构长什么样用任意一款支持UVM的仿真器编译运行上面的代码仿真log里会输出类似这样的信息UVM_INFO 0: uvm_test_top [HIER] build_phase: uvm_test_top UVM_INFO 0: uvm_test_top.env [HIER] build_phase: uvm_test_top.env UVM_INFO 0: uvm_test_top.env.agent [HIER] build_phase: uvm_test_top.env.agent UVM_INFO 0: uvm_test_top.env.agent.driver [HIER] build_phase: uvm_test_top.env.agent.driver UVM_INFO 0: uvm_test_top.env.agent.monitor [HIER] build_phase: uvm_test_top.env.agent.monitor UVM_INFO 0: uvm_test_top.env.agent.sequencer [HIER] build_phase: uvm_test_top.env.agent.sequencer UVM_INFO 0: uvm_test_top.env.scoreboard [HIER] build_phase: uvm_test_top.env.scoreboard注意观察顺序test最先build接着是env再往下是agentagent之后再逐个build它下面的driver、monitor、sequencer最后才是scoreboard。这就是UVM phase从树顶递归向下展开的直观体现。接下来是print_topology的输出会以树状形式清晰展示关系UVM_INFO 0: reporter [UVM_TOPOLOGY] ---------------------------------------------- Name Type Size Value ---------------------------------------------- uvm_test_top my_test - 123 env my_env - 456 agent my_agent - 789 driver my_driver - abc monitor my_monitor - def sequencer my_sequencer - ghi scoreboard my_scoreboard - jkl ----------------------------------------------看到这个输出你就直观地理解了UVM树形结构的形态每个组件都有自己唯一的full path比如uvm_test_top.env.agent.driver。后续时报里的消息、config_db的路径、寄存器模型里的路径匹配、镜像值更新时的hdl路径全部都以这套路径为基准。3.4 connect_phase的连接逻辑树形结构如何支撑组件间的交互树不只是用来“看”的它还负责让组件之间建立真正的连接。在build_phase把整棵树长出来后UVM会进入connect_phase此时组件之间可以通过层次路径互相引用并建立连接。以agent内部的连接为例在my_agent的connect_phase里我们写了driver.seq_item_port.connect(sequencer.seq_item_export);这一行把driver的seq_item_port和sequencer的seq_item_export连接起来。为什么connect_phase能安全地做这件事因为build_phase时agent已经创建了driver和sequencer这两个子组件并且它们已经被挂到了树的正确位置此时通过agent内部的成员变量就能直接访问到它们。只有在connect_phase阶段才能保证所有组件都已经build完成。如果你试图在build_phase里连接driver和sequencer会发现sequencer可能还不存在因为build_phase还在从上往下执行同层的sequencer尚未被创建。同理如果你想在test的build_phase里通过uvm_top.find(env.agent.driver)去找driver也可能会失败因为此时agent的build_phase很可能还没执行driver根本还不存在。想要在树里按路径找到任意节点比较稳妥的时机是end_of_elaboration_phase之后那时整棵树已经完整长好了。我之前在一个项目里就遇到过类似问题想写一段代码从test里访问env.agent.driver的某成员变量放在build_phase里一直拿到空指针移到check_phase里就正常了。这就是树形结构对各个phase能力的天然限制——你只有在树完全成形之后才可能通过路径完成任意访问。4. 树形结构使用中的常见问题与排查技巧实录4.1 get_full_name()是空的怎么回事有同学问我明明组件已经创建了为什么在component里调用get_full_name()打出来是空字符串或者只有组件自己的名字。排查这种问题的第一反应就是看是不是在new函数里没有正确调用super.new(name, parent)。有一个特别容易踩的坑自己写的component的new函数没有加parent参数只接收name然后内部调用super.new(name)这样即使你在创建时传入了this这个this也会被uvm_component的new函数忽略因为new函数签名不匹配。更隐蔽的情况是你在某个uvm_object比如sequence里试图使用get_full_name()但uvm_object没有层次概念它的get_full_name()返回的就是get_name()。所以你会看到名字但没有路径。如果确实需要在sequence里拿到路径应该通过start的sequencer参数或其他手段获取而不是直接调自己。4.2 组件创建时机不对在build_phase里试图访问“孙节点”这个误区的本质还是没吃透树形结构的执行顺序。有的同学在test的build_phase里创建完env后立刻想通过test的env成员变量去访问agent.driver结果发现agent都还是空指针。为什么因为test.build_phase执行完只说明env被创建了但env.build_phase还没有执行agent还没有被创建出来更不要说driver了。一般我的建议是如果你想在test里对整棵树做点什么比如改配置、加回调、访问叶子节点放到end_of_elaboration_phase或之后更合理。UVM的phase设计已经帮你规划好了“树长齐”的时机你要做的不是强行提前而是遵循它的节奏。4.3 两个组件同名导致config_db和update路径错乱这个问题在前面2.4节里提过但因为它太常见、太隐蔽值得单独再展开一次。config_db的set和get都是基于层次路径匹配的具体匹配规则是set时使用相对路径或绝对路径get时从当前组件的full_name路径开始向上查找。如果同一个父节点下出现了两个同名的子组件UVM在查找时无法精确定位目标就可能出现“set到了A组件、get却在B组件里拿到了”的混乱局面。寄存器模型的镜像值mirror value更新会特别受这个影响。如果你通过reg_block的default_map执行mirror操作它内部会根据寄存器模型的hier路径去访问对应的信号或寄存器而hier路径本质上依赖组件树的结构和命名。一旦树里出现重名路径解析就会错乱导致镜像值怎么读都不对。所以平台里组件命名一定要见名知意且确保同一层不重名。看似是个“命名小问题”实际能引发一连串令人头皮发麻的bug。4.4 常用的树形结构调试三板斧排查树形相关的问题我一般就三板斧。第一板斧uvm_top.print_topology()在end_of_elaboration_phase里调用打印整个树看每个组件的类型、名字、路径是否符合预期。树的长相不对后面所有关于层次的分析都是白搭。第二板斧UVM报告机制里的冗余度控制。把UVM_VERBOSITY调高比如UVM_HIGH或UVM_DEBUG然后专门看带“HIER”标签的日志或者用UVM_LOG_RECORD来记录每个phase的进入和退出能很清楚地看到每个组件是在哪个phase、什么时候被执行到的。第三板斧用uvm_top.find(env.agent.driver)这种方式在代码里动态查找节点并在关键位置断言它不是null。如果有一天你重构了组件结构但忘了同步更新访问路径这个断言能第一时间帮你暴露问题。三招用完大部分树形结构问题都能定位到具体原因。剩下的就是改代码的事情了。4.5 phase顺序对树形排查的影响最后再说一个和树形结构强相关的点phase的执行顺序。在build_phase阶段树从上到下递归展开到了run_phase则是所有叶子节点并行执行到了final_phase又是从下到上依次收尾。这个“从上到下建树、并行运行、从下到上收尾”的节奏如果你对树形结构没有清晰认知很容易在debug时蒙圈。举个实际例子有次我的平台里scoreboard在check_phase里发现比对数据为空一开始我以为是scoreboard本身写错了后来才发现是monitor的run_phase里采集数据的逻辑被放进了build_phase导致信号根本还没有被采样。build_phase是函数phase不是任务phase它不能做耗时的等待操作。而那个同事之所以把采集逻辑写进build_phase是因为他还没彻底理解树形结构和phase的配合关系。理解了树和phase的组合关系之后这种错误基本不会再犯。UVM的树形结构就像一个公司的组织架构图每个组件有自己的位置、自己的上下级phase则是公司的运行流程流程沿着组织架构一步步推进。两者互相成就缺一不可。所以面试官特别喜欢围绕Hierarchy和phase出题本质上就是在考察你有没有把验证平台当成一个“有组织、有纪律的统一体”来理解而不是只会写零零散散的class。我在实际项目里带过不少应届生刚开始他们总觉得树形结构是UVM的“附加题”其实恰恰相反这是所有UVM平台的地基。地基没打好后面搭分子级的功能模块越顺利、越容易在联调时翻车。反过来你把树搞清楚了view层次、路径匹配、消息路由这些问题都会变得非常自然甚至你写出的代码风格都会明显上一个台阶。这篇先讲到这里。后续如果想继续深入我建议下一篇可以聊聊uvm_component和uvm_object的更深层区别或者直接展开phase机制的运行细节。哪个方向你们想先看评论区留个言我们下一篇继续。
返回列表