ARTICLE DETAIL

资讯详情

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

深入理解UVM组件树:构建原理、核心机制与调试技巧

深入理解UVM组件树:构建原理、核心机制与调试技巧 1. 先搞明白UVM的Hierarchy树到底是什么接触UVM验证平台的人几乎每天都会和层次树打交道但真正把整棵树的来龙去脉说清楚的人并不多。很多初学者搭环境靠的是照抄模板顶层叫啥、env里挂几个agent、scoreboard放哪个位置全凭惯性。结果一到调试阶段就抓瞎为什么config_db取不到值为什么某个组件build_phase没执行为什么寄存器模型读回来的镜像值不对这些问题十有八九都出在没吃透UVM组件树的构建规则上。UVM的Hierarchy树简单说就是所有uvm_component组件按照父子关系组织起来的一棵倒挂的树。树的根是uvm_top它是UVM环境启动时自动创建的一个uvm_root对象你写的test继承自uvm_test被uvm_top挂成子节点。再往下test的build_phase里创建envenv的build_phase里创建agent、scoreboard、reference modelagent的build_phase里创建sequencer、driver、monitor。如此一级一级往深里挂最终形成一棵完整的组件树。这棵树的地位有多重要可以这么说UVM平台里几乎所有核心机制都建立在这棵树上。phase调度靠它决定先后顺序uvm_config_db靠它实现路径匹配TLM端口连接靠它确定组件实例日志打印靠它生成层级前缀甚至波形里的UVM Hierarchy面板也是用它来展示的。把树理解透了验证平台的搭建和调试能力会有一个质的提升。这一篇我打算把UVM树形结构的原理、构建过程、各层节点职责、与关键机制的关联以及我实际踩坑积累的调试技巧一次性讲透。适合正在搭UVM环境的朋友也适合准备UVM验证面试、想把层次机制讲清楚的工程师。1.1 树上的每个节点都不许裸奔new函数的parent参数UVM组件树的第一块砖是uvm_component的构造函数。所有组件在new的时候都要带上两个参数组件名字name和父亲parent。这个parent参数就是树形结构的挂载点。组件只有通过new把自己挂到某个父节点下面才能真正进入这棵树的体系享受到phase调度、config_db、层次访问这些待遇。比如最常见的env代码class my_env extends uvm_env; my_agent agt; 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); agt my_agent::type_id::create(agt, this); scb my_scoreboard::type_id::create(scb, this); endfunction endclass这里的create(agt, this)第二个参数this就是父节点。UVM工厂create内部会调用相应类的构造函数把name和parent传进去。父节点在树里的位置决定了子节点在树里的位置而树里的位置又决定了很多隐含行为。我在实际项目里见过不少新手把parent传成null或者随便传一个不相关的组件结果就是组件虽然创建出来了但它不属于你期望的那棵子树。最典型的表现是build_phase执行顺序不对、config_db的路径匹配不上。等你回头查full_name才发现组件挂到了一个你完全没想到的地方。这个问题很隐蔽因为编译和仿真都不会报错只有行为对不上。所以从一开始写组件类就要养成良好的习惯构造函数保留name和parent两个参数super.new一定别漏build_phase里create子组件时第二个参数老老实实传this。1.2 树是怎么长出来的build_phase的递归魔力组件树的生长不是靠某个全局注册表一次性铺开的而是靠phase机制逐层驱动、递归展开的。UVM的build_phase是个自上而下的phase从uvm_top开始先执行它的build_phase然后在build_phase里创建test组件test的build_phase创建envenv的build_phase创建下面的子组件。这一层层的create和下一层的build_phase环环相扣把树从根到叶一层层长出来。这里有个关键点build_phase里没有显式调用父类或者子类的某种递归构建函数那树是怎么自动长下去的其实秘密就在于UVM在调度build_phase时会自动遍历当前组件的所有子组件然后依次执行它们的build_phase。换句话说当你在env的build_phase里执行了create(agt, this)UVM调度器会在env的build_phase结束后自动找到agt这个子组件继续执行agt自身的build_phase在agt的build_phase里再创建driver、monitor、sequencer。这个递归展开的过程是UVM framework自动完成的你只需要保证每个组件的build_phase里把该有的子组件都create出来。所以判断一棵树长得对不对逻辑上很简单哪个组件的build_phase里创建了谁谁就是它的子节点。但要注意create方法本身只是new了一个对象并挂到了父节点下这个对象接下来能不能走完自己的build_phase取决于它是不是真的被挂到了树上。如果某个组件在create之后、build_phase之前出了异常或者parent传错树的生长过程就会被破坏。这也是为什么UVM的build_phase建议只做创建子组件和配置读取而不要写复杂的逻辑一旦在这里出错整棵树的构建都会崩掉。2. 树里每个节点都是谁核心组件的层级定位把树的结构理解了再看每个节点在树里的典型分布。一棵常规的UVM验证平台树大致长这样uvm_top (uvm_root) └── uvm_test_top (my_test) └── env (my_env) ├── agt (my_agent) │ ├── sqr (my_sequencer) │ ├── drv (my_driver) │ └── mon (my_monitor) ├── ref_model (my_reference_model) ├── scb (my_scoreboard) └── reg_model (my_reg_block)当然实际项目中树的深度和宽度会复杂得多。比如agent里可能分成master agent和slave agentenv里可能有多个agent、多个scoreboard还可能挂virtual sequencer、寄存器模型、覆盖率收集器等。但不管多复杂每个节点在树里的位置都是有讲究的不是随便挂的。2.1 test和env树最顶上的两个管理者test位于整棵树最接近根的位置。UVM验证平台运行后uvm_root会自动创建一个名为uvm_test_top的实例类型就是你通过run_test()传入或者通过工厂override之后指定的test类。test通常负责组织整个验证环境的开合在build_phase里创建env在connect_phase里做必要的连接在run_phase里启动sequence在report_phase里做覆盖率检查和结果汇总。test下面一般就是env。env是验证环境的顶层容器负责把agent、scoreboard、reference model、寄存器模型这些零件组装起来。为什么要多包一层env而不是把组件全部挂在test下因为env是可复用的。一个成熟的项目里env往往和DUT接口一一对应换一个testenv基本不用动只要test里通过配置或override换掉某些组件行为就行。这种分层的核心价值就是复用和隔离test管场景env管结构。我在做多test验证的时候深有体会如果env组织得清晰新增一个testcase的成本极低只需要继承base_test改一下约束或sequence就行。如果env里逻辑乱组件挂错位置每个新testcase都要跟着改env那维护成本就爆炸了。所以树形结构的顶层设计直接影响验证平台的可扩展性。2.2 agent内部的小树driver、monitor、sequencer的父子关系agent是UVM里一个非常经典的单元式组件。一个agent通常对应DUT的一类接口比如AXI agent、APB agent、UART agent。agent内部一般包含三个核心成员sequencer、driver、monitor。这三者在树里都是agent的直接子节点但职责完全不同。driver负责把sequence产生的transaction转换成接口时序驱动给DUTmonitor负责采样接口上的信号把时序还原成transactionsequencer则负责仲裁和分发sequence产生的transaction给driver。在树的视角里三个组件平级都是agent的儿子。driver和sequencer之间通过seq_item_port和seq_item_export建立TLM连接这个连接发生在connect_phase。而monitor和driver之间的信号采样一般不通过树结构访问而是通过virtual interface。这里有个常被忽视的点monitor到底该挂在agent下还是应该站在env下这取决于监控的是DUT接口还是agent内部激励。如果是接口协议监控通常monitor在agent内部和driver共享同一个virtual interfacefulfilled协议级的功能如果是要收集整个环境的信息比如总线事务统计那可能需要独立的参考监控组件放在agent外面。挂在不同的位置full_name路径不同config_db路径不同波形的hierarchy也不同。这个选择要结合DUT接口划分和验证需求来定。2.3 scoreboard、reference model、寄存器模型该挂哪scoreboard和reference model通常直接挂在env下面和agent平级。这样设计的好处是agent负责收发reference model负责算scoreboard负责比三个角色职责清晰。reference model通常需要从driver侧拿到激励transaction从monitor侧拿到DUT输出所以它的TLM端口要接到agent的monitor和分析端口上。scoreboard则接收reference model的期望值和DUT实际输出做对比。寄存器模型uvm_reg_block比较特殊。它一般以组件的形式挂在env下面比如reg_model my_reg_block::type_id::create(reg_model, this)。寄存器模型在树里的位置决定了它通过哪个sequencer访问总线。在env的connect_phase里要把寄存器模型的default_map的sequencer和agent里的sequencer关联起来这样寄存器模型发起的读写操作才能通过总线实际访问DUT寄存器。这里特别说一下热词里提到的uvm寄存器模型镜像值。寄存器模型里有两套值一套是期望值desired value一套是镜像值mirrored value。镜像值是寄存器模型认为当前DUT寄存器里的实际值。用reg.mirror()可以读取DUT寄存器并更新镜像值用reg.predict()可以根据monitor采集的值预测镜像值。镜像值的作用是让验证环境在软件层面随时知道硬件寄存器的实际状态避免频繁发起真实总线访问。而镜像值要维护得准前提是寄存器模型正确挂在了树里并且通过正确的sequencer访问总线。如果树挂错了读回来的镜像值自然就乱了。3. 树形结构如何支撑UVM的核心机制树不只是用来展示的它是UVM许多核心机制运转的骨架。掌握了树你就能把UVM的很多魔法看穿。3.1 phase调度与树的关系为什么build能自上而下UVM的phase调度可以说是树形结构最大的受益者。以build_phase为例UVM的build_phase一定是从根到叶、自上而下执行的。为什么要这样因为父组件要在build_phase里创建子组件如果子组件先执行build_phase它连对象都不存在build_phase根本无从而起。所以UVM调度器先执行根的build_phase等根的子组件全部创建完毕再执行这些子组件的build_phase一层层往下。和build_phase相反run_phase等耗时的phase是所有组件并行执行的而connect_phase是在build_phase全部执行完之后自下而上执行的。为什么connect要自下而上因为连接通常发生在平级或跨层组件之间而且子组件的端口要先准备好父组件才能把它们连起来。比如agent内部的driver和sequencer连接必须在driver和sequencer都完成build之后才能进行env要连接agent的monitor和scoreboard的analysis端口也得等monitor和scoreboard都就绪。这个先后顺序本质上是树形结构决定的一种拓扑序。实际项目中phase顺序错乱带来的问题不少。比如有人想在connect_phase里创建子组件这是不行的因为connect_phase执行的时候树已经建完了。还有人想在run_phase里动态创建组件虽然UVM支持动态创建但组件的build_phase不会再被调度后续的phase也不会自动跑很容易踩坑。理解了树和phase的关系这种问题就很好判断了。3.2 config_db与树路径就是树的门牌号uvm_config_db是UVM平台里最常用的配置传递机制它和树形结构的关系极其紧密。config_db的set/get操作都依赖一个层次路径这个路径实际上就是组件树里的full_name。uvm_config_db#(int)::set(null, uvm_test_top.env.agt.drv, num_transactions, 100);这里的uvm_test_top.env.agt.drv就是driver组件在树里的完整路径。config_db在get的时候会沿着这个路径找到对应的组件然后把它设置的值传给该组件的config字段。如果路径写错或者树的结构和路径不一致get端拿到的就是null。常见的一个坑是在test的build_phase里给env下面的组件set配置但此时env还没有被create路径uvm_test_top.env还不存在。UVM的config_db设计上允许先set后get所以这个看起来还不存在的路径并不会报错但如果拼写和实际路径不一致在env的build_phase里get时就会失败拿到的是默认值。排查这类问题我通常会在get之后做个uvm_info打印第一时间暴露路径问题。3.3 层次的自动化操作get_parent、get_children和print树形结构不光是被动承载UVM还提供了一套API做主动遍历。常用的有get_parent()获取父节点get_children()获取所有直接子节点get_num_children()获取子节点数量get_full_name()获取完整路径。这些API在调试和通用工具类里非常有用。最实用的是uvm_top.print()也叫uvm_root::get().print()。它会把整棵组件树的结构、每个组件的类型名和层级关系以ASCII树的形式打印出来。仿真日志里看到的树形结构图就是这个方法打出来的。遇到环境结构不清晰、组件创建时机不对的问题我第一件事就是在end_of_elaboration_phase里加一句uvm_top.print()一眼就能看出哪里有组件没有挂到预期的位置。另外uvm_component还支持按名字查找子组件比如get_child(drv)以及uvm_top.find(uvm_test_top.env.agt.drv)这种全路径查找。这些工具在写通用组件、动态配置脚本时非常好用能少写很多硬编码的层次引用。4. 实操从零搭一棵标准的UVM组件树理论聊够了来点实操。我以一个典型的APB寄存器配置类验证平台为例把树从根到叶完整搭一遍代码可以直接抄。4.1 顶层模块和接口树外的土壤组件树的根uvm_top跑在仿真里但它的土壤是顶层module和接口。顶层module里会实例化DUT把时钟复位拉起来然后调用run_test()启动UVM环境。接口interface是DUT和验证环境之间的桥梁它们虽然不是uvm_component不是树的一部分但通过virtual interface传递到树的各个节点里。module tb_top; reg clk; reg rst_n; apb_if apb_if0(clk, rst_n); dut u_dut( .clk(clk), .rst_n(rst_n), .pclk(apb_if0.pclk), .psel(apb_if0.psel), // ... 其他连接 ); initial begin clk 0; forever #5 clk ~clk; end initial begin rst_n 0; #100 rst_n 1; run_test(my_test); end endmodule注意一个细节接口在传入组件之前需要在顶层module里用initial块通过config_db以虚拟接口的形式set出去。这是很常见的操作也是树外和树内交接的门面。4.2 从test到env确定树的顶层骨架test是树的顶层骨架。我的习惯是先写一个base_test把env的类型、接口配置、公共sequence的启动都放在里面具体的testcase叠在base_test上只重写需要调整的部分。class base_test extends uvm_test; my_env env; uvm_component_utils(base_test) function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); uvm_config_db#(virtual apb_if)::set(null, uvm_test_top.env.agt.drv, vif, tb_top.apb_if0); uvm_config_db#(virtual apb_if)::set(null, uvm_test_top.env.agt.mon, vif, tb_top.apb_if0); env my_env::type_id::create(env, this); endfunction endclassbuild_phase里两个config_db的set路径分别是driver和monitor的full_name路径。因为set发生在这两个组件创建之前所以用null作为第一个参数路径字符串作为第二个参数这是config_db的标准用法。4.3 env、agent、driver/monitor/sequencer树的枝干env的build_phase里把agent、scoreboard、reference model、寄存器模型都创建出来。agent的build_phase里再创建driver、monitor、sequencer。这一层的代码结构非常固定但细节要小心。class my_env extends uvm_env; my_agent agt; my_scoreboard scb; my_reg_block reg_model; my_reference_model ref_model; uvm_component_utils(my_env) function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); agt my_agent::type_id::create(agt, this); scb my_scoreboard::type_id::create(scb, this); ref_model my_reference_model::type_id::create(ref_model, this); reg_model my_reg_block::type_id::create(reg_model, this); reg_model.configure(null, null); // 具体配置按需 reg_model.lock_model(); reg_model.default_map.set_sequencer(agt.sqr, null, 0); endfunction virtual function void connect_phase(uvm_phase phase); super.connect_phase(phase); agt.mon.ap.connect(ref_model.analysis_export); ref_model.exp_ap.connect(scb.exp_fifo.analysis_export); agt.mon.ap.connect(scb.act_fifo.analysis_export); endfunction endclass由于agent内部的driver和monitor在agent的build_phase里创建所以env的build_phase里create(agt, this)之后再访问agt.sqr其实是不行的因为agent的build_phase还没执行。所以对agent内部的连接应该放在connect_phase里此时整棵树已经建完agt.sqr已经存在。如果你在build_phase里直接访问agt.sqr拿到的是null这也是一个常见错误。4.4 叶子节点的自我修养叶子也要写对build和connect叶子组件看起来没有子节点但它们的build_phase通常要做两件事从config_db获取配置参数比如virtual interface以及初始化内部成员。connect_phase则可能把自己的analysis端口连到上层组件的FIFO上。class my_driver extends uvm_driver #(my_transaction); virtual apb_if vif; uvm_component_utils(my_driver) function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual apb_if)::get(this, , vif, vif)) uvm_fatal(NOVIF, virtual interface not set for driver) endfunction virtual task run_phase(uvm_phase phase); // 驱动事务的时序逻辑 endtask endclassuvm_config_db#(virtual apb_if)::get(this, , vif, vif)这里有个细节get的第一个参数是当前组件第二个参数是相对当前组件的子路径传表示就查当前组件本身。第三个参数是config_db的字段名要和set端保持一致。如果不一致get失败会触发uvm_fatal这其实是个好事能尽早暴露配置缺失问题。5. 树形结构里的坑我踩过的和替你们踩的树形结构看着简单实际操作中问题层出不穷。这一节把我遇到的、以及帮别人排查过的高频问题整理一下按实战价值排序。5.1 build_phase里访问子组件为什么总是null这个问题几乎每个UVM新手都会遇到。在env的build_phase里创建完agent之后马上用agent.drv或者agent.sqr结果发现是null。原因我之前说过了build_phase是自上而下的env的build_phase执行时agent的build_phase还没跑所以agent内部的drv、sqr、mon都还没有创建。正确的做法是所有组件之间的连接放到connect_phase里做所有跨组件的配置传递放到build_phase之后、run_phase之前的阶段来做。connect_phase的调度顺序是自下而上的agent内部的连接会先于env级别的连接执行所以agent内部connect好之后env再connect就有完整的端口可用了。5.2 路径写错config_db静默失败怎么办config_db的set/get有一层静默失败机制路径不存在不会报错get端拿到的就是默认值。这很坑因为表现往往是某个配置没生效但日志里完全看不出异常。我的排查习惯是三步走。第一步在config_db的get端打印get到的值确认是不是默认值第二步打开UVM_CONFIG_DB_TRACE仿真选项它会打印config_db的set和get的全过程路径匹配情况一目了然第三步在仿真日志里搜Configuration或config_db关键字查看是否有什么warning提示。UVM本身会打印config_db访问的超时alert但很多时候不详细靠trace选项最直观。5.3 动态创建组件树上能不能后长新枝UVM支持在run_phase里动态创建组件比如my_agent::type_id::create(agt2, this)。但我要提醒一句动态创建组件要非常谨慎。因为一旦运行进入run_phase树基本已经定型动态创建的组件不会再有build_phase、connect_phase等被调度的机会某些版本可能例外它也不会参与到默认的run_phase任务里去。我实际遇到过一个场景想在测试中途动态创建一个monitor专门观测某个信号的变化。结果发现这个monitor虽然能new出来但它的run_phase不会自动执行要自己手动fork一个task。最终我建议同事改用静态创建加enable开关的方式在run_phase里通过语法控制是否收集数据效果更好、更可控。除非你有非常明确的需求否则不建议动态长枝。5.4 树的根uvm_top一个容易被忽略的幽灵节点uvm_top是树的根但很多人在代码里几乎感觉不到它的存在。它是在仿真开始前由UVM自动创建的类型是uvm_root对应的变量可以通过uvm_root::get()拿到。uvm_root有几个常见的用法。一是uvm_top.print()打印整棵树这个前面提过。二是uvm_root::get().run_test(my_test)显式指定测试名。三是在某些通用组件里用uvm_root::get().set_report_verbosity_level_hier()设置全局日志等级。uvm_top本身是个组件也参与phase调度但它的build_phase里没什么可造的主要就是传递test类型。有些人在写全局配置工具时喜欢用null作为config_db的路径起点从uvm_top的角度看设置的是从根开始匹配的路径这个要理解清楚。6. 面试常见问题Hierarchy树怎么讲才不会漏uvm验证面试是热搜词那顺便把我面试别人和被面试时经常遇到的相关问题整理一下供大家自检。6.1 一页纸讲清UVM树形结构如果被问请介绍一下UVM的组件树我会用三个层次来讲第一树由uvm_component通过new(name, parent)构成根是uvm_top每个组件通过build_phase创建子组件树就沿着这个递归过程生长。第二树是phase调度、config_db路径、TLM连接、日志打印等机制的基础。第三树的每个节点都有full_name树的组织方式直接影响验证平台的可复用性和可调试性。这样讲的好处是逻辑完整从怎么长出来到有什么用再到怎么用既有原理也有实践。如果面试官继续追问再展开build_phase的递归过程、connect_phase自下而上的原因、config_db路径匹配规则等细节。6.2 build_phase和connect_phase为什么一个自上而下、一个自下而上这是面试里能体现真的懂的高频问题。build_phase自上而下是因为父组件要先创建子组件子组件的build需要父组件先跑。connect_phase自下而上是因为连接需要子组件的端口先准备好父组件的连接才能引用它们并且底层agent内部的连接要先于上层env的跨agent连接顺序才能对。讲清楚这个顺序之后可以顺带提一下run_phase是所有组件并行执行的这一点和树形结构关系不大但能体现你对phase调度整体框架的理解。6.3 树形结构怎样影响验证平台的可复用性这一问通常出现在偏架构设计的面试里。可以从两层讲第一env和agent的分层让一块接口一个agent成为高内聚的单元可以独立复用换DUT只需要换env组装方式。第二config_db路径和树路径的绑定让组件可以通过配置文件动态调整参数和结构而不用改代码。如果树的层次划分得当新项目里粘贴旧代码时只需要改路径字符串就能把组件挂到新树上。我还会强调树形结构是一种约定优于配置的体现UVM把你必须遵守的层次规则固化成了phase调度和config_db路径机制只要你按规则组织代码平台就会自动帮你处理很多繁琐的时序和连接问题。这一点在面试里说出来会让人觉得你真的理解UVM的设计哲学。7. 再深一层树形结构背后值得琢磨的几个方向树形结构本身不难但顺着它往下想能延伸出不少值得琢磨的点对深入理解UVM很有帮助。7.1 树与TLM连接的寄生关系TLM端口连接本质上是一种组件间的引用关系但它和树有很强的寄生关系。端口必须在组件build完成后才能连接连接时通过port.connect(export)建立引用。树决定了组件的生命周期而TLM连接在树的框架内完成。如果某个组件在树里被替换比如通过工厂override换了一个更复杂的agent它的TLM连接通常也要跟着适配。所以TLM端口和树结构之间是端口跟着组件走、连接顺应树顺序的关系。7.2 树的打印和调试怎么让整棵树一眼看懂UVM的uvm_top.print()默认打印的是组件名、类型名和子组件列表格式较简单。如果觉得不够直观可以写一个递归函数遍历组件树自定义打印每个组件的类型、层次路径、配置字段等信息。我写过一个简单的打印函数function void print_tree(uvm_component comp, string prefix ); uvm_component children[$]; comp.get_children(children); $display(%s%s [%s], prefix, comp.get_name(), comp.get_type_name()); foreach (children[i]) print_tree(children[i], {prefix, }); endfunction调试时在end_of_elaboration_phase调用它能输出一个非常清晰的树状结构比默认print更符合自己的阅读习惯。这也是对树形结构API的一个很好的练手项目。7.3 树形结构与UVM寄存器模型的镜像值维护热词里专门有uvm寄存器模型镜像值可见这个话题确实是验证工程师关注的重点。寄存器模型本身就是一棵树uvm_reg_block是根下面挂uvm_reg、uvm_reg_map、uvm_mem等节点。这些节点不是uvm_component所以不参与phase调度但它和组件树有交接口——寄存器模型整体是env里的一个组件它通过default_map的sequencer访问总线。镜像值的维护核心是两点一是读写操作之后寄存器模型内部的镜像值要同步更新二是外部访问比如sequence直接通过bus访问寄存器时如果希望模型跟上需要用到predict操作。如果寄存器模型挂在树里的位置不对或者sequencer的路径配错镜像值就会出现看似读到了、实则没更新的奇怪现象。排查镜像值不准的问题第一步永远是检查寄存器模型在树里的挂载位置和sequencer连接第二步才是检查sequence的访问方式对不对。UVM的树形结构说到底是UVM一切机制的地基。地基稳了上面盖什么楼都不慌地基歪了楼再漂亮也经不起调试风暴。花点时间把树吃透值。
返回列表