ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

UVM验证平台树形结构深度解析:从组件挂载到寄存器模型

UVM验证平台树形结构深度解析:从组件挂载到寄存器模型 干验证这一行的没有人能绕过UVM里的那棵树。不管你是刚把UVM实战那本书啃到第三章的初学者还是在面试中被问到“UVM为什么设计成树形结构”的老油条这棵从uvm_top长出来的树基本决定了你整个验证平台能不能站稳、能不能扩展、能不能在项目里被队友看得懂。这篇文章我想认真聊一聊UVM验证平台的Hierarchy树形结构从组件为什么要有父节点到build_phase怎么把树一层层种出来再到寄存器模型在树上的挂载方式最后还附上我在项目里踩过的坑以及面试官最爱问的几个树相关的问题。这篇属于“待更系列”的第一篇先写透架构部分后面再陆续补phase、sequence机制、寄存器模型镜像值这些更细的点。1. 先从“为什么是树”说起——UVM把验证环境的骨架定义清楚了1.1 一棵树是怎么长出来的你打开任何一个稍微正式一点的UVM验证平台都会看到类似这样的组件结构uvm_top └── uvm_test_top └── my_env ├── my_agent │ ├── my_sequencer │ ├── my_driver │ └── my_monitor ├── my_model └── my_scoreboard这个结构本质上是一棵多叉树。树根是uvm_top它是UVM内部维护的一个全局根节点所有test_case被启动之后都会挂在它下面。紧接着是uvm_test或者叫uvm_test_top然后test去创建envenv再去创建agent、model、scoreboardagent再去创建driver、sequencer、monitor。这棵树的生长规则其实非常简单每个组件在new的时候必须指定两个参数第一个是组件名字第二个就是它的parent。例如function new(string name, uvm_component parent); super.new(name, parent); endfunction这一句话是九十percent环境搭建的基础。如果你忘了把parent传进去或者在create的时候没有把this传进去那这个组件就会变成孤儿节点挂不到树上phase机制和config_db都找不到它后面调试的时候非常痛苦。为什么必须是树而不是随便一个列表或者哈希表因为验证平台需要的不是数据存储而是一种“有层次的调度关系”。树形结构天然支持父节点统一管理子节点创建、配置、连接、启动、结束每一步都可以从根节点递归下去。而且树形结构让每个组件都有一个唯一的全路径名例如uvm_test_top.env.agent.driver这在调试、打印、配置传参时都非常方便。1.2 uvm_component与uvm_object只有组件才能上树UVM里有两类核心对象一类是uvm_object另一类是uvm_component。很多人一开始会把这两个搞混尤其是transation、sequence这些uvm_object总觉得它们应该也是树的一部分。实际上只有uvm_component及其派生类才能进Hierarchy树形结构。为什么因为uvm_component有parent、有层次、有build/connect/run等phase它是有“生命周期”的它负责环境和平台的组织。而uvm_object是轻量级的对象比如uvm_sequence_item、uvm_reg_item、uvm_config_object它们没有parent没有独立的phase调度不需要在树上占据一个节点。这就是UVM设计里的一个关键点树形结构承载的是“验证平台的物理组织”而object承载的是“数据与控制流”。你可以把component想象成一个公司里的部门把object想象成部门之间流转的工单。部门有上下级汇报关系工单没有工单就是文件本身。所以你在搭平台的时候要问自己一个问题这个东西要不要参与phase调度要不要被config_db按路径配置如果要它就必须是component如果它只是一次性的数据包、寄存器描述、配置对象那它就是object不该强行挂到树上。2. 树的搭建规则build_phase才是真正的“种树”现场2.1 build_phase树是在这里种出来的我在前面说了new的时候要传parent但new本身只是给树预留了一个位置真正让树枝繁叶茂的是build_phase。UVM在仿真启动之后会从根节点开始自顶向下递归执行所有组件的build_phase。这意味着什么意味着你站在任何一个component的build_phase里面你都可以安全地创建它的子组件因为父节点先build子节点后build。你不需要担心子组件还没创建因为你是它的父亲你正在创建它。反过来你也绝对不可以在build_phase之外再去创建一个需要上树的子组件比如在connect_phase或者run_phase里面突然create一个agent这在绝大多数情况下是违规操作phase机制不会再去调它的build_phase它的配置也收不到。一个标准的build_phase写法长这样function void build_phase(uvm_phase phase); super.build_phase(phase); // 从config_db中获取配置 if (!uvm_config_db#(int)::get(this, , packet_num, packet_num)) packet_num 100; // 创建子组件 env my_env::type_id::create(env, this); endfunction这里有几个关键点值得说透。第一点是super.build_phase(phase)千万别漏。父类uvm_component的build_phase里会做很多基础工作包括处理factory重载和默认配置你漏了它很多隐性问题会在后面爆炸。第二点是uvm_config_db::get(this, , packet_num, packet_num)。config_db的机制也是沿着树走的它先在你当前的组件节点以及它的祖先节点路径上搜索匹配的配置项。路径匹配和组件的full name强相关所以如果你树挂得不对config_db就搜不到最终只能靠默认值兜底而默认值往往不是你想用的值。第三点是type_id::create(env, this)。factory create会先查一下有没有类型重载如果有就创建重载后的类型如果没有就创建默认类型。第一个参数是组件实例名字第二个参数是parent。很多新手会写成type_id::create(env, null)结果环境没挂到当前test下面后续phase顺序和配置传递全部错乱。2.2 向上寻根uvm_top与parent的妙用在build阶段你经常会遇到一个需求自己的组件需要拿到全局的某个资源或者需要调用env层的方法。这时候你不能硬编码一个全局变量最稳的写法就是通过parent句柄向上寻根。比如在driver里想拿到配置对象可以层层往上找my_config cfg; if (!uvm_config_db#(my_config)::get(this, , config, cfg)) uvm_fatal(NOCFG, config not found)但如果你起的组件名字很不巧和别人撞了或者你写代码时把parent传错了那么get就会失败。排查这类问题比较有效的方法是在build_phase里打印一下自己的全名uvm_info(TRACE, $sformatf(component full name: %s, get_full_name()), UVM_HIGH)get_full_name()沿parent链逐级拼接而成名字对不上八成是树挂错了。还有一个更直观的办法调用uvm_top.print_topology()这一行代码会在仿真开始阶段把整棵组件的家族图谱打出来我在调试环境搭建问题时几乎每次都会先看一眼打印结果。2.3 树的生长顺序和factory机制稍微深入一点讲build_phase自顶向下执行是一个大原则但在一个component内部如果你创建了多个子组件这些子组件之间的build顺序取决于你在build_phase里调用type_id::create的先后顺序。UVM不会因为你先create了driver后create了sequencer就自动帮你调整顺序它严格按代码执行顺序来。那这种顺序有什么影响影响主要体现在初始化依赖上。比如scoreboard想在build_phases里拿到agent的analysis_port连接但analysis_port在connect_phase才完成绑定所以你不能在build_phase里做跨组件的连接检查一定要等到connect_phase之后。这个和树的执行顺序相关如果没搞清楚很容易写出“build里检查连接”这种注定失败的错误代码。factory机制和树的结合点在于你重载某个类之后不仅仅是换了一个类名它连带着会对整棵树上所有实例化该类型的位置生效。所以重载一个agent时它下面的driver、sequencer、monitor只要它们都是通过factory create创建并用了type_id就都能被一致地替换。这也从侧面解释了为什么UVM官方强烈建议组件创建必须用type_id::create而不是直接new——直接new就绕过了factory树还是那棵树但重载机制失效了。3. phase机制树形结构在仿真时间上的投影3.1 phase从树根开始往下跑搭建好了树之后接下来就是考验树形结构如何和phase机制协同工作。UVM的phase大体上分两大类一类是build/connect/end_of_elaboration等函数式phase一类是run_phase和12个run-time phase它们是任务型phase内部可以消耗仿真时间。函数式phase的执行顺序是自顶向下也就是根节点先跑然后一层层往下这在build_phase尤其明显。但connect_phase正好反过来它是自底向上执行的先连叶子节点再连父节点。为什么这样设计因为父节点通常要连接子节点暴露出来的端口如果父节点先开始执行connect而子节点还没把自己的port建好那连接就无从谈起了。run_phase则没有严格的从上往下或从下往上它更像是所有组件在同一时刻被fork出来一起并行运行。树形结构这时候的作用变成了“按名字空间隔离”和“按调度器组织”而不是真正严格的任务先后关系。你可以在driver的run_phase里和sequencer通信在monitor的run_phase里采样在scoreboard的run_phase里比对大家都是同时跑的。理解这个之后就能回答面试里一个高频问题为什么UVM要设计成树形结构而不是组件之间互相直接持有句柄就行答案里一定要提到phase的自动调度只有组件上了树UVM才知道该按什么顺序调用它的build/connect/run才知道它什么时候应该结束。如果是普通的objectUVM根本不会去自动调用它的任何phase方法。3.2 phase如何在树上决定测试的结束树的另一个重要作用是决定仿真结束的时机。UVM会监控树上的所有component只有所有component都声明自己可以结束了仿真才会真正停止。这里最常见的一种写法是在test里设置一个超时保护然后在scoreboard里根据比对结果决定是否提前结束。例如在test的run_phase里写task run_phase(uvm_phase phase); phase.raise_objection(phase.get_objection(), this); #1000; phase.drop_objection(phase.get_objection(), this); endtask如果没有raise_objectionrun_phase可能瞬间就结束了DUT还没跑起来。objection机制和树的关系是树上的任意一个component都可以raise/drop objectionUVM会汇总整棵树的objection状态来决定仿真是否结束。所以某个driver报了uvm_fatal并drop了objection即使其他组件还挂着objection整体也可能正常结束到report阶段然后打印出最终的pass/fail统计。从这里也能看出树形结构不只是存储数据的容器它是整个UVM生命周期调度的核心骨架。树挂得对不对直接影响phase执行的先后逻辑进而影响你能不能拿到正确的仿真波形和比对结果。4. 寄存器模型视角镜像值、adapter与树形挂载4.1 寄存器模型本身也是一棵树提到寄存器模型很多人第一反应是“寄存器模型和driver并列放在env下面不就行了”。实际上寄存器模型内部也是一棵树它有uvm_reg_block下面挂uvm_reguvm_reg下面还有uvm_field。你在顶层创建一个reg_block把它作为一个component挂在env树下然后block内部再通过uvm_reg::create和default_map.add_reg来生出一棵寄存器树。这棵树和平台组件树是交织在一起的。寄存器模型想要访问DUT必须通过adapter而adapter必须挂在某个sequencer上寄存器模型想要更新镜像值必须通过predictor去接收总线monitor采到的transaction。所以reg_block、adapter、predictor这几个对象都会成为平台树的一部分。典型的挂载方式是这样的// env的build_phase里 reg_model my_reg_block::type_id::create(reg_model, this); reg_model.configure(null); // 不依赖hdl路径时传null reg_model.build(); reg_model.lock_model(); // env的connect_phase里 reg_model.default_map.set_sequencer(agent.sqr, agent.adapter); reg_model.default_map.set_auto_predict(0); // 由predictor预测 reg_model.predictor new(predictor, this); reg_model.predictor.map reg_model.default_map; reg_model.predictor.adapter agent.adapter; agent.monitor.ap.connect(reg_model.predictor.bus_in);这段代码看着不难但它依赖的前提是reg_model本身必须能通过树的机制找到agent的sequencer。如果env把agent创建在了别的树下或者名字路径不对set_sequencer拿到的handle就可能为null。4.2 镜像值mirror value与期望值寄存器模型这个章节里热词中反复出现的“镜像值”是面试和实战的焦点。寄存器模型并不真的去读取DUT里每个寄存器的硬件值它在软件维护了一份“你认为硬件当前是什么值”的缓存这个就是mirror value。当你写了一个寄存器后UVM默认会同步更新这个镜像值但前提是你走的访问路径正确。举个例子reg_model.ctrl_reg.set(32h1); reg_model.ctrl_reg.update(status); // 把期望值写进硬件并更新镜像值set操作改变的是desired value期望值update操作才会比较desired和mirrored的差异并把差异值通过adapter发到总线上成功后把mirrored更新成desired。镜像值和树的关系体现在哪里体现在predictor的挂载链路上。predictor通过monitor的analysis_port接收总线上的读写数据再去更新对应寄存器的镜像值。这条链路如果断了那么reg_model里维护的镜像值就会和DUT真实值脱节后续你无论做reg.mirror()还是reg.get()拿到的都是过期数据。还有一个小坑是set_auto_predict和predictor同时使用时镜像值会被更新两次导致预测错乱。所以实际项目中我一般只选一个要么用auto_predict要么用独立的predictor。除非你能确认两者更新的路径是互补的否则不要图省事同时开。4.3 树挂不挂得稳map、adapter与sequencer三者必须对齐寄存器模型使用中最常见的报错之一是“No sequencer is associated with the map”。这个报错的原因就是adapter没有和map绑定或者map没有和sequencer关联。本质上是你树上的资源关系没有接好。在调试这类问题的时候我建议先做三件事第一打印reg_model.default_map.get_sequencer()是否为null第二打印agent.sequencer.get_full_name()确认sequencer的路径和你在map里设置的路径一致第三在adapter的reg2bus和bus2reg里打debug信息确认是否有transaction真正流经adapter。如果这三个都正常一般就能访问通。如果还不行再看一下reg_model这个component的parent有时候reg_model放在test下而agent放在env下虽然也能访问但config_db的路径影响会让某些参数传不进去导致map里的寄存器地址配置不正确。5. 最终display显示非常醒目的pass和fail——从树顶到report的传递5.1 为什么需要醒目的Pass/Fail仿真一跑几十分钟最后你扫一眼log最想看到的就是一个一眼能判断是否通过的标记。很多团队会要求在每个test的结尾统一打印一行大写加亮的PASS或FAIL。这在UVM里一般不是靠硬编码$display就能优雅解决的因为UVM的report机制默认会把pass和fail信息按severity分开统计而且分散在各条report里。比较实用的做法是在自定义的test_base里重写report_phasefunction void report_phase(uvm_phase phase); uvm_report_server server; server uvm_report_server::get_server(); int err_count; int fail_count; super.report_phase(phase); err_count server.get_severity_count(UVM_ERROR); fail_count server.get_severity_count(UVM_FATAL); if (err_count 0 || fail_count 0) begin uvm_info(TEST_RESULT, , UVM_NONE) uvm_info(TEST_RESULT, TEST FAILED, UVM_NONE) uvm_info(TEST_RESULT, , UVM_NONE) // 还可以把fail信息输出到独立文件 end else begin uvm_info(TEST_RESULT, , UVM_NONE) uvm_info(TEST_RESULT, TEST PASSED, UVM_NONE) uvm_info(TEST_RESULT, , UVM_NONE) end endfunction这段代码的关键在于report_phase是UVM在end_of_test之后统一执行的一个阶段它遍历整棵树的report信息。uvm_report_server是全局单例它汇总了所有component上报的error数和fatal数。你在任何一个test里产生的UVM_ERROR最终都会被统计到这里。我自己还会做一点扩展在PASS或FAIL的打印信息里带上test_name和仿真seed这样在回归环境里哪条用例挂在哪个seed下一眼就能看出来省去打开log慢慢翻的麻烦。5.2 用uvm_report_catcher拦截异常severity如果你还想更细粒度地控制Pass/Fail逻辑比如某些error只在特定条件下一票否决某些warning可以忽略那么就可以使用uvm_report_catcher。它是一个可以在report信息发给服务器之前拦截并修改severity的组件也挂在树上通过factory机制注册。class my_catcher extends uvm_report_catcher; function new(string namemy_catcher); super.new(name); endfunction virtual function action_e catch(); if (get_severity() UVM_ERROR get_message() matches *known_issue*) set_severity(UVM_WARNING); return THROW; endfunction endclass在test的build_phase里用my_catcher::type_id::create(my_catcher, this)创建之后它就会自动对所有report生效。最终report_phase里统计到的UVM_ERROR数量就是过滤后的结果PASS/FAIL的判定也随之变化。这个技巧在处理那些“已知问题但暂时无法修复”的用例时非常好用既不会漏报新的致命错误又能让回归结果保持干净。6. 面试高频点Hierarchy树形结构怎么答不丢分6.1 先把几个必问题目梳理一遍UVM验证面试里树形结构相关的题目出现频率极高我这里整理几个我面试别人时经常问的以及我准备候选人时一定会储备的知识点。第一题uvm_component和uvm_object的区别是什么最核心的答法是component有parent、可以参与phase调度、可以通过config_db按路径配置object是轻量级对象没有独立生命周期。然后可以举transaction和driver的例子说明各自场景。第二题build_phase的执行顺序是自上而下还是自下而上答案是自上而下。connect_phase呢是自下而上。有些面试官会追问为什么connect要自下而上你要说出父节点要连接子节点所以子节点必须先完成连接准备。第三题如果在connect_phase里再创建一个component会怎样这个问题考的是对UVM固定执行流程的理解。connect_phase阶段已经不会再执行build_phase新建的组件可能没有经过buildphase调度也不会覆盖它大概率会导致后续行为异常。一个合格的验证工程师不会这么写。第四题config_db的get和set是如何沿树查找的这题要讲清楚set时指定的路径是相对于当前component的实例路径get时会在当前node及其父节点路径上递归向上搜索直到根节点。第五题寄存器模型的树和平台组件的树是什么关系更准确的问法是adapter和predictor在树上分别挂在哪个节点下面。答案是adapter通常放在agent下面predictor可以挂在env下而reg_block本身也是一个component节点。6.2 面试中的“加分回答”技巧在上述基础回答上如果能加上个人项目经验会明显加分。比如面试官问build_phase自上而下你就可以补一句“我在某个项目里统计过如果build_phase里创建子组件时父句柄传错会导致config_db搜索路径完全失效整棵树的配置可能全部回退到默认值。后来我们强制在build_phase开头打印get_full_name()这类问题定位时间从半天缩短到十分钟。”这种回答表明你不是背概念而是真的在项目里被坑过、也真的解决了问题。面试官最想看到的不是完美的背答案而是你对这套机制的理解有没有落到工程层面。7. 实操中常见的坑与调试技巧——树的“雷区”实录7.1 树相关的高频故障速查我把自己在项目里实际遇到过的、以及帮同事排查过的树结构问题整理成了一张速查表如果你调试时遇到类似现象可以直接对照。现象根因排查思路组件没有执行build_phaseparent传了null未挂上树打印get_full_name()确认树上有无该节点config_db取不到配置路径不匹配或parent错误检查set和get的路径注意this和“”的区别connect_phase里句柄为null子组件不是在build_phase创建检查create位置确认是build_phase内创建仿真结束但run_phase没跑完objection没有正确raise/drop检查test和sequencer的objection配对寄存器访问超时/无响应adapter未绑定到sequencer打印default_map.get_sequencer()镜像值一直不更新predict链路断掉检查monitor ap是否连到predictorreport_phase统计不到errorreport被uvm_report_catcher吃掉查看catcher是否误改severity7.2 我习惯用的一段“树检查代码”为了快速确认树结构是否正常我在每个项目的test_base里都会放一个辅助函数在end_of_elaboration_phase里调用function void check_tree(uvm_component node, int depth); uvm_component children[$]; foreach (node.m_children[key]) begin children.push_back(node.m_children[key]); end foreach (children[i]) begin uvm_info(TREE, $sformatf(depth%0d path%s, depth1, children[i].get_full_name()), UVM_MEDIUM) check_tree(children[i], depth1); end endfunction function void end_of_elaboration_phase(uvm_phase phase); super.end_of_elaboration_phase(phase); uvm_info(TREE, begin dump tree , UVM_NONE) check_tree(this, 0); uvm_info(TREE, end dump tree , UVM_NONE) endfunction不过m_children是uvm_component内部的protected变量直接访问UVM版本兼容性不好所以我更建议用系统自带的uvm_top.print_topology()或者遍历get_children()方法。上面这段主要是说明思路把树上所有节点按深度打印出来配合full name基本能一眼定位到结构问题。我还有一个习惯是给关键组件加统一的uvm_info开关例如UVM_LOW时只打印创建信息UVM_HIGH时把连接信息、子组件列表也打出来。这样在回归log里通过“TREE”关键字就能快速看出一棵树的生成全貌。end of the day树形结构是UVM的骨架但不是终点。真正让这棵树发挥价值的是你对component、object、phase、config_db、寄存器模型之间交互的理解。我见过不少兄弟能把UVM手册背得滚瓜烂熟但真到了搭建平台的时候还是会在树的结构上栽跟头。这篇先写到这里树形结构、组件创建、寄存器模型挂载、pass/fail输出都覆盖到了。后续“待更”部分我准备继续写phase调度细节、sequence与driver的握手流程以及寄存器模型镜像值的深挖。如果你在搭建自己的验证平台时遇到过树形结构相关的怪问题欢迎在评论区留下现象我们下次一起拆。
返回列表