ARTICLE DETAIL

资讯详情

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

UVM验证平台搭建实战:从架构设计到仿真调试的完整指南

UVM验证平台搭建实战:从架构设计到仿真调试的完整指南 1. 为什么我劝你先想清楚再动手写第一行UVM代码搞数字IC验证的人迟早都会撞上UVM这堵墙。你可能是刚入行的验证新人被mentor丢过来一个模块说“搭个UVM环境跑一下”也可能是做了几年Verilog Testbench的老手发现定向测试越来越难覆盖复杂场景想转到UVM这套方法论上来。不管你是哪种情况UVM验证平台搭建和仿真这件事核心要解决的问题就一个用一套可复用、可扩展、自动化的验证架构替代写死的、一次性的Testbench让验证效率跟上设计复杂度的增长。我见过太多人一上来就打开编辑器开始写driver、写monitor写到一半发现sequence发不出激励或者scoreboard对不上数据然后回头大改。这种返工的成本非常高。UVM平台搭建不是写代码比赛它更像盖房子——你得先有图纸再打地基最后才是砌墙装修。这篇文章我会按照实际工程中搭建一个完整UVM验证平台的流程来展开从架构设计思路、核心组件实现、仿真调试到常见问题排查把每个环节的关键细节和踩坑经验都讲透。适合有基本Verilog和SystemVerilog基础、准备上手或正在搭建UVM平台的读者也适合已经搭过但总觉得哪里不对劲、想系统梳理一遍的同行。2. UVM验证平台的整体架构设计与思路拆解2.1 为什么UVM要搞这么复杂的组件结构刚接触UVM的人最大的困惑就是为什么一个验证平台要拆成driver、monitor、sequencer、agent、env、test这么多层我直接写一个initial块把激励打进去不就行了吗这个问题的答案藏在复用两个字里。假设你验证一个APB接口的模块用传统Testbench写了200行定向激励。现在项目来了第二个模块也有APB接口但数据通路完全不同。你的200行激励代码能直接拿来用吗大概率不能因为激励内容和协议时序混在一起了。UVM的解决思路是分层解耦把“产生什么样的数据”sequence和“怎么把数据送到DUT接口上”driver分开把“检查什么”scoreboard和“怎么采集数据”monitor分开。这样APB的driver和monitor可以原封不动复用到下一个项目你只需要写新的sequence和scoreboard就行。我个人的经验是一个标准的UVM平台架构应该包含以下核心层次层次组件职责复用粒度信号层interface连接DUT物理引脚项目级代理层agent (drivermonitorsequencer)处理单一协议协议级环境层env集成所有agent和checker模块级测试层test选择sequence和配置环境用例级这个结构看起来层次多但每一层都有明确的职责边界。当你搭过两三个平台之后就会发现真正需要每次重写的只有test层和scoreboard其他部分基本可以“复制粘贴改参数”。2.2 验证平台的目录结构怎么规划在动手写代码之前我强烈建议先把目录结构定下来。这不是形式主义而是因为UVM的编译依赖顺序很讲究目录乱了之后include路径会变成一场灾难。我常用的目录结构是这样的uvm_project/ ├── rtl/ # DUT源代码 ├── tb/ │ ├── interface/ # 接口定义 │ │ └── apb_if.sv │ ├── agent/ │ │ ├── apb_agent.sv │ │ ├── apb_driver.sv │ │ ├── apb_monitor.sv │ │ ├── apb_sequencer.sv │ │ └── apb_seq_item.sv │ ├── env/ │ │ └── apb_env.sv │ ├── test/ │ │ └── apb_base_test.sv │ ├── seq/ │ │ └── apb_basic_seq.sv │ └── tb_top.sv # 顶层 ├── sim/ │ └── Makefile # 仿真脚本 └── filelist.f # 文件列表这么分的好处是agent目录下的所有文件构成一个自包含的协议代理换项目时整个目录拷走就行env和test分开方便用不同的test去配置同一个envsim目录独立管理仿真脚本不跟代码混在一起。注意filelist.f里的文件顺序很重要。package的include顺序、interface的编译顺序都会影响仿真能否正常启动。我一般按照“interface → seq_item → driver/monitor → sequencer → agent → env → test → tb_top”的顺序排列。2.3 工厂机制和config_db到底解决了什么问题UVM有两个机制是初学者最容易忽略、但老手最离不开的factory工厂和config_db配置数据库。工厂机制的核心价值是在不修改原有代码的前提下替换组件类型。举个例子你的base_test里例化了一个apb_agent默认使用apb_driver。现在有个特殊用例需要用到apb_error_driver来注入错误激励。如果没有工厂机制你得改agent代码或者继承整个env。有了工厂机制你只需要在test里用set_type_override_by_type把apb_driver替换成apb_error_driver就行其他代码一行不动。config_db解决的则是跨层次传递配置的问题。UVM的组件树是树形结构test在顶层driver在底层。如果test想给driver传一个参数比如时钟频率、超时时间没有config_db的话就得一层一层往下传非常繁琐。config_db用全局的配置数据库让任何层次的组件都能按路径名取值。// 在test中设置 uvm_config_db#(virtual apb_if)::set(this, env.agent.*, vif, apb_vif); // 在driver中获取 if (!uvm_config_db#(virtual apb_if)::get(this, , vif, vif)) uvm_fatal(DRV, Failed to get virtual interface)这两行代码几乎出现在每一个UVM平台里。我踩过的坑是set的时候路径写错了get的时候一直失败但仿真不会报错只是driver里的vif是null跑起来激励全是x。所以每次get失败一定要用uvm_fatal而不是uvm_warning让问题在仿真开始时就暴露出来。3. 核心组件的实现细节与实操要点3.1 sequence_item事务的定义决定了平台的灵活性sequence_item也叫transaction是整个平台的“货币”driver把它转换成引脚信号monitor把引脚信号还原成它scoreboard拿它做比对。所以sequence_item的字段设计非常关键。以APB协议为例一个基本的sequence_item应该包含class apb_seq_item extends uvm_sequence_item; rand bit write; // 读写标志 rand bit [31:0] addr; // 地址 rand bit [31:0] wdata; // 写数据 bit [31:0] rdata; // 读数据由monitor填充 rand int delay; // 传输间隔 uvm_object_utils_begin(apb_seq_item) uvm_field_int(write, UVM_ALL_ON) uvm_field_int(addr, UVM_ALL_ON) uvm_field_int(wdata, UVM_ALL_ON) uvm_field_int(rdata, UVM_ALL_ON) uvm_object_utils_end function new(string name apb_seq_item); super.new(name); endfunction constraint c_addr { addr[1:0] 2b00; } // 地址对齐 constraint c_delay { delay inside {[0:5]}; } endclass这里有几个实操要点值得展开说。第一rdata字段不加rand因为它是由DUT返回的不是激励产生的。如果你不小心给它加了randscoreboard比对时会出现莫名其妙的错误。第二uvm_field_int宏注册的字段会影响print()、compare()、copy()等方法的默认行为。如果某个字段不参与比对比如delay可以用UVM_NOPACK或直接不注册。第三约束块要合理设置地址对齐这种硬性要求必须加约束否则driver发出去的地址可能违反协议。实操心得sequence_item的字段不要一开始就设计得太复杂。我见过有人把几十个字段全塞进去结果调试时print出来的信息刷屏根本看不清。建议先定义核心字段后续按需扩展。3.2 driver的实现从事务到引脚信号的桥梁driver的职责很纯粹从sequencer拿到sequence_item按照协议时序把数据驱动到interface上。它的核心是一个无限循环的get_next_item→ 驱动 →item_done流程。class apb_driver extends uvm_driver #(apb_seq_item); uvm_component_utils(apb_driver) virtual apb_if vif; function new(string name, uvm_component parent); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual apb_if)::get(this, , vif, vif)) uvm_fatal(DRV, virtual interface not set) endfunction task run_phase(uvm_phase phase); apb_seq_item req; vif.psel 1b0; vif.penable 1b0; forever begin seq_item_port.get_next_item(req); drive_one(req); seq_item_port.item_done(); end endtask task drive_one(apb_seq_item req); repeat(req.delay) (posedge vif.pclk); (posedge vif.pclk); vif.psel 1b1; vif.paddr req.addr; vif.pwrite req.write; if (req.write) vif.pwdata req.wdata; (posedge vif.pclk); vif.penable 1b1; // 等待从机响应 wait(vif.pready 1b1); (posedge vif.pclk); if (!req.write) req.rdata vif.prdata; vif.psel 1b0; vif.penable 1b0; endtask endclass这段代码里有几个容易出问题的地方。第一get_next_item和item_done必须成对出现如果中间抛异常或者return了sequencer会卡死。第二驱动信号一定要用非阻塞赋值否则在时钟沿可能出现竞争。第三wait(vif.pready)这种等待要加超时保护万一DUT挂了仿真会永远卡在这里。我通常会在base_test里设置一个全局的phase timeout比如phase.phase_done.set_drain_time(this, 100ns)配合仿真器的超时选项。3.3 monitor的实现数据采集的正确姿势monitor和driver相反它被动地观察interface上的信号把协议时序还原成sequence_item然后通过analysis_port发送给scoreboard和coverage collector。class apb_monitor extends uvm_monitor; uvm_component_utils(apb_monitor) virtual apb_if vif; uvm_analysis_port #(apb_seq_item) ap; function void build_phase(uvm_phase phase); super.build_phase(phase); ap new(ap, this); if (!uvm_config_db#(virtual apb_if)::get(this, , vif, vif)) uvm_fatal(MON, virtual interface not set) endfunction task run_phase(uvm_phase phase); forever begin apb_seq_item item; // 等待传输开始 (posedge vif.pclk iff (vif.psel vif.penable)); item apb_seq_item::type_id::create(item); item.write vif.pwrite; item.addr vif.paddr; if (vif.pwrite) item.wdata vif.pwdata; else item.rdata vif.prdata; ap.write(item); end endtask endclassmonitor的采样时机非常关键。APB协议里传输完成的标志是psel penable pready同时为高。如果你在psel刚拉高时就采样数据还没稳定。我一般用iff条件等待三个信号同时有效这样采到的数据一定是准确的。注意monitor里绝对不要驱动任何信号。我见过有人在monitor里“顺便”把某些信号拉高结果和driver产生冲突波形上出现X态。monitor就老老实实做观察者这是铁律。3.4 agent的组装active和passive的选择agent是把driver、monitor、sequencer打包在一起的容器。它有一个关键配置参数is_active。当is_active UVM_ACTIVE时agent包含driver和sequencer可以发送激励当is_active UVM_PASSIVE时只包含monitor用于纯监测场景。这个配置在实际项目中非常有用。比如你的DUT有两个APB接口一个是你需要主动驱动的另一个是DUT内部自己产生的你只需要监测。这时候就可以例化两个agent一个active一个passive复用同一套monitor代码。class apb_agent extends uvm_agent; uvm_component_utils(apb_agent) apb_driver drv; apb_monitor mon; apb_sequencer sqr; function void build_phase(uvm_phase phase); super.build_phase(phase); mon apb_monitor::type_id::create(mon, this); if (get_is_active() UVM_ACTIVE) begin drv apb_driver::type_id::create(drv, this); sqr apb_sequencer::type_id::create(sqr, this); end endfunction function void connect_phase(uvm_phase phase); if (get_is_active() UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export); endfunction endclassconnect_phase里只连接active agent的driver和sequencerpassive agent不需要。这个细节如果搞错了passive agent会报“sequencer port未连接”的警告。4. 仿真环境的搭建与完整运行流程4.1 tb_top的编写连接硬件世界和验证世界tb_top是整个验证平台的物理顶层它负责例化DUT、例化interface、产生时钟复位、启动UVM测试。这是唯一一个非UVM类的模块。module tb_top; import uvm_pkg::*; import apb_pkg::*; include uvm_macros.svh reg pclk; reg presetn; // 时钟产生 initial begin pclk 0; forever #5 pclk ~pclk; // 100MHz end // 复位产生 initial begin presetn 0; repeat(10) (posedge pclk); presetn 1; end // 接口例化 apb_if apb_vif(pclk, presetn); // DUT例化 apb_slave dut ( .pclk (pclk), .presetn (presetn), .psel (apb_vif.psel), .penable (apb_vif.penable), .paddr (apb_vif.paddr), .pwrite (apb_vif.pwrite), .pwdata (apb_vif.pwdata), .prdata (apb_vif.prdata), .pready (apb_vif.pready) ); // 传递virtual interface并启动测试 initial begin uvm_config_db#(virtual apb_if)::set(null, uvm_test_top.env.agent.*, vif, apb_vif); run_test(apb_base_test); end endmodule这里有个细节uvm_config_db::set的路径参数。第一个参数是null表示从根开始第二个参数uvm_test_top.env.agent.*是目标路径。uvm_test_top是run_test自动创建的test实例名env和agent的名字要和你在代码里create时用的名字一致。这个路径写错了config_db就传不进去。4.2 用Makefile管理仿真流程仿真命令通常很长涉及编译选项、库路径、仿真参数等。用Makefile管理是最省事的方式UVM_HOME /path/to/uvm-1.2 SIM vcs FILELIST ../filelist.f TOP tb_top TEST apb_base_test SEED 1 compile: $(SIM) -full64 -sverilog -ntb_opts uvm-1.2 \ -f $(FILELIST) \ -timescale1ns/1ps \ -debug_accessall \ -l compile.log sim: ./simv UVM_TESTNAME$(TEST) ntb_random_seed$(SEED) -l sim.log wave: verdi -ssf novas.fsdb clean: rm -rf simv* csrc *.log *.fsdb novas.* ucli.key .PHONY: compile sim wave clean这套流程用起来很顺手make compile编译make sim跑仿真make wave打开波形make clean清理。改测试用例只需要make sim TESTapb_error_test就行。实操心得ntb_random_seed这个参数一定要用。UVM的随机激励每次跑结果可能不同出问题时如果不记录seed根本复现不了。我习惯在sim.log开头打印seed值方便回溯。4.3 从零跑通第一个用例的完整步骤假设你已经写好了所有代码现在要跑通第一个用例。我建议按照以下顺序逐步验证第一步确认编译通过。先不管功能对不对确保没有语法错误和include路径问题。常见的编译错误包括package import顺序不对、宏定义找不到、类继承关系写错。第二步确认UVM树结构正确。在base_test的end_of_elaboration_phase里加上uvm_top.print_topology()仿真开始后会打印出完整的组件树。检查每个组件是否都正确例化层次关系是否符合预期。第三步确认config_db传递成功。如果driver或monitor报“virtual interface not set”的fatal说明config_db的set/get路径不匹配。可以在tb_top里用uvm_config_db#(virtual apb_if)::dump()打印所有配置项。第四步确认sequence能正常发送。在sequence的body里加uvm_info打印在driver的get_next_item后也加打印。如果sequence发了但driver收不到检查sequencer和driver的connect是否正确。第五步确认monitor能采到数据。在monitor的analysis_port write处加打印看是否和driver发送的数据一致。第六步确认scoreboard比对通过。这是最后一步如果前面都对了scoreboard的比对通常不会有问题。如果有问题重点检查monitor采样的时机和scoreboard的比对逻辑。这个顺序看起来笨但能帮你快速定位问题出在哪个环节。我最怕的就是一上来全跑报了一堆错不知道从哪查起。5. 常见问题与排查技巧实录5.1 仿真起不来先查这几个地方UVM仿真启动阶段的问题占了所有问题的60%以上。我把最常见的几类整理成速查表现象可能原因排查方法编译报错找不到uvm_pkgUVM库路径未设置检查-ntb_opts uvm-1.2或incdir$UVM_HOME/srcrun_test后无任何输出tb_top未调用run_test或test名拼错检查run_test参数和UVM_TESTNAMEuvm_test_top为nullrun_test在模块外调用或拼写错误确认run_test在initial块内config_db get失败set/get路径不匹配用uvm_config_db::dump()打印所有配置sequencer卡死get_next_item后未调用item_done检查driver的run_phase波形全是X复位未正确释放或interface未连接检查presetn时序和DUT端口连接5.2 sequence发不出去激励的几种典型情况sequence发了但driver收不到这个问题我遇到过不下十次原因每次都不一样。最常见的是sequencer和driver没有connect。在agent的connect_phase里drv.seq_item_port.connect(sqr.seq_item_export)这行如果漏了或者写反了sequence调用start()后就会一直挂着。第二种情况是sequence的start参数写错。seq.start(sqr)里的sqr必须是正确的sequencer句柄。如果传了null或者错误的sequencersequence会挂起但不报错。第三种情况比较隐蔽virtual sequence和virtual sequencer的协调问题。在多agent环境里virtual sequence通过virtual sequencer来调度各个子sequence。如果virtual sequencer里的子sequencer句柄没有正确赋值子sequence就发不出去。// virtual sequencer中需要指向实际的sequencer class apb_vseqr extends uvm_sequencer; apb_sequencer apb_sqr; uvm_component_utils(apb_vseqr) endclass // 在env的connect_phase中赋值 function void apb_env::connect_phase(uvm_phase phase); vseqr.apb_sqr agent.sqr; endfunction5.3 scoreboard比对失败的调试思路scoreboard报mismatch是最让人头疼的因为可能的原因太多了。我的调试思路是从后往前查先确认monitor采到的数据是否正确。在monitor的write处打印item的所有字段和波形上看到的信号对比。如果monitor采的数据就不对那问题在采样时机或字段映射上。如果monitor数据正确再查scoreboard的参考模型。参考模型的输出和DUT的输出应该在同一时间点对齐。如果参考模型有延迟或者顺序不一致比对就会失败。我通常会在scoreboard里加一个transaction队列把monitor采到的数据先存起来等参考模型也算完之后再逐笔比对。还有一种情况是复位期间monitor误采数据。DUT复位时信号可能不稳定monitor如果没加复位屏蔽会采到无效数据。解决方法是在monitor里加一个复位检测复位期间不采样。避坑技巧scoreboard的比对信息一定要打印足够详细。不要只报“mismatch”要打印期望值、实际值、地址、读写类型、时间戳。这样一看log就能定位问题。5.4 覆盖率收集不上怎么办功能覆盖率是验证完备性的重要指标。但很多人搭好平台后发现覆盖率收集不上covergroup定义了但一直是0%。第一个要检查的是采样事件。covergroup需要一个采样触发条件通常是(posedge clk)或者某个事件。如果采样条件永远不满足覆盖率就不会更新。第二个是coverpoint的bins定义。如果bins的范围没有覆盖到实际的值覆盖率也不会增长。比如你定义了bins low {[0:10]}但实际地址都是32位的那永远命中不了。第三个是覆盖率实例化位置。covergroup通常放在monitor或专门的coverage collector里。如果放在一个没有被例化的组件里自然不会收集。class apb_coverage extends uvm_subscriber #(apb_seq_item); uvm_component_utils(apb_coverage) apb_seq_item tr; covergroup cg_apb; cp_write: coverpoint tr.write; cp_addr: coverpoint tr.addr { bins low {[32h0000_0000 : 32h0000_00FF]}; bins mid {[32h0000_0100 : 32h0000_FF00]}; bins high {[32h0000_FF01 : 32hFFFF_FFFF]}; } cp_wdata: coverpoint tr.wdata { bins zero {0}; bins ones {32hFFFF_FFFF}; bins others default; } endgroup function new(string name, uvm_component parent); super.new(name, parent); cg_apb new(); endfunction function void write(apb_seq_item t); tr t; cg_apb.sample(); endfunction endclass注意uvm_subscriber的write函数会在每次analysis_port写入时被调用在这里更新tr并调用sample()就能持续收集覆盖率。6. 平台调试与性能优化的实战经验6.1 用UVM的verbosity控制log输出量仿真跑起来之后log文件动辄几百MB大部分是没用的信息。UVM的verbosity机制可以帮你控制输出级别。UVM_LOW是默认级别UVM_MEDIUM、UVM_HIGH、UVM_FULL、UVM_DEBUG逐级递增。在代码里用uvm_info的第三个参数指定级别uvm_info(DRV, $sformatf(Driving addr0x%08h wdata0x%08h, req.addr, req.wdata), UVM_HIGH)仿真时用UVM_VERBOSITYUVM_LOW只打印关键信息调试时改成UVM_VERBOSITYUVM_HIGH看详细过程。这个技巧在调试复杂场景时特别有用平时跑回归用LOW级别log小、跑得快。6.2 仿真速度慢的优化手段UVM平台跑大规模回归时仿真速度是瓶颈。除了换更快的服务器代码层面也有优化空间。减少不必要的打印是最直接的手段。uvm_info的字符串拼接即使在verbosity不够时也会执行如果拼接很复杂比如$sformatf里套了很多字段会白白消耗时间。可以用uvm_info的宏变体uvm_info_begin/uvm_info_end来延迟字符串构造。合理使用clocking block也能提速。interface里的clocking block可以让驱动和采样在时钟沿前后有明确的时序关系仿真器优化起来更高效。避免在run_phase里用while(1)死循环。UVM的phase机制本身有很好的调度用forever配合(posedge clk)比while(1)更高效。6.3 回归测试的自动化管理单个用例跑通只是开始真正的验证工作需要跑几百上千个用例的回归。我通常用脚本管理回归#!/bin/bash SEEDS(1 2 3 4 5) TESTS(apb_basic_test apb_error_test apb_stress_test) for test in ${TESTS[]}; do for seed in ${SEEDS[]}; do make sim TEST$test SEED$seed log/${test}_${seed}.log 21 if grep -q UVM_ERROR log/${test}_${seed}.log; then echo FAIL: $test seed$seed else echo PASS: $test seed$seed fi done done每个用例跑多个seed是为了覆盖随机激励的不同分支。UVM的ntb_random_seed参数控制随机种子不同seed会产生不同的激励序列。回归脚本自动收集结果失败的用例重点分析。实操心得回归测试一定要在夜间或者空闲时段跑白天用来调试失败的用例。我习惯把回归结果汇总成一个表格按用例名和seed列出通过/失败状态一目了然。7. 寄存器模型与高级特性的落地建议7.1 寄存器模型镜像值的同步机制UVM寄存器模型是验证平台里最实用的高级特性之一。它给DUT里的每个寄存器建了一个“影子副本”让你可以用reg_model.ctrl_reg.write()这样的方式读写寄存器而不需要手动构造sequence。寄存器模型的核心概念是镜像值mirror value和期望值desired value。镜像值表示寄存器模型认为DUT里当前是什么值期望值是你在write之后希望DUT变成什么值。正常情况下两者应该一致如果不一致说明DUT的行为和预期不符。// 前门访问通过总线读写会消耗仿真时间 reg_model.ctrl_reg.write(status, 32h0000_0001, UVM_FRONTDOOR); // 后门访问直接修改DUT内部寄存器不消耗仿真时间 reg_model.ctrl_reg.write(status, 32h0000_0001, UVM_BACKDOOR); // 镜像值更新 reg_model.ctrl_reg.mirror(status, UVM_CHECK);前门访问走正常的总线协议能验证寄存器的读写通路后门访问直接操作DUT内部信号适合快速初始化。实际项目中两者结合使用初始化用后门功能验证用前门。镜像值不同步是常见问题。比如DUT内部硬件自动修改了某个寄存器的值但寄存器模型不知道镜像值就过时了。这时候需要调用mirror()或update()来同步。我建议在scoreboard里定期检查关键寄存器的镜像值发现不一致及时报警。7.2 virtual sequence在多接口场景下的应用当DUT有多个接口时比如一个APB配置接口加一个AXI数据接口你需要协调不同接口上的激励。virtual sequence就是干这个的。virtual sequence本身不产生激励它通过virtual sequencer来调度各个agent的sequence。比如一个场景需要先通过APB配置寄存器再通过AXI发送数据最后通过APB读状态寄存器。这个流程用virtual sequence写最清晰class config_then_data_vseq extends uvm_sequence; uvm_object_utils(config_then_data_vseq) uvm_declare_p_sequencer(apb_vseqr) task body(); apb_config_seq cfg_seq; axi_data_seq data_seq; apb_status_seq sts_seq; cfg_seq apb_config_seq::type_id::create(cfg_seq); data_seq axi_data_seq::type_id::create(data_seq); sts_seq apb_status_seq::type_id::create(sts_seq); cfg_seq.start(p_sequencer.apb_sqr); data_seq.start(p_sequencer.axi_sqr); sts_seq.start(p_sequencer.apb_sqr); endtask endclassuvm_declare_p_sequencer宏会自动把virtual sequencer的句柄赋值给p_sequencer这样你就能通过它访问各个子sequencer。这个宏用起来方便但要注意virtual sequencer的类型必须和宏参数一致。7.3 用factory override实现用例的灵活扩展工厂override是UVM最强大的特性之一但很多人搭完平台都没用过。它的典型应用场景是base_test定义了一套标准流程某个特殊用例需要替换其中一个组件的行为。比如你的base_test里例化了一个normal_scoreboard现在有个用例需要检查错误注入后的行为需要换成error_scoreboardclass error_test extends apb_base_test; uvm_component_utils(error_test) function void build_phase(uvm_phase phase); super.build_phase(phase); // 把normal_scoreboard替换成error_scoreboard normal_scoreboard::type_id::set_type_override(error_scoreboard::get_type()); endfunction endclass这行override代码放在build_phase里在env例化之前执行。这样env里create scoreboard时工厂会自动创建error_scoreboard的实例。整个env代码一行不用改。注意factory override必须在组件create之前调用。如果放在build_phase的super之后env已经例化完了override就不生效了。我一般放在build_phase的最开头。8. 从能跑到好用平台维护的几点个人体会搭UVM平台这件事从零到能跑通第一个用例快的话两三天慢的话一两周。但从“能跑”到“好用”中间还有很长的路。我自己踩过的坑里最浪费时间的是前期架构没设计好后期改起来牵一发动全身。所以如果让我重新来一遍我会在动手写代码之前多花半天时间想清楚这个平台要验证什么、未来可能扩展什么、哪些组件需要复用。另一个体会是log和波形要配合看。光看log很难定位时序问题光看波形又不知道UVM组件在干什么。我的习惯是log里每个关键操作都带上时间戳波形里标出对应的时刻两边对照着查效率最高。最后说一个容易被忽略的点平台代码也要做版本管理。UVM平台的代码量不比RTL小而且经常需要回退到某个能跑的版本对比。用Git管理每次改动写清楚commit message出问题的时候能快速定位是哪次改动引入的。这个习惯养成之后调试效率至少提升一倍。
返回列表