ARTICLE DETAIL

资讯详情

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

UVM寄存器模型怎么搭?详解uvm_reg_block的设计与工程实践

UVM寄存器模型怎么搭?详解uvm_reg_block的设计与工程实践 搞了几年的UVM验证有一个被反复问到的点就是“寄存器模型到底怎么搭”。尤其是刚把uvm_reg和uvm_reg_field写完一编译就报错或者是构建好了模型前门访问、后门访问、镜像值同步却怎么都不对最后发现是uvm_reg_block这层没搞明白。uvm_reg_block.svh里面定义的uvm_reg_block说白了就是整个寄存器模型的骨架。没有它寄存器、字段、地址映射、hdl_path、前门后门访问这些全都串不起来。这篇文章我从头聊一下uvm_reg_block的设计思路、关键机制、真实环境里的接线方式再给一套可以直接抄的搭建步骤。适合正在学UVM的验证新人也适合准备验证面试、想把寄存器模型这块讲清楚的工程师。1. 先搞清楚uvm_reg_block在整个寄存器模型里的位置1.1 一个block对应一个可独立访问的IPUVM寄存器模型不是把一堆寄存器塞进一个容器里就完事它的设计思路其实是照着真实芯片的结构来的。一个芯片内部通常有多个IP每个IP有自己的寄存器组、存储器和专用的总线接口。对验证环境来说保证“一个IP对应一套寄存器模型”后面做寄存器读写测试、配置sequence、回归比对时思路才会清晰。而承载这套对应关系的就是uvm_reg_block。你可以在uvm_reg_block内部创建多个uvm_reg对象每个uvm_reg描述一个寄存器一个寄存器里再划分出多个uvm_reg_field每个字段描述寄存器的某几个bit位。这种层级关系如果用一张表来表达大概是下面这样类名对应DUT中的对象主要职责uvm_reg_block一个IP、子系统或整个芯片组织寄存器/内存、维护地址映射、提供前门/后门访问入口uvm_reg一个寄存器定义偏移地址、字段组合、读写行为uvm_reg_field寄存器中的一个bit字段定义复位值、属性RW/RO/W1C等uvm_reg_map一个总线地址视图把寄存器的偏移地址映射到实际总线地址所以在动手画验证环境结构的时候先不要把reg_block当成一个可选项它是树状结构的根。没有这个根uvm_reg和uvm_reg_field之间确实也能有自己的一套逻辑但要接入到整个UVM phase机制、sequence机制、镜像值同步机制里就卡住了。1.2 和uvm_reg、uvm_reg_field的分工很多初学者搞混一个东西uvm_reg_block虽然叫“block”但它不是一个寄存器也不是一个字段集合这么简单。它承担的职责是“组织”和“路由”。举例来说在DUT里地址0x1000处有一个控制寄存器0x1004处是状态寄存器。从验证环境角度这两个寄存器是独立的uvm_reg对象但你总得告诉UVM“这两个寄存器分别在哪一块地址区域里、地址偏移是多少、总线的数据位宽是基于32位还是64位”。这些信息就是挂在uvm_reg_block的uvm_reg_map上的。uvm_reg主要负责描述“这个寄存器有几个bit、每个bit是什么属性、复位值是多少、偏移地址是0x00还是0x08”。uvm_reg_field则只关心单个位域。真正把“哪个寄存器在哪一段地址、能不能通过哪条总线访问、读回来之后往哪个镜像值里更新”是uvm_reg_block和它内部的uvm_reg_map在干活。这样分工的结果就是头部代码可以复用同一个uvm_reg定义可以在不同block里使用只需要给它配置不同的偏移和映射关系。这在实际项目里非常重要因为同一个IP被复用到多个芯片里地址布局可能会变但寄存器的bit定义不变。如果寄存器定义和地址映射绑死在同一个类里复用成本就会高很多。2. uvm_reg_block的关键机制地址映射、镜像值与模型锁2.1 create_map与default_map地址映射是怎么算出来的uvm_reg_block里最常用的一个方法就是create_map。我在做APB接口的模块时几乎每个block里都有这样一段代码function void build(); default_map create_map(default_map, h0, 4, UVM_LITTLE_ENDIAN); default_map.add_reg(reg_ctrl, h0, RW); default_map.add_reg(reg_status, h4, RO); endfunction这里create_map的第二个参数是基地址第三个参数是总线的字节数。32位APB总线填416位总线填264位总线填8。这个参数理论上和DUT的实际数据位宽绑定一旦填错前门访问计算出来的总线地址就会差出几个字节。default_map是block的默认地址映射。每次你调用reg_model.default_map.set_sequencer(...)就是在告诉UVM“默认情况下通过这条总线sequencer对模型发起的前门访问按这个map地址来。”当然block里也可以有多个map这种情况通常是同一个寄存器块接了两条总线比如APB和AHB都能访问。这种场景下uvm_reg_block里会创建两个map每个map有自己的基地址和总线属性寄存器在两个map里的偏移可以不一样。有一个细节值得注意create_map的最后一个参数byte_addressing控制是否按字节寻址默认是1。如果你的总线是字寻址比如32位总线按字对齐、寄存器偏移是0、1、2而不是0、4、8那就要把byte_addressing设为0否则映射表计算出来的地址会全部乘4。总线上所有访问都会偏而且偏得很隐蔽不是X态只是每次都访问到错误的位置。2.2 镜像值、期望值与已知值寄存器模型为什么需要block这层容器UVM寄存器模型里有一套非常实用的“状态跟踪”机制分别叫镜像值mirror value、期望值desired value和已知值known value。这三个值的更新和查询表面上看是发生在uvm_reg_field里的但真正让它们对一个IP下的所有寄存器同步生效还是要靠uvm_reg_block这层容器。先解释一下这三个值期望值软件或验证环境“希望”寄存器变成的值。写一个寄存器时先写期望值。镜像值验证环境认为DUT内部当前实际的值。这个值可以由预测器uvm_reg_predictor根据总线观测自动更新也可以靠后门访问时更新。已知值是否知道当前值是有效状态。如果环境没有复位过模型也没有做过任何read/mirror操作这个寄存器就是未知的。uvm_reg_block里虽然没有直接存放这些值但它在两个地方起了决定性作用。第一当你对block里的某个寄存器调用reg.write(status, value)时UVM会顺着这个register所属的map找到对应的sequencer发起总线事务。写完以后会把这个值更新到当前寄存器的期望值同时通过predictor或auto_predict更新镜像值。第二当你想一次性让整个IP的所有寄存器都恢复到某个状态时可以直接在block级别调用update()或mirror()这两个方法会递归遍历block下的所有寄存器、所有字段统一同步。如果没有这个容器每个寄存器都要单独写一堆重复的逻辑工程上根本没法维护。还有一点容易被忽略后门访问里peek和poke也会更新镜像值。这很好理解因为后门访问是直接操作DUT内部存储单元的相当于你亲眼看到了真实值所以UVM会顺手把镜像值修正为后门读到的数据。但前门访问默认情况下不会自动更新镜像值必须在环境中连接好predictor或者打开block的auto_predict选项。这个我后面在实操部分会具体演示。2.3 lock_model到底锁了什么uvm_reg_block有一个很容易被一笔带过但实际非常重要的方法lock_model()。新手阶段我有一阵子经常忘记调用它。一天的现象是初始化完寄存器模型之后sequence里通过reg_model.reg_ctrl.read(status, value)去读结果返回的数据全是0sequence也不报错就是数据不对。排查了很久最后发现是没调lock_model。lock_model干的事情是把block内的地址映射、寄存器偏移、字段分配这些信息全部计算一遍把中间状态固化下来。在调用它之前你还可以继续add_reg、修改偏移、增加字段但调用之后UVM模型会进入“锁定态”之后任何试图改变映射和寄存器结构的操作都会报错。你可以这样理解lock_model之前uvm_reg_block还在“搭建模式”各种寄存器可以临时调整位置lock_model之后模型变成了“运行模式”地址计算逻辑已经被固定下来了。在实际项目中我通常会在build_phase的最后调用lock_model()比如function void build_phase(uvm_phase phase); super.build_phase(phase); rgm rgm_mmio_block::type_id::create(rgm, this); rgm.configure(null, ); rgm.build(); rgm.lock_model(); endfunction这个过程也符合UVM phase本身的设计build阶段负责建模之后才进入连接和运行阶段。如果建模没完成就运行后面所有的地址换算结果都不稳定自然会出现各种令人抓狂的偶发问题。3. 验证环境里怎么把uvm_reg_block接进去3.1 前门访问block通过adapter和sequencer干活前门访问形象地说就是让寄存器模型的读写“走真实的总线协议”。DUT侧是APB也好AXI也好总线上必须要真的发生一次读或写事务。要实现这个效果光有uvm_reg_block不够还需要uvm_reg_adapter和总线sequencer配合。环境里通常这样连接// env 的 build_phase 里创建对象 rgm rgm_mmio_block::type_id::create(rgm, this); apb_sqr apb_agent.sqr; reg_adapter apb_apb2reg_adapter::type_id::create(reg_adapter); reg_predictor uvm_reg_predictor#(apb_transfer)::type_id::create(reg_predictor, this); // connect_phase 里接线 rgm.default_map.set_sequencer(apb_sqr, reg_adapter); reg_predictor.map rgm.default_map; reg_predictor.bus_in apb_agent.monitor.item_collected_port;这段代码是UVM寄存器模型接入环境的标准姿势。uvm_reg_adapter的职责是把UVM寄存器操作转换为总线事务做到“模型侧的reg.write(status, value)”和“总线上真正发起一笔APB写”之间的桥接。uvm_reg_predictor则监听总线monitor发出来的事务一旦总线上出现读写操作就顺带把该地址对应的寄存器镜像值更新掉。这里有个容易忽略的关键点uvm_reg_block里的default_map是连接sequencer和adapter的入口。如果你有多个map每个map都要单独set_sequencer不能只设一个。因为UVM会根据访问时依赖的map来查找对应的sequencer和adaptermap不配对前门访问就找不到正确的总线。前门访问最大的特点就是“真实”。尤其在做配置类sequence的时候通过前门把DUT从复位态配置到工作态整个过程和实际软件驱动芯片的流程一致所以验证结果的说服力更强。代价是每条访问都要真正走一遍总线协议仿真时间会变长而且如果总线上有排队延迟sequence的响应也要等总线空闲。3.2 后门访问hdl_path的设置与常见误区后门访问就是不通过总线协议直接通过uvm_hdl_read、uvm_hdl_write这类工具或DPI接口直接读DUT内部信号。它的优点是快但前提是UVM必须知道“这个寄存器的内部信号在仿真层次结构里的路径”也就是hdl_path。hdl_path的管理正好是uvm_reg_block在build里要做的事情。以一个MMIO模块为例如果DUT的例化路径是tb_top.dut.mmioblock里有一个控制寄存器reg_ctrl那么你要在block里给reg配置好相对路径reg_ctrl.configure(this, reg_ctrl);同时在build时给整个block配置一个总的hdl_pathrgm.configure(null, tb_top.dut.mmio);这样UVM拼接出来的后门路径就是tb_top.dut.mmio.reg_ctrl。如果寄存器里的字段还需要更细粒度的路径比如某个位域内部还有独立信号可以在reg_field.configure(...)里也指定hdl_path。大多数情况下让uvm_reg直接代表一个寄存器的RTL例化单元就足够了。后门访问常见的坑有三个。第一block配置了hdl_path但reg里没有配置相对路径UVM会不知道该去哪个信号里读第二reg里配了hdl_path但block没有配UVM拼出来的路径只有半边第三路径写错比如少了一层dut后门读回来的数据经常是X态而且不报错因为uvm_hdl_read对不存在的路径默认返回X。后门访问对调试非常有用比如你想在测试开始时快速配置整个寄存器区而不想一条条走总线那用后门poke几笔就能搞定。不过要记住后门访问是“蛮力”手段很多总线协议层面的时序、延迟、仲裁行为都没有经过所以绝大多数功能验证场景里还是要以前门访问为主后门访问用在准备阶段和特定check。3.3 子块嵌套和多地址空间稍微高级一点的玩法真实的IP往往不是一层结构。一个子系统里可能包含APB子模块、SRAM子模块、中断控制器子模块每个子模块都有自己的寄存器偏移地址。这种情况下uvm_reg_block支持嵌套也就是在一个block内部再创建子block。子block的构建方式和一个独立block没什么两样只是在configure时要把父block传进去并且把子block的map挂到父block的某个地址范围里。拿代码来说大概是这样的思路class rgm_subsystem_block extends uvm_reg_block; rgm_submodule_a_block sub_a; rgm_submodule_b_block sub_b; virtual function void build(); default_map create_map(default_map, 0, 4, UVM_LITTLE_ENDIAN); sub_a rgm_submodule_a_block::type_id::create(sub_a); sub_a.configure(this, tb_top.dut.sub_a); sub_a.build(); sub_a.default_map.add_submap(sub_a.default_map, 16h0000); // 父块地址0x0000起映射 sub_b rgm_submodule_b_block::type_id::create(sub_b); sub_b.configure(this, tb_top.dut.sub_b); sub_b.build(); sub_b.default_map.add_submap(sub_b.default_map, 16h1000); // 父块地址0x1000起映射 endfunction endclass这里有一个概念容易混淆uvm_reg_map::add_submap是把子map放到父map的地址空间里而不是把子block整个挂进去。也就是说父block的default_map是“总地址视图”子块的map是局部视图通过add_submap把局部视图挂到总视图的某个偏移上。这样在父block里通过get_reg_by_offset查询某个地址时才能顺着map一层层找到真正属于哪个子模块的哪个寄存器。如果你正在做整个芯片级别的寄存器模型我非常建议用这种嵌套结构。好处是每个子模块的寄存器定义可以独立维护base address变化时只需要改父block里的add_submap偏移量子模块的代码完全不用动。4. 实操从零搭建一个带uvm_reg_block的寄存器模型这一节我直接给出一套可以动手实验的结构以一个APB总线的MMIO模块为例。DUT侧定义很简单有一个reg_ctrl控制寄存器偏移0x00包含一个RW字段和一个RO字段一个reg_status状态寄存器偏移0x04还有一个内部SRAM起始地址0x1000深度256字节。4.1 定义寄存器与字段先写寄存器类。注意这两个定义放在uvm_reg_block类之外因为它们是独立的模型组件编译顺序上要先于block类。class rgm_reg_ctrl extends uvm_reg; rand uvm_reg_field rw_field; uvm_reg_field ro_field; function new(string name rgm_reg_ctrl); super.new(name, 32, UVM_NO_COVERAGE); endfunction virtual function void build(); rw_field uvm_reg_field::type_id::create(rw_field); ro_field uvm_reg_field::type_id::create(ro_field); // parent_reg, bit_start, width, access, volatile, reset, has_reset rw_field.configure(this, 0, 16, RW, 0, h0, 1, 1, 0); ro_field.configure(this, 16, 16, RO, 0, h0, 1, 1, 0); endfunction endclass class rgm_reg_status extends uvm_reg; rand uvm_reg_field irq_status; function new(string name rgm_reg_status); super.new(name, 32, UVM_NO_COVERAGE); endfunction virtual function void build(); irq_status uvm_reg_field::type_id::create(irq_status); irq_status.configure(this, 0, 32, RO, 0, h0, 1, 1, 0); endfunction endclassuvm_reg_field::configure的操作很多关键参数包括字段起始bit、位宽、访问属性、复位值、是否支持复位、是否独立设置复位值等。这个设计和DUT里寄存器的RTL定义要保持一致否则后面做寄存器一致性检查时模型和RTL会对不上。4.2 写一个规范的reg_block并完成地址映射接下来是重头戏uvm_reg_block类的定义。我在这个类的build里会一次性把所有寄存器对象创建并完成地址映射。class rgm_mmio_block extends uvm_reg_block; rand rgm_reg_ctrl reg_ctrl; rand rgm_reg_status reg_status; uvm_reg_map default_map; function new(string name rgm_mmio_block); super.new(name, UVM_NO_COVERAGE); endfunction virtual function void build(); // 1. 子组件创建 reg_ctrl rgm_reg_ctrl::type_id::create(reg_ctrl); reg_status rgm_reg_status::type_id::create(reg_status); // 2. 寄存器configureparent都传this reg_ctrl.configure(this); reg_status.configure(this); // 3. 字段构建 reg_ctrl.build(); reg_status.build(); // 4. 地址映射32位总线n_bytes4小端按字节寻址 default_map create_map(default_map, 0, 4, UVM_LITTLE_ENDIAN, 1); // 5. 把寄存器塞进map地址偏移分别是0x00和0x04 default_map.add_reg(reg_ctrl, h0, RW); default_map.add_reg(reg_status, h4, RO); // 6. 如果有内存也可以加到map里 // default_map.add_mem(mem_sram, h1000, RW); lock_model(); endfunction // 后门访问路径在前门访问测试前的初始化里配置 function void set_hdl_path(string hdl_root); configure(null, hdl_root); endfunction endclass这段代码最后调用了lock_model()这是我在实际项目中比较推荐的做法在block自己build的最后就锁定避免后续误操作。如果某些环境下确实需要在lock之后动态调整寄存器UVM也提供了Xlocked相关机制但那种“事后反悔”的用法一般只出现在特殊调试场景正常测试不建议用。4.3 在env里实例化、连接adapter和predictorblock写好了就到环境里去实例化。需要说明的一点是uvm_reg_block本身是uvm_object不是uvm_component所以不能像agent、driver那样在env里用type_id::create创建后自动挂在component树上。标准做法是把它作为一个普通类成员在env的build_phase里create在connect_phase里完成和总线sequencer、adapter、predictor的连接。class test_env extends uvm_env; rgm_mmio_block rgm; apb_agent apb; apb2reg_adapter reg_adapter; uvm_reg_predictor #(apb_transfer) reg_predictor; uvm_component_utils(test_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); rgm rgm_mmio_block::type_id::create(rgm); rgm.configure(null, tb_top.dut.mmio); apb apb_agent::type_id::create(apb, this); reg_adapter apb2reg_adapter::type_id::create(reg_adapter); reg_predictor uvm_reg_predictor#(apb_transfer)::type_id::create(reg_predictor, this); endfunction virtual function void connect_phase(uvm_phase phase); super.connect_phase(phase); rgm.default_map.set_sequencer(apb.sqr, reg_adapter); reg_predictor.map rgm.default_map; reg_predictor.bus_in apb.monitor.item_collected_port; endfunction endclass这里rgm.configure(null, tb_top.dut.mmio)里的null表示这是根block没有父block。第二个参数是这个block在后门访问时对应的HDL根路径。一旦配好后门访问就能直接拼出完整路径。前门访问不依赖这个路径但它仍然是后门访问能工作的必要配置。uvm_reg_predictor的bus_in挂的是总线monitor的item端口它会观察每一次总线事务把地址和数据发给对应的map由UVM根据reg2bus和bus2reg完成数据还原。这里还有一个选择如果不想接predictor可以用default_map.set_auto_predict(1)。但个人建议在真正做项目时尤其是APB/AHB这类有握手和等待状态的总线尽量接predictor因为auto_predict在某些延迟场景或者ARRAY类型的memory访问场景下会丢掉一些信息导致镜像值和真实值在复杂时序下对不上。auto_predict适合快速验证路径、临时跑通数据流的阶段。4.4 让最终仿真打印出醒目的PASS/FAIL很多UVM初学者在看别人写的验证环境时会注意到仿真最后会打印一行非常显眼的PASS或者FAIL很多面试题也会问这个怎么实现。其实方式不止一种最常见的是在test的report_phase里根据自定义的check结果做打印。简单点的做法可以在sequence或scoreboard结束时用自定义宏控制打印if (compare_ok) uvm_info(get_type_name(), $sformatf(), UVM_LOW) uvm_info(get_type_name(), $sformatf( PASS ), UVM_LOW) uvm_info(get_type_name(), $sformatf(), UVM_LOW) else uvm_error(get_type_name(), $sformatf()) uvm_error(get_type_name(), $sformatf( FAIL )) uvm_error(get_type_name(), $sformatf())因为uvm_error默认会在终端把对应信息标红所以用uvm_error打印FAIL视觉上能很清楚地看到。如果嫌UVM默认的report信息太啰嗦还可以自己继承uvm_report_server重写report_summarize或者直接修改uvm_report_handler的severity。不过这些属于“锦上添花”面试时能说出用uvm_error和uvm_info的语义区别就够了。需要格外注意的是PASS/FAIL不应该只靠最后一行打印输出真正有效的判断一定要有对应的实际数据比对和覆盖率统计打印只是一个给人类看的“摘要”。建议在打印PASS之前也把失败的寄存器地址、期望值和读取值一并打印出来否则看到FAIL却不知道是哪个寄存器failed调试效率会大打折扣。5. 查询与调试让reg_block替你省时间的几个方法5.1 get_reg_by_offset、get_mem_by_offset按地址找对象uvm_reg_block提供了几个查询接口在写通用测试代码时非常有用。比如你从测试用例里传入一个地址想在仿真时确定这个地址对应哪个寄存器就可以用uvm_reg_base reg_base; reg_base rgm.default_map.get_reg_by_offset(h4);get_reg_by_offset会根据给定的偏移在map中查找到寄存器返回一个uvm_reg_base对象。get_mem_by_offset则是按类似方式查询memory。这两个接口的价值在于可以让测试代码不用事先硬编码“哪个寄存器在哪”而是从地址反查这样既能提高代码复用性也方便做参数化测试。不过要注意这个函数返回的是uvm_reg_base如果后面还要调用read、write、mirror这些方法最好先cast成具体的寄存器类型或者用uvm_reg的通用接口去操作。UVM的寄存器操作基本都是通过uvm_reg提供的直接操作uvm_reg_base的语义不如具体reg清晰。5.2 打印整棵寄存器树的两种方法遇到模型行为和DUT对不上时我第一反应就是先把模型里的寄存器树打出来看每个reg的偏移是不是和RTL一致。最简单的方法是直接打印default_map的寄存器列表foreach (rgm.default_map.get_registers()[i]) begin uvm_reg r rgm.default_map.get_registers()[i]; uvm_info(REG_TREE, $sformatf(reg[%0d] name%s offset0x%0h, i, r.get_name(), r.get_offset()), UVM_LOW) end第二种方法是用UVM自带的打印功能对整个block调用rgm.print();print会以UVM的object打印格式把block下的所有寄存器、字段、map信息、偏移地址都列出来非常直观。不过打印内容比较长大型芯片下输出量会非常大建议只在排查问题时用别在回归时全局开。还有一种更贴近实际需求的场景在测试里想遍历所有寄存器统一检查它们的读写属性。以前我是手动把每一个reg列一遍后来改成遍历default_map.get_registers()代码一下缩短了很多。遇到新增寄存器时也不用手动改遍历代码自动就加进来了。5.3 reg_block相关的排错思路与速查表调试寄存器模型时被问得最多的几个问题我整理成了一张速查表基本覆盖了90%的幺蛾子。当你发现寄存器读回来全是X、写不进去、sequence卡死、mirror值不对时对照着查一遍效率会比自己抓瞎高很多。现象大概率原因排查/解决办法编译报错unknown type uvm_reg_field头文件包含顺序不对或没include先include uvm_reg、uvm_reg_field、uvm_reg_block相关.h头文件前门访问读回全Xadapter没接或总线monitor没接对检查default_map.set_sequencer和adapter的reg2bus/bus2reg实现后门访问读到0或Xhdl_path配置不完整确认block.configure第二个参数和每个reg的configure相对路径拼接后等于DUT真实路径镜像值不更新没接predictor也没开auto_predict接predictor或设置set_auto_predict(1)镜像值在复杂读写后错乱总线上有其他master也在写predictor漏了一下检查monitor是否抓到所有总线事务必要时改为手动更新lock以后add_reg报错对已lock的block再次修改结构必须在lock之前完成所有寄存器挂载reg读出来的位域和RTL不一致field的bit_start或width设错对照RTL参数逐一核对field配置地址偏移差4倍create_map的byte_addressing误设为1但总线是字寻址按总线寻址方式正确设置byte_addressingblock里嵌套子block后找不到reg子map没有add_submap到父map在父block里用default_map.add_submap把子map挂到对应偏移同一地址在不同map里偏移不同只设置了default_map的访问其他map没set_sequencer每个map都要单独配置sequencer和adapter这张表里的问题基本都是我在真实环境里踩过的不夸张地说寄存器模型的调试时间有一大半都花在这些细节上。多数情况下模型本身没多大问题只是环境“接线”没接对。5.4 自定义report让FAIL更醒目刚才提到了用uvm_error让FAIL变红这里再补一个更普适的方法也是面试时容易加分的点自定义uvm_report_server。UVM默认的uvm_report_server负责汇总整个仿真过程中的UVM_INFO、UVM_WARNING、UVM_ERROR、UVM_FATAL。你可以写一个派生类重写report_summarize在彩色终端或日志文件里输出一段醒目的PASS/FAIL横幅。class custom_report_server extends uvm_report_server; uvm_object_utils(custom_report_server) function new(string name custom_report_server); super.new(name); endfunction virtual function void report_summarize(); super.report_summarize(); if (get_severity_count(UVM_FATAL) 0 get_severity_count(UVM_ERROR) 0) $display( PASS ); else $display( FAIL ); endfunction endclass然后用全局配置把自定义server替换默认serverinitial begin uvm_report_server::set_server(custom_report_server::type_id::create(custom_report_server)); end这种做法的好处是无论测试是在回归脚本里批量跑还是本地调试单独跑都能在一堆log的最后直接看到PASS/FAIL状态红色或绿色都方便人工识别。而且这个办法不污染业务代码适合团队统一标准化。6. 关于uvm_reg_block的高频面试题和我的心得6.1 面试官爱问的4个问题uvm_reg_block几乎每场验证面试都会被问到而且问题往往层层深入。我自己被问过、也问过别人的几个典型问题列在这里供参考。第一uvm_reg_block和uvm_reg_map是什么关系回答要点是block是寄存器模型的组织者map是block内部针对某一条总线的地址映射视图。block可以持有多个map但一个寄存器在同一个map里只能挂一次。第二前门访问和后门访问的区别是什么前门要通过总线sequencer和adapter走真实的协议事务能验证真实的总线时序和握手流程后门直接通过HDL路径读写速度快但因为不走真实总线不适合用来验证总线协议相关的功能。uvm_reg_block在这里的作用是统一维护这两类访问的注册信息前门通过map绑定sequencer和adapter后门通过configure绑定hdl_path。第三镜像值什么时候会变predictor监听到总线事务时会更新后门访问后也会更新调用mirror()时可以主动查询更新set_auto_predict(1)时会在sequence发起访问后自动更新。镜像值不更新不要只盯寄存器模型先检查predictor的连接和总线monitor是否抓到了事务。第四一个DUT如果有两个总线入口可以访问同一个寄存器组模型怎么建这个问题比较进阶。通常做法是给同一个block创建两个map或者给子模块分别建block再嵌套到父block里。每个map根据自己总线的基地址和位宽配置寄存器可以挂到多个map上但要注意不同map的访问是否需要不同的adapter。回答时能提到多map和add_submap的区别面试官一般会觉得你确实做过实际项目。6.2 我自己踩过的三个坑第一过度依赖auto_predict导致镜像值错乱。那次是在一个多主场景的AXI系统里一个寄存器可以被CPU核写也可以被DMA控制器写。我图省事在default_map上开了auto_predict结果预测器只根据寄存器模型自己发起的访问做预测DMA侧发出的写事务完全没进模型镜像值跟着就乱了。从那以后凡是存在多个master的环境我都是老老实实接predictor并且确认monitor能抓到所有master的事务。第二忘记hdl_path的全局拼接规则。早期我写的block里每个reg的configure里都写了相对路径但block的configure里hdl_path只写了“tb_top”结果后面访问到第二级、第三级时拼出来的路径和实际DUT完全对不上。后来我规定block的hdl_path必须是根路径reg的hdl_path只写相对寄存器的例化名两层拼接成最终路径而且所有路径宏统一管理避免直接散落在代码里。第三嵌套block时把add_submap和add_reg搞混。父block的map里如果同时挂了一个寄存器区和子模块的map必须注意地址区间别重叠。有一回子模块基地址写成了0x1000而父block自己也挂了一个偏移0x1000的寄存器结果get_reg_by_offset查询命中时model优先匹配到了子map父block那个寄存器的前门访问被路由到了子模块总线上仿真半天拉不出数据。最后打印整棵树才发现问题。6.3 一个值得养成的习惯我现在每搭一套寄存器模型无论规模大小都会在最后专门写一个极简的冒烟测试sequence只做一件事把block里所有寄存器做一遍前门写读再做一遍后门读最后在模型里把镜像值、期望值、RTL实际值三方比对一次。一旦这套东西跑通再往上层叠加业务sequence就很有信心。这个习惯帮我节省了不少加寄存器之后连RTL移植带回归的调试时间也是在uvm_reg_block层面发现映射偏移写错、hdl_path漏配这类问题的最短路径。
返回列表