
UVM验证平台最容易被忽视却又最关键的就是Hierarchy树形结构。很多同学搭平台时照着demo抄topology一print出来乱成一团sequence和driver的数据对不上reg_model的镜像值怎么都跟RTL对不齐最后只能一句“UVM太难了”收场。其实这些问题十有八九不是UVM本身难而是你把树搭歪了。这篇文章我把UVM验证平台的Hierarchy树形结构从根到叶完整拆一遍不讲虚的直接落到每个组件为什么挂在那个节点、代码怎么写、phase怎么从上往下跑、TLM连接怎么走以及树搭错之后你会踩到哪些经典坑。适合正在搭平台或者平台跑不通正在排查的朋友不管你是刚入门还是做了两年验证这棵树的形状都应该刻在脑子里。1. 内容整体设计与思路拆解为什么Hierarchy是UVM平台的“骨架”1.1 从两个面试必问题说起“UVM验证平台里driver和sequencer是怎么握手的”“config_db为什么在build_phase里配在connect_phase里才拿得到”——这两个问题几乎所有UVM验证面试都会问。答案的钥匙都藏在同一件事里组件在树上的位置。我见过太多人把UVM当字典背知道new一个driver要传uvm_component parent这个参数知道build_phase是自顶向下connect_phase是自底向上但这些知识点是散的串不起来。直到你把整个平台当成一棵树来看所有东西才突然通了谁是谁的parent决定了config_db的视野范围组件在树上的深度决定了phase的执行先后TLM端口的连接方向决定了数据流的走向。这棵树就是整个UVM验证平台的骨架骨架正了肌肉和血管也就是phase和TLM才能各司其职。1.2 树形结构到底在解决什么问题芯片验证平台本质上是一个数据采集、激励生成、结果比对的系统。uvm_test是树根下面挂的是uvm_envenv下面挂的是各种agent、scoreboard、reference model、寄存器模型。每一个组件只负责一件事组件之间通过TLM端口传递transaction对象通过config_db共享配置参数。如果这棵树是扁平的、无序的那整个验证环境就是一团线数据该往哪流、配置该往哪传全都说不清楚。所以UVM用一棵树来强制规定验证环境的组织方式是层次化的每个组件有且只有一个parent组件之间不直接互相引用而是通过树结构加TLM端口来通信。这不是UVM故意设计得复杂而是芯片验证本身就需要这种结构化拆分——测试用例可以无穷多但平台的骨架必须稳定。树的形状稳定了你换testcase只是换叶子上的配置主干几乎不动。1.3 我在实际项目中看到的两种极端带过的实习生里搭平台有两种典型风格。一种是把所有组件都new在test里面env形同虚设agent、scoreboard全部在test里创建print_topology的时候发现树是平的所有东西都是test的child。这种平台的后果是config_db的路径配置乱成一团因为set和get的层级路径完全对不上phase执行顺序的层次感消失后期要加第二个agent几乎等于推倒重来。另一种是过度设计env套envagent里塞了三个子agentvirtual sequencer套了两层树的深度搞得比业务逻辑还复杂。print_topology一看层次是有了但连自己都分不清哪个组件在哪个分支上调试一个failure要在树上爬半天。这两种极端本质上是同一个问题没有想清楚树的每一层应该承载什么职责。这篇文章后面会给出我常用的分层标准test只管用例场景env只管环境组件的组装agent只管协议时序这一个方向scoreboard只管比对reference model只管行为建模。每一层只做一件事树的形状自然就清楚了。2. 核心细节解析与实操要点从根到叶逐层拆解树的每个节点2.1 根节点uvm_test验证用例的入口任何UVM验证平台的树根都是一个从uvm_test派生的类。你运行仿真时传给UVM的test名字通过factory机制或者仿真器选项UVM会实例化这个类然后从它开始往下build整棵树。test是整棵树的根意味着所有组件的parent最终都能追溯到它也意味着config_db的set操作如果从test这层开始整棵树都能看到。test的职责有两件事。第一创建env也就是uvm_env的派生类实例这是test下面唯一的必挂节点。第二配置用例场景也就是通过config_db把tctestcase相关的参数set下去比如数据包数量、延时模式、错误注入开关等。test的run_phase里通常就是sequence.start(env.agent.sequencer)把事情交给sequence去干。实操上有个很容易犯的错在test里直接访问agent的子组件写成env.agent.sequencer。这在UVM里虽然能编译过但打破了封装——如果后面agent内部结构变了test也要跟着改。正确的做法是通过config_db传递sequencer的句柄uvm_sequencer是uvm_component的子类可以放进config_db或者使用virtual sequencer。我在项目中一般用virtual sequencer这样test只跟virtual sequencer打交道agent内部的sequencer对test完全透明。2.2 uvm_env组装环境而不是实现逻辑env是树的第二层它的核心职责是组装创建agent、scoreboard、reference model、寄存器模型等然后把它们之间的TLM端口连起来。connect_phase就是专门干这个的因为connect_phase执行时所有组件都已经build完了句柄都有效可以安全地做端口连接。env不应该包含任何协议时序逻辑也不应该包含任何比对算法。它应该像一块PCB板只管把芯片组件焊上去、把线路TLM通道布好至于芯片内部怎么工作是组件自己关心的事。举个反面例子有些人习惯在env里直接写一个task用来启动sequence或者直接在env里做数据比对。这其实是在把env当test用或者当scoreboard用。树结构的层次感一旦被破坏后面代码的可维护性会急剧下降。正确做法是env只做build和connectrun_phase里最多做一些简单的同步控制业务逻辑一律下放到叶子组件。2.3 agent层协议时序的封装单元agent是树的第三层它封装了一个物理接口interface相关的所有验证组件。一个标准的agent包含driver、sequencer和monitor。driver负责把sequence_item激励事务转成引脚时序sequencer负责把sequence产生的item分发给drivermonitor负责采样引脚时序并转成transaction对象送给参考模型和scoreboard。对于协议接口agent一般分为两种模式active模式和passive模式。active模式下agent内部有driver和sequencer可以主动发送激励passive模式下只有monitor只观察不驱动。这种设计在总线的多个agent场景下特别有用——比如AHB总线上只有一个master agent是active的其他从机agent只需要monitor来观察事务。agent的设计要点是agent对上层暴露的接口要尽量简单。上层env只需要知道这是一个AHB agent它有一个sequenceractive模式它有一个analysis_portmonitor发出的transaction。至于agent内部是通过什么方式驱动时序的上层不需要知道。这样agent成为一个可复用的独立模块挪到其他平台也能用。2.4 driver、sequencer、monitor树上的三个干活的人driver和sequencer是UVM里最经典的“生产者-消费者”模式。sequencer内部有一个item队列sequence在sequencer上启动后产生的item会进入这个队列driver在run_phase里通过seq_item_port.get_next_item()向sequencer申请item拿到后驱动到接口上驱动完调用seq_item_port.item_done()。这两个组件是典型的握手关系而它们之间的连接发生在agent的connect_phase里driver.seq_item_port.connect(sequencer.seq_item_export);monitor则相对独立它不断采样接口信号把采到的信息打包成transaction通过analysis_port广播出去。analysis_port是一种一对多的TLM端口monitor不需要知道谁在监听它scoreboard和reference model各自去connect这个端口就行。这种松耦合设计是UVM验证平台可扩展性的关键。2.5 scoreboard和reference model数据的终点和参照系reference model参考模型和scoreboard计分板是验证平台里“结果比对”的两个核心组件。reference model根据输入激励模拟DUT的行为产生期望输出scoreboard接收来自reference model的期望数据和来自monitor的实际数据进行比对。这两个组件的输入都是通过analysis端口收到transaction因此它们通常是分析型组件的典型代表。在树的组织上reference model和scoreboard可以挂在env下面与agent平级。连接关系是monitor的analysis_port连接到reference model的analysis_exportreference model的输出analysis_port再连接到scoreboard的analysis_export同时monitor的实际输出也连接到scoreboard的analysis_export。这里有个常见的分层问题有人习惯把reference model的代码逻辑直接塞进scoreboard里省掉一个组件。短期看是省事了但长期看reference model是可重用的同一个算法模型可以用在多个项目scoreboard往往和具体用例绑定。分开挂树上是更合理的设计。2.6 寄存器模型一棵独立的子树寄存器模型uvm_reg_block/uvm_reg_map在树上的位置稍微特殊一点。reg_block通常挂在env下面但它内部是一个独立的层次结构reg_block里包含reg_mapreg_map里有多个reg每个reg里又有field。寄存器模型通过uvm_reg_adapter和agent的sequencer对接实现前门访问也可以通过uvm_reg_backdoor直接读写DUT内部的寄存器变量实现后门访问。实操中寄存器模型的镜像值mirrored value是验证工程师特别关注的问题。“镜像值”指的是UVM寄存器模型内部保存的、反映DUT寄存器当前值的一个副本。前门访问时UVM通过adapter发出总线读写事务然后更新镜像值后门访问时直接读写DUT内部信号同时更新镜像值。镜像是用来做寄存器比对的基础如果镜像值和DUT实际值不一致自检就会报错。树形结构对寄存器模型的影响主要体现在config_db的路径上。寄存器模型里的adapter需要在connect_phase里拿到sequencer的句柄而这个句柄往往要通过config_db从agent那边传过来。如果agent在树上挂的位置不对这个config_db的get就会失败寄存器前门访问就会因为sequencer句柄为null而直接报错。3. 实操过程与核心环节实现手把手搭一棵能跑的UVM树3.1 最小验证平台的分层搭建流程为了把树的搭建过程讲明白我用一个最简单的例子一个加法器DUT输入两个数输出两者的和。这个平台麻雀虽小五脏俱全刚好可以演示UVM树的标准搭法。第一步写一个从uvm_test派生的test类在build_phase里创建env并通过config_db设置必要的参数class adder_test extends uvm_test; uvm_component_utils(adder_test) adder_env env; function new(string name adder_test, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); // 创建env注意parent传this这样env挂到test下面 env adder_env::type_id::create(env, this); endfunction task run_phase(uvm_phase phase); adder_sequence seq; phase.raise_objection(this); // 防止test结束得太快 seq adder_sequence::type_id::create(seq); // 通过virtual sequencer启动或者直接env.agent.sequencer seq.start(env.agent.sequencer); phase.drop_objection(this); endtask endclass第二步写env类创建agent和scoreboard并在connect_phase里连接monitor和scoreboard的端口class adder_env extends uvm_env; uvm_component_utils(adder_env) adder_agent agent; adder_scoreboard sb; function new(string name adder_env, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); agent adder_agent::type_id::create(agent, this); sb adder_scoreboard::type_id::create(sb, this); endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); // 把monitor的analysis_port连到scoreboard的analysis_export agent.monitor.ap.connect(sb.analysis_export); endfunction endclass第三步写agent类在build_phase里创建driver、sequencer和monitor在connect_phase里连接driver和sequencerclass adder_agent extends uvm_agent; uvm_component_utils(adder_agent) adder_driver driver; adder_sequencer sequencer; adder_monitor monitor; uvm_analysis_imp_decl(_monitor) // 如果需要对外广播用analysis_port uvm_analysis_port #(adder_transaction) ap; function new(string name adder_agent, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); driver adder_driver::type_id::create(driver, this); sequencer adder_sequencer::type_id::create(sequencer, this); monitor adder_monitor::type_id::create(monitor, this); ap new(ap, this); endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); // 关键连接driver的seq_item_port连接sequencer的seq_item_export driver.seq_item_port.connect(sequencer.seq_item_export); monitor.ap.connect(ap); endfunction endclass3.2 connect_phase连接的本质与常见误区connect_phase是UVM树形结构里连接关系最集中的一个phase。这个phase执行的时候所有组件的build_phase已经完成所有子组件的句柄都已经有效所以可以做端口连接操作。connect_phase的执行顺序是自底向上也就是说叶子节点的connect先执行根节点的connect后执行。很多人误解“自底向上”的含义以为connect_phase一定先执行最深层组件的connect再执行浅层的。实际上UVM的phase调度是先执行所有组件的connect_phase按照树结构从叶子到根的顺序逐个执行所有组件都执行完了才进入run_phase。所以你在env的connect_phase里访问agent的monitor.ap一定是可以的因为agent子树的connect_phase已经执行完了。connect_phase里不能做的一件事是发送transaction。TLM端口连接只是把两个端口“接线”并没有数据流动。如果有人在connect_phase里调用ap.write()多半是在做测试性的调试代码这种写法会有风险因为connect_phase阶段其他组件的端口可能还没接好数据发出去没人收会造成transaction丢失且极难排查。3.3 树形结构的可视化验证print_topology树搭好了怎么验证UVM自带的uvm_top.print_topology()是最直接的检查手段。在test的build_phase结束后调用或者直接在run_test之前调用会打印出整个平台的树形结构。打印出来如果看到类似下面的结构说明树基本搭对了uvm_test_top (adder_test) env (adder_env) agent (adder_agent) driver (adder_driver) sequencer (adder_sequencer) monitor (adder_monitor) sb (adder_scoreboard)如果打印出来的树结构和这个差异很大比如组件没有挂在预期的parent下面或者出现了孤立节点parent显示为null那就要检查每个组件的create调用。记住type_id::create(name, parent)的第二个参数决定了这个组件挂在树的哪个节点上。如果你在env的build_phase里创建agent时忘了传this或者传了nullagent就会变成一棵独立的树不会挂在env下面config_db的路径就会对不上。3.4 树形结构中的访问路径与config_db传参UVM的config_db是基于树形结构路径的配置机制。set和get操作都依赖组件的层次路径这个路径就是树根到节点的全路径。比如要在test里给env.agent.driver设置一个参数可以写成uvm_config_db#(int)::set(this, env.agent.driver, num_items, 100);然后在driver的build_phase里uvm_config_db#(int)::get(this, , num_items, num_items);这个“env.agent.driver”路径就是基于树形结构的。如果你在test里创建env时parent传错了或者agent创建时parent传错了这个路径就失效了get到的会是默认值。config_db的set和get都发生在build_phase并且build_phase是自顶向下执行的所以test的build_phase先执行set先发生然后env的build_phase执行再然后agent的build_phase执行这样get才能拿到值。另一个容易踩的坑是config_db的第三个参数和第四个参数的关系第三个参数是变量名string第四个参数是变量值。get的时候第二个参数通常是空字符串表示从当前组件的build_phase里获取这个组件的路径就是get的匹配路径。如果你get的时候第二个参数传了别的路径UVM会把当前组件的路径和这个相对路径拼接起来很容易匹配失败。4. 常见问题与排查技巧实录树搭歪了会出哪些幺蛾子4.1 组件句柄为null的经典场景与排查思路UVM验证平台开发中最常见的错误就是句柄为null调用组件方法时直接报空指针错误。这类错误十有八九和树形结构有关。我举个实际排查过的案例一个平台的test里run_phase想通过env.agent.sequencer启动sequence结果报null pointer。排查的时候先看print_topology发现agent下面根本没有sequencer这个子组件。再查agent的build_phase发现创建sequencer的代码被条件编译宏包住了这个宏在当前的仿真配置下没有打开。这就是典型的树搭歪了的例子——代码里该创建的组件没创建树的结构和访问路径不一致。排查这类问题有个固定套路先print_topology看树的结构对不对再确认访问路径和树结构是否匹配最后看创建组件的代码是否满足创建条件。三个步骤走完大部分“句柄为null”的谜题都能解开。4.2 config_db get不到值的路径解析方法config_db get不到值或者get到的是默认值这类问题在验证平台开发里出现的频率极高。我总结了一套自己的排查流程第一步确认set和get的路径拼写完全一致。路径匹配是字符串精确匹配大小写、分隔符任何一个不一样都会失败。第二步确认set先于get执行。build_phase自顶向下执行如果set放在run_phase里而get在build_phase里那get一定拿不到。第三步确认set的时候this指向的组件路径符合预期。uvm_config_db::set(this, env.agent.driver, ...)这里的this如果跑错了组件拼接出来的路径就完全不对。这里有一个实用技巧在get失败的地方临时加一段uvm_config_db::exists的调用来确认配置是否存在配合打印出来的实际路径快速定位。UVM还支持通配符比如set(this, *driver, ...)但我建议只在调试时用通配符正式代码里尽量写完整路径因为通配符会让配置的意图变得不明确。4.3 phase执行顺序错乱为什么我的objection不生效UVM的phase执行顺序本身是树形结构决定的build_phase自顶向下connect_phase自底向上run_phase所有组件并行执行。如果发现某个组件的phase行为不符合预期先看这棵树本身是不是符合预期。一个很经典的问题是test的run_phase里raise_objection但仿真还是瞬间结束了。这种问题通常是因为objection raise的位置和drop的位置没配对或者raise发生在phase的raise_objection阶段之后。UVM里test的run_phase执行时phase的objection机制默认需要至少有一个raised的objection才能阻止phase结束。如果raise_objection放在了一个永远不会执行的fork块里那test的run_phase很快就跑完了树上的其他组件还没来得及干正事phase就结束了。排查phase问题的时候我习惯在关键组件的phase方法里加display打印打上组件路径和时间戳一眼就能看出phase的执行顺序是否符合树结构的预期。4.4 寄存器模型镜像值不对的常见原因与修正步骤寄存器模型的镜像值mirrored value是热词里特意提到的点说明很多人在这里卡过。镜像值不对寄存器自检就过不了display出来一片红。我遇到过的镜像值不对的场景大致有三类第一类是前门访问的路径问题。寄存器模型通过adapter访问总线adapter拿到sequencer的句柄然后启动sequence做总线读写。如果adapter的sequencer句柄是null或者连到了错误的sequencer上读写事务发不出去镜像值自然就停在初始值。这种情况下先查寄存器模型和agent的connect有没有连对再查adapter的配置。第二类是预测机制没使能。UVM寄存器模型默认有自动预测auto predict和显式预测explicit predict两种方式。如果自动预测没开启那么寄存器模型不会自动把adapter发出去的事务更新到镜像值里。需要调用uvm_reg_map::set_auto_predict(1)。如果不开auto predict就得用uvm_reg_predictor组件把monitor采到的总线事务通过predictor回填给寄存器模型。很多人在用了寄存器模型但没配predictor也没开auto predict镜像值就永远不变。第三类是后门访问的路径和hdl_path配置问题。后门访问通过uvm_hdl_read和uvm_hdl_write直接操作DUT信号需要寄存器模型里每个reg配置正确的hdl_path。如果hdl_path写错了后门访问会静默失败镜像值也不会更新。修正这类问题我建议的顺序是先确认前门访问的transaction有没有发出去在adapter的reg2bus/bus2reg里打display再确认predictor或者auto predict有没有生效最后检查hdl_path。三步走完镜像值的问题基本能定位到具体环节。4.5 热词里那个“非常醒目的pass和fail”是怎么实现的很多人搜“uvm中最终display显示非常醒目的pass和fail的代码”其实就是想在仿真结束的时候用醒目的打印信息告诉你这轮用例过了还是挂了。这个功能本身不难但要让它靠谱关键是它依赖于整棵树的健康运转——只有树搭正确了所有组件的比对结果都汇总到scoreboard最终的pass/fail才有意义。一个常见的做法是在test的check_phase或者report_phase里做判断使用UVM的uvm_report_server来获取当前测试的uvm_error和uvm_fatal数量再结合自己的比对结果标志位决定display打印“TEST PASSED!!!”还是“TEST FAILED!!!”function void report_phase(uvm_phase phase); uvm_report_server server; int error_count; int fatal_count; super.report_phase(phase); server uvm_report_server::get_server(); error_count server.get_severity_count(UVM_ERROR); fatal_count server.get_severity_count(UVM_FATAL); if (error_count 0 fatal_count 0 env.sb.tc_pass) begin $display(); $display( TEST PASSED!!! ); $display(); end else begin $display(); $display( TEST FAILED!!! ); $display(); $display(Total errors %0d, Fatal %0d, error_count, fatal_count); $display(); end endfunction注意tc_pass这个标志位它应该由scoreboard根据比对结果更新。scoreboard在树上是独立的一个节点它内部负责把每个transaction的比对结果累积起来。如果树上scoreboard的analysis_export没接好tc_pass就永远是默认值pass/fail的显示就失去了意义。所以我一直强调display只是最后那一嗓子前面那一整棵树的正确性才是真正的核心。5. 树形结构对UVM实战能力的决定性影响5.1 从“会写代码”到“会搭平台”的分水岭我带过不少新人一个很明显的分水岭是刚入门的时候能把一个个UVM组件写出来——driver会写、monitor会写、scoreboard也会写但到了要自己搭一个完整验证平台的时候往往就卡住了。卡住的原因不是某个组件不知道怎么实现而是不知道这些组件应该怎么组织成一棵树。能画出这棵树并且能解释清楚每个节点为什么挂在这个位置的人搭出来的平台基本一次就能跑通。反过来画不出树、全凭感觉写的人平台第一次编译通过的概率很低即使通过也是靠反复试错调出来的后期维护成本极高。5.2 树的形状决定排错效率平台跑出问题之后排查路径也完全由树的形状决定。比如一个数据比对失败你先从scoreboard入手看它从哪个analysis_port拿到的数据再顺着这个端口回到monitor看monitor采样到的信号波形最后回到transaction生成的地方看是激励就错了还是采样就错了。每一步都是在树上做“溯洄”。树清晰溯洄就快树混乱溯洄就在一团乱麻里打转。我在实际项目里有一个习惯每搭完一个模块先print_topology确认树形结构然后再往下写代码。这个习惯帮我避免了很多“回头改结构”的灾难。树是平台的地基地基没打好就往上盖楼后面每一层都会歪。5.3 关于“待更”的两个字标题里写了“待更”我把它理解成两层意思。一层是这篇文章本身还有很多可以继续写下去的内容——寄存器模型的树、virtual sequencer的树、sequence library的组织方式每一块展开都是一篇长文。另一层是你自己的知识树也处于“待更”状态今天你把Hierarchy这棵树的脉络理清了下次在print_topology里看到任何一朵奇怪的“分枝”你都能一眼看出问题在哪再下次当你自己设计一个多agent、多接口的大型验证平台时你会发现你不再需要抄demo了因为树的形状已经在你的脑子里了。我个人的体会是UVM学到后期真正拉开差距的从来不是你会用多少个UVM的类而是你能不能在任何时刻都清楚地知道当前这个组件在树上处于什么位置、它和周围的节点是什么关系、数据流经它的路径是什么。这些问题的答案都在这棵Hierarchy树里。