尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

UVM实战:从零搭建只有一个driver的最小验证平台

UVM实战:从零搭建只有一个driver的最小验证平台 雪花的验证环境跑了几百个case之后我回过头来还是想把UVM的基础重新过一遍。倒不是说工作里遇到多大瓶颈而是每次和新人聊起UVM的搭建过程总发现大家一上来就扎进完整的agent、scoreboard、reference model、sequence机制里环境搭得铺天盖地结果跑不通的时候连个最简单的driver都调不明白全线卡死。所以这一篇笔记我准备老老实实只写一件事在UVM体系下只有一个driver的验证平台到底怎么搭。标题里的“UVM实战 卷I学习笔记1”就是我给自己定的节奏第一卷先别贪多把这个最小系统彻底吃透后面的东西都好说。为什么非要强调“只有driver”因为学习UVM最大的敌人不是复杂是贪多。一个完整可用的验证环境组件之间互相牵制配置项几十个一旦报错你根本分不清是factory没注册好还是config_db没配进去还是phase机制跳错了。而当平台里只有一个driver的时候问题边界被压缩到了最小代码量少依赖少所有异常基本都集中在driver这一个Component上。你把这条最细的链路打通之后后面加monitor、加sequence、加scoreboard每一步都是增量理解而不是一次性知识爆炸。这篇内容适合刚接触UVM、想从零开始搭环境的新人也适合已经写过不少验证代码、但总觉得UVM底层机制不够透的工程师跟着我走一遍你至少能明白验证平台里最基础的组件到底是怎么落地的。1. 从零搭平台为什么偏偏先写一个只有driver的环境1.1 验证平台的最小组件是什么早些年写Verilog testbench我习惯把所有东西塞到一个module里初始化信号给它赋值打时钟然后$finish收工。这种方式用起来直接但致命弱点是不可复用、不可扩展。项目一换环境全废。UVM解决这个问题的思路是把验证平台里功能相对独立的部分拆成一个一个的组件每个组件只负责一类事组件之间用标准接口通信。而这个拆分的起点就是driver。所谓driver字面意思就是“驱动器”。它要干的事情只有一件按照接口协议把激励信号按时序送到DUT的输入端口上。时钟、复位、数据有效、数据通道——凡是DUT输入侧需要的行为都由driver负责生成。它是整个验证平台里最靠近DUT的组件之一也是激励从验证意图变成实际波形的最后一公里。在UVM里driver通常是uvm_driver的派生类。uvm_driver本身是uvm_component的扩展UVM已经把run_phase、report、factory机制这些底子都替你铺好了你只需要关心怎么把数据转换成信号就行。这也是我认为UVM入门最合适的切入点它不涉及复杂的TLM通信不涉及objection的精细把控只涉及最基本的“类派生 接口连接 时序驱动”这三板斧一过整个UVM的脉络就顺了。1.2 只有driver能验证出什么东西有人会问只有一个driver的平台没有monitor没有scoreboard连数据结果都没人自动检查这也能叫验证能但它价值不在“发现bug”而在验证环境本身跑得通。就好比你造了一条公路第一天只通了从A到B的一段路路上没有检查站也没有收费站车能开过去这件事本身就说明路基是平的、方向是对的。从实际验证角度说只有一个driver的案子能验证这些点DUT的输入接口时序是否与设计约定一致。DUT在复位释放后能否给出基本的状态变化靠波形观察或打印日志确认。UVM环境自身的编译、UVM树建立、phase执行顺序是否正确。你写的class是否正确注册到了factory机制里、virtual interface有没有正确传递进来。这些都是后续所有验证工作的地基。我见过太多人一上来就搞完整环境结果UVM根本报UVM_ERROR调查了一圈发现是driver里virtual interface是空指针连环境都没立住。所以说先跑通一个只有driver的平台相当于给整个验证环境做了一次“冒烟测试”。2. 环境搭建与基础框架准备2.1 仿真工具与UVM库的准备工作搭建UVM环境第一步不是写代码而是确认工具链齐不齐。常用的仿真器有Synopsys VCS、Mentor现在叫Siemens EDA的QuestaSim以及开源圈子常用的Verilator。对于UVM来说VCS和QuestaSim对UVM原生支持都比较成熟我个人经常用QuestaSim做教学和小型验证原因无他license好搞定编译速度快UVM源码里带的example可以直接参考。无论你用哪个工具基本原理都一样你得先有UVM库的源文件或者使用工具自带的预编译UVM库。以QuestaSim为例UVM库通常装在安装目录下的verilog_src/uvm-1.2或者uvm-1.1里面。编译的时候需要在命令行里指明UVM库路径并且让UVM库先于你的验证代码编译。VCS的做法类似用-ntb_opts uvm-1.2或者指定incdir$UVM_HOME来告知工具去哪找UVM的包。我的建议是不管用什么工具第一件事先把环境变量$UVM_HOME配好指向你本机的UVM源码目录。后面所有编译脚本里引用这个变量既统一又不容易出路径错乱。这个步骤看似基础但配置不当引发的编译错误却能卡住半天下面第5章我会专门讲这类问题。2.2 目录结构与文件规划只有一个driver的最小平台其实不需要复杂的目录管理但养成分类习惯没坏处。我通常建议这样一个最小结构rtl/ dut.v tb/ tb_top.sv // 顶层模块连接DUT、interface、启动UVM my_if.sv // 接口定义让driver和DUT真正接起来 uvm_env/ my_driver.sv // driver组件 my_test.sv // 测试用例入口创建driver并启动它 sim/ run.do // 仿真脚本QuestaSim的do文件或Makefile compile.log waveform.vcd别看文件少这个结构已经体现了UVM环境从“验证代码”和“测试用例”分离的思想。rtl放设计文件tb放仿真顶层和接口uvm_env放UVM组件sim放仿真产物。以后环境变大这个框架只需要往里加目录不需要推倒重来。有人会问为什么接口文件my_if.sv要放在tb而不是uvm_env因为interface是硬件层面的抽象它更贴近测试平台顶层而UVM组件是软件层面的事务抽象。两者解耦之后你换一个DUT可能只需要改interface和driver的时序段UVM代码的骨架不用动。3. driver的完整设计从类定义到驱动时序3.1 类定义与工厂注册为什么一定要做这一步一个UVM driver类最基础的定义大概是这样的class my_driver extends uvm_driver#(my_transaction); uvm_component_utils(my_driver) function new(string name my_driver, uvm_component parent null); super.new(name, parent); endfunction virtual my_if vif; virtual task run_phase(uvm_phase phase); // 驱动流程写在这里 endtask endclass很多新手对my_driver extends uvm_driver#(my_transaction)里的参数化一头雾水。uvm_driver是一个参数化类括号里的my_transaction表示这个driver将来处理什么类型的事务对象。如果你暂时没定义transaction类型也可以先用uvm_driver #(uvm_sequence_item)或者直接继承uvm_driver这个基类但那样在后续接sequence的时候会变麻烦。这里先定义一个极简单的transaction哪怕里面暂时没有太多字段也能为后面扩展铺平道路class my_transaction extends uvm_sequence_item; rand bit [7:0] data; uvm_object_utils(my_transaction) function new(string name my_transaction); super.new(name); endfunction endclass这里有个关键点driver是uvm_component的派生类必须用uvm_component_utils注册而transaction是uvm_object的派生物要用uvm_object_utils注册。两者注册宏不一样因为前者参与到UVM的组件树生命周期由UVM管理后者只是运行时的一个对象生命周期由使用者管理。用错宏在编译阶段不一定爆错但后面factory机制和打印信息会各种出问题。3.2 关键成员变量virtual interface为什么能提升可移植性再看上面代码里的virtual my_if vif;。这个成员变量是整个driver和硬件世界交互的窗口。interface是SystemVerilog里连接DUT和验证平台的桥梁它定义了时钟、复位、数据等信号以及这些信号的时序关系。在UVM组件里我们不会把整个DUT实例传进去而是用一个virtual interface指向真实的interface。这个virtual关键字很关键它把接口从具体的硬件实例抽象成了一个引用这样driver代码本身不依赖具体是哪个interface实例只要通过配置机制把正确的interface句柄传进来就行。相当于插座是标准化的你插哪个电器都行。配置传递的标准姿势是uvm_config_db#(virtual my_if)。在test或top层我们把顶层实例化的interface塞进UVM配置数据库uvm_config_db#(virtual my_if)::set(null, uvm_test_top.drv, vif, uvm_test_top.drv, this.tb_if);然后在driver的build_phase里取出来virtual function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual my_if)::get(this, , vif, vif)) begin uvm_fatal(NOVIF, virtual interface not found) end endfunction为什么不在top层直接driver.vif tb_if那样做的问题是driver实例可能在tb_top里还没创建出来或者你根本不想让顶层代码关心UVM内部组件的成员。通过config_db机制UVM把组件间的依赖关系放在配置阶段统一解决这让代码的耦合度大大降低。这也是UVM设计哲学里很重要的一点组件之间尽量通过配置和事务通信而不是互相直接引用成员变量。3.3 run_phase里写驱动时序从复位到正常激励driver的核心工作都在run_phase里完成。这一部分就是传统testbench里initial块干的事但UVM把它规范到了一个固定的阶段里执行并且允许你在执行期间通过raise_objection告诉UVM“我正在干活别急着结束”。最基础的驱动流程是这样virtual task run_phase(uvm_phase phase); phase.raise_objection(this); // 复位 vif.rst_n 0; vif.data 0; repeat (5) (posedge vif.clk); vif.rst_n 1; (posedge vif.clk); // 发几个数据 repeat (10) begin (posedge vif.clk); vif.data $random; end repeat (5) (posedge vif.clk); phase.drop_objection(this); endtask这里有个细节phase.raise_objection必须写否则UVM会认为UVM树里没有任何active任务直接结束仿真。我在刚入门时吃过好几次亏环境编译过了一跑仿真瞬间就结束了看波形一团空白排查半天才发现忘了raise。驱动信号时最好先复位一段时间把DUT内部状态清干净否则一上来就是非法的初始状态。每次驱动数据之前先等一个时钟上升沿保证信号更新和时钟对齐避免组合逻辑毛刺在同一个时刻传播。实际项目里driver会复杂得多有命令包格式、有反压处理、有超时等待但底层思路不变等待时钟沿拉高拉低信号再等待下一个时钟沿。3.4 驱动事务数据与信号时序的常见匹配方法当driver后面接到sequence之后你通常会在循环里不断取事务然后把事务里的字段转化到信号上。即便现在还没引入sequence我也会建议在driver里预留一个get_and_drive类型的任务把“接收激励”和“驱动信号”两层逻辑分开。将来接sequence的时候你只需要改动“接收激励”这一层驱动代码原封不动。伪代码思路如下task get_and_drive(); my_transaction tr; (posedge vif.clk); vif.data tr.data; endtask再往后如果出现了类似“UVM不回respond但也只能发八个包”的奇怪现象多半是sequence机制里的握手环节出了问题但那就是后话了。当前只有driver的阶段你只要确认你发出去的数据在DUT输入引脚上能真实看到DUT输出了预期波形就算完成任务。4. 验证平台集成与运行4.1 test类的构建UVM环境的最小入口有了driver还不够UVM需要通过一个uvm_test类来实例化并启动整个环境。即使只有一个driver我也建议把test单独写一个文件因为以后加agent、加envtest永远是仿真的入口类。最简单的一个test是这样class my_test extends uvm_test; uvm_component_utils(my_test) my_driver drv; function new(string name my_test, uvm_component parent null); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); drv my_driver::type_id::create(drv, this); endfunction task run_phase(uvm_phase phase); phase.raise_objection(this); #10000; phase.drop_objection(this); endtask endclass注意create方法它是UVM工厂机制的入口而不是直接调用构造函数new。虽然两者在简单场景下看起来差不多但工厂机制允许你在不改代码的情况下替换组件类型这在后端的UVM验证组件复用中价值极大。养成用type_id::create的习惯比用new安全得多。这里补充一句driver通常由env创建而不是test直接创建。在只有driver时可以先用test直接创建等引入agent后再调整成test创建env、env创建agent、agent创建driver这条标准链路。但为了贴近标准结构你也可以现在就直接建一个最简env然后再进agent。我这里为了突出“只有driver”先按最简方式走本章后面会说明如何过渡。4.2 顶层module的接线从interface到DUTtest是软件层DUT是硬件层两者通过顶层module来粘合。顶层代码一般长这样module tb_top; logic clk; logic rst_n; logic [7:0] din; logic [7:0] dout; my_if u_if (clk, rst_n, din, dout); dut u_dut ( .clk(clk), .rst_n(rst_n), .din(din), .dout(dout) ); initial begin clk 0; forever #5 clk ~clk; end initial begin run_test(my_test); end endmodule这段代码里的run_test(my_test)是UVM的启动命令。它会在后台构建一棵UVM组件树找到叫my_test的那个component执行它的build_phase和run_phase。实际项目中run_test的参数常常通过命令行传进去比如UVM_TESTNAMEmy_test这样同一个仿真环境可以跑不同test。不过对于这篇笔记的第一阶段直接在代码里指定也行。接口这里还有一个细节my_if里通常同时包了DUT的输入和输出信号。driver实际上只用到din、时钟、复位这些输入侧信号输出侧现在没人收集但接口里把两者都列出来是为了将来加monitor时不用再改接口。这一点属于“现在多写一行将来省一小时”的典型投资。4.3 运行仿真与波形查看的关键命令仿真运行时以QuestaSim为例常用的编译和运行方式类似这样vlog -work work incdir$UVM_HOME/src $UVM_HOME/src/uvm.sv vlog -work work rtl/dut.v tb/my_if.sv uvm_env/my_driver.sv uvm_env/my_test.sv tb/tb_top.sv vsim -c -do run -all work.tb_top如果你的UVM库是Questa自带的可以直接用vsim -uvmversion 1.2命令行会简洁很多。VCS的话编译命令通常是vcs -sverilog -ntb_opts uvm-1.2 -debug_accessall \ rtl/dut.v tb/my_if.sv uvm_env/my_driver.sv uvm_env/my_test.sv tb/tb_top.sv \ -o simv ./simv UVM_TESTNAMEmy_test仿真跑完之后最重要的是打开波形亲眼看一看driver输出的din在时钟沿上有没有正常变化。只有波形正确了才算真正跑通了环境。这个阶段UVM打印信息是辅助波形才是金标准。5. 运行中常见的坑与排查技巧实录5.1 编译顺序和UVM库路径引发的玄学报错UVM环境从零开始搭时报错最多的地方反而是编译环节而不是运行环节。典型的几类报错Cannot find package uvm_pkg本质是incdir没有指向UVM库源码目录或者UVM库本身没有被提前编译到工作库。一堆redefinition或already defined错误可能是因为同一个UVM库被编译了两遍或者编译脚本里同时拉了工具内置UVM和自己下载的UVM源码。自定义类的uvm_component_utils宏报错检查一下类是不是继承自uvm_component以及是否在类定义开始处包含了uvm_macros.svh。我的排查习惯是先只编译uvm.sv这个总包再编译自己的文件全部通过后再加-sv选项和incdir。如果还有问题把编译脚本里的include路径简化不要用什么通配符一行一个文件路径全都用绝对路径试一遍。有次我花了一下午排查一个编译错误最后发现是工作目录下存在一个旧版本的uvm_pkg.sv后来被默认编译进去了跟新版的类定义冲突。所以编译报错先查“是不是有重名文件混进来了”这种归因方式很管用。5.2 virtual interface空指针导致的UVM_FATAL这是最常见的UVM FATAL之一。代码里明明写了uvm_config_db#(virtual my_if)::get运行时却报UVM_FATAL... NOVIF ... virtual interface not found出现这个结果原因通常是uvm_config_db::set那行根本没执行到。比如set写在test的build_phase里但调用run_test的时机太早config_db还没建立好搜索路径。set和get的路径参数对不上。UVM的set和get第一个参数是是一个uvm_component指针路径参数是相对这个指针的搜索路径写错就取不到。新手最简单的方式是set和get都统一用null加全路径uvm_test_top.drv避免歧义。set之前interface还没实例化我就去set了会得到一个null句柄。调试方法是在set和get两端都加打印把interface句柄打印出来看是不是null。也可以用uvm_config_db#(virtual my_if)::exists来检查指定路径下是否有这个配置项。我一般在driver的build_phase里写一句if (!uvm_config_db#(virtual my_if)::exists(this, , vif)) begin uvm_error(NOIF, vif not set, please check set() code) end5.3 仿真瞬间结束objection没打对仿真一运行就直接输出--- UVM Report Summary ---看起来没报错但波形里一个信号都没动。这是新手早期最容易遇到的另一个大坑。原因几乎百分之百是test里没有raise_objection或者raise了之后很快被drop了UVM认为没有活跃的任务从而结束仿真。还有一种隐蔽情况你raise了objection但driver里的run_phase还没开始跑UVM的phase机制可能已经判断超时。解决思路确认test的run_phase里至少有一个raise_objection。用phase.phase_done这类打印确认raise和drop是否一一配对。如果你只是想让环境跑固定时长最简单的做法是repeat (N) (posedge vif.clk);让时间拉长再drop。同时最好打印几条信息方便定位。5.4 驱动信号时序错乱波形上看到毛刺和保持时间不足这个问题通常跟interface里信号的驱动方式有关系。如果你在driver的run_phase里用阻塞赋值驱动信号又恰好跟时钟沿对齐仿真器在同一个时间槽里可能会有多个事件竞争让你看到的驱动结果不稳定。最常见的现象是波形里数据在时钟沿上反复跳变像毛刺一样。推荐的做法是用非阻塞赋值或者在时钟沿之后用#1偏移来驱动。具体解释一下#1的意思是在时钟沿后延时1个仿真时间单位再更新信号这样做能有效避开时钟沿上DUT采样和driver驱动的竞争窗口。这个方法被不少老工程师视为默认写法。时序仿真的时钟频率越高这个#1的偏移量就相对越明显所以实际项目中要根据时钟周期合理设置偏移大小不能盲目用#1否则会引入保持时间问题。5.5 波形已正确但打印日志里看不到关键信息代码功能正常signals也对只是UVM日志没输出你关心的信息。这种情况一般不是功能问题而是UVM的verbosity设置太低。uvm_info默认级别是UVM_MEDIUM如果你的打印用UVM_HIGH或UVM_DEBUG控制台上就不会显示。用UVM_VERBOSITYUVM_HIGH可以把更详细的信息打开。另外uvm_error和uvm_fatal不受verbosity控制它们任何时候都会打印所以测试代码想要永久输出关键信息用它俩更稳。6. 从只有driver的最小平台再走一步6.1 下一步加monitor从纯驱动到能读输出只有driver时你是靠肉眼在波形图里看DUT输出这显然不能当自动化手段。下一步自然是加一个monitor组件专门采集DUT的输出信号再打包成事务对象。monitor的结构和driver很像同样是继承uvm_component同样需要virtual interface但它不在接口上输出激励只是在时钟沿上采样信号然后通过TLM端口发送出去。这个阶段你就开始接触UVM的TLM通信了但起步仍然不难monitor把采集到的事务发到某个analysis端口后面爱接scoreboard还是reference model都行。6.2 sequence机制接入driver从被动到主动的转变改造driver的另一个方向是用sequence生成激励。原理并不复杂driver的run_phase里不再直接写死数据流而是循环获取sequence产生的事务对象再驱动到接口上。sequence和driver之间靠sequencer连接这个机制我在这篇笔记里不展开但你必须意识到driver的工作方式发生了变化从“自己决定发什么”变成了“别人发指令你负责执行”。这正是UVM里数据激励产生与执行分层思想的体现。6.3 环境复用的威力往往是在搬运过程中体现我在实际项目里最舒服的体验是什么是把一套写好的driver、monitor基本不改从一个IP搬到另一个IP只改interface里的信号名和部分时序。UVM这套框架的复用价值在你只有driver的时候还体会不到因为就那么几行代码怎么搬都行。但等你的验证平台有几十个组件、成百上千个配置项时标准化的组件接口、标准化的配置方式、标准化的phase执行就变成救命稻草了。所以我才建议从第一个driver开始就按标准方式来写该注册就注册该走config_db就走config_db该用create就绝不用new。关于这个只有driver的最小平台最后再分享一个小技巧。我在仿真脚本里往往会额外加一条-wlf保存waveform log并且开跑前先执行log -r /*把顶级以下所有信号都记录下来。那个阶段我不知道哪个信号会出事干脆全录波形文件大一点没关系能让我在trace问题的时候少跑几遍仿真。后来遇到几次bug光靠UVM打印没法定位细节全靠那波全量波形一眼看出了异常时序。这个习惯陪我走了很久。
返回列表