ARTICLE DETAIL

资讯详情

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

从零搭建可复用UVM验证平台:架构设计与实战避坑指南

从零搭建可复用UVM验证平台:架构设计与实战避坑指南 UVM验证平台搭建这件事说难也难说简单也简单。难的是第一次接触时那一堆factory、config_db、phase机制的概念能把人绕晕简单的是一旦你理解了它用软件工程的方法来验证硬件这个核心思路剩下的就是按部就班地搭积木。我见过太多人卡在编译报错上三天动弹不得也见过有人平台跑起来了但覆盖率死活上不去。这篇内容就把我从零搭一套可复用UVM验证平台的完整过程拆开讲包括目录结构怎么设计、每个组件为什么这么写、仿真脚本怎么配、以及那些只有踩过才知道的坑。适合正在学UVM的IC验证新手也适合已经能跑通demo但想搞清楚为什么这么写的进阶者。1. 先把UVM平台的骨架想清楚再动手1.1 为什么不能上来就写代码很多人学UVM的方式是打开一个现成的demo看到driver、monitor、scoreboard一堆文件然后照着抄一遍跑通了就觉得自己会了。但一旦换一个DUT立刻又不知道从哪下手。根本原因在于没有建立平台架构的思维模型。UVM验证平台的本质是一个分层的数据流系统。最底层是DUT它有自己的引脚和时序往上一层是interface负责把物理信号抽象成逻辑信号再往上是agentagent里的driver把transaction转成pin级激励monitor把pin级信号转回transaction再往上是envenv把多个agent和scoreboard、reference model组装在一起最顶层是testtest通过config_db来配置env的行为模式。这个层次结构不是UVM强制规定的而是行业实践中总结出来的最佳组织方式。你完全可以把所有代码写在一个文件里但那样做的话换一个项目就得全部重写。分层的目的是让每一层只关心自己的事层与层之间通过标准接口通信这样DUT换了只需要改最底层的driver和monitor上层几乎不用动。1.2 目录结构的设计逻辑我在实际项目中用的目录结构是这样的uvm_project/ ├── rtl/ # DUT源代码 │ ├── dut_top.v │ └── sub_modules/ ├── tb/ │ ├── interface/ # 接口定义 │ │ └── dut_if.sv │ ├── agent/ # agent组件 │ │ ├── my_agent.sv │ │ ├── my_driver.sv │ │ ├── my_monitor.sv │ │ └── my_sequencer.sv │ ├── env/ # 环境组件 │ │ ├── my_env.sv │ │ ├── my_scoreboard.sv │ │ └── my_ref_model.sv │ ├── seq/ # 激励序列 │ │ ├── base_seq.sv │ │ └── smoke_seq.sv │ ├── test/ # 测试用例 │ │ ├── base_test.sv │ │ └── smoke_test.sv │ └── pkg/ # 包定义 │ └── my_pkg.sv ├── sim/ # 仿真脚本 │ ├── Makefile │ └── filelist.f └── doc/ # 文档这个结构的关键在于pkg目录。UVM要求所有类都定义在package里通过import来使用。很多人忽略这一点把类直接写在module里结果编译顺序一乱就报找不到类型的错误。正确的做法是把所有UVM类放在一个package中package内部通过include来引入各个文件这样编译顺序由package统一管理不会出错。1.3 编译顺序的坑UVM的编译顺序有一个基本原则被依赖的类必须先编译。比如driver依赖transaction那transaction必须定义在driver之前。在package中include的顺序就决定了编译顺序。我通常的include顺序是先include transaction和sequence_item再include sequencer、driver、monitor然后include agent接着include scoreboard和reference model最后include env和test。这个顺序保证了每个类在定义时它引用的其他类已经存在。注意如果你的transaction里用了uvm_object_utils宏而driver里用了uvm_component_utils宏这两个宏展开后会引用factory相关的静态方法所以uvm_macros.svh必须在所有类定义之前include。2. 从transaction到driver激励是怎么产生和发送的2.1 transaction的定义不只是声明字段transaction是验证平台中数据的基本单位。一个ADC验证的transaction可能包含采样率、通道号、采样数据等字段一个总线协议的transaction可能包含地址、数据、读写标志、burst长度等。定义transaction时除了声明字段还必须用uvm_object_utils宏注册到factory并实现convert2string、do_copy、do_compare等方法。这些方法在scoreboard比对和调试打印时会用到。class my_transaction extends uvm_sequence_item; rand bit [31:0] addr; rand bit [31:0] data; rand bit wr_en; uvm_object_utils_begin(my_transaction) uvm_field_int(addr, UVM_ALL_ON) uvm_field_int(data, UVM_ALL_ON) uvm_field_int(wr_en, UVM_ALL_ON) uvm_object_utils_end function new(string name my_transaction); super.new(name); endfunction virtual function string convert2string(); return $sformatf(addr0x%08h, data0x%08h, wr_en%0b, addr, data, wr_en); endfunction endclassuvm_field_int宏的作用是自动实现copy、compare、print等方法。虽然有人说用uvm_field宏会影响性能但在大多数项目中这个性能损失可以忽略不计而它带来的开发效率提升是巨大的。我建议在项目初期用uvm_field宏等到性能真的成为瓶颈时再手动实现那些方法。2.2 driver的职责边界driver只做一件事从sequencer拿到transaction然后按照DUT的时序要求把transaction驱动到interface上。它不应该做任何数据处理或协议转换的工作那些是reference model的事。class my_driver extends uvm_driver #(my_transaction); uvm_component_utils(my_driver) virtual dut_if vif; 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 dut_if)::get(this, , vif, vif)) uvm_fatal(DRV, virtual interface not set!) endfunction virtual task run_phase(uvm_phase phase); forever begin seq_item_port.get_next_item(req); drive_one_pkt(req); seq_item_port.item_done(); end endtask virtual task drive_one_pkt(my_transaction tr); (posedge vif.clk); vif.addr tr.addr; vif.data tr.data; vif.wr_en tr.wr_en; (posedge vif.clk); vif.wr_en 1b0; endtask endclass这里有一个容易忽略的细节get_next_item和item_done之间不能有阻塞等待以外的其他get_next_item调用。有些人想在driver里做流水线处理同时拿两个transaction这在UVM的sequencer-driver握手机制下是不允许的。如果需要流水线应该用try_next_item配合item_done的延迟调用来实现。2.3 sequencer和sequence的关系sequencer是driver和sequence之间的仲裁器。一个sequencer可以挂多个sequencesequencer决定哪个sequence的transaction先被driver取走。默认的仲裁算法是FIFO也可以通过set_arbitration方法改成加权轮询或随机。sequence是transaction的容器它定义了transaction产生的顺序和约束。一个最基本的sequence长这样class smoke_seq extends uvm_sequence #(my_transaction); uvm_object_utils(smoke_seq) function new(string name smoke_seq); super.new(name); endfunction virtual task body(); repeat(10) begin uvm_do_with(req, {addr inside {[0:255]}; wr_en 1;}) end endtask endclassuvm_do_with宏展开后会自动创建transaction、随机化、发送给sequencer、等待完成。如果不想用宏也可以手动写req my_transaction::type_id::create(req); start_item(req); if (!req.randomize() with {addr inside {[0:255]};}) uvm_error(SEQ, randomize failed) finish_item(req);手动写的好处是可以在randomize失败时做更精细的错误处理而uvm_do_with宏在随机化失败时只会报一个warning然后继续。3. monitor、scoreboard和覆盖率验证的闭环怎么形成3.1 monitor的采样时机决定验证准确性monitor负责观察DUT的接口信号把pin级活动转换成transaction然后发送给scoreboard和coverage collector。monitor的采样时机非常关键采早了信号还没稳定采晚了可能错过一个周期的数据。对于同步接口通常的做法是在时钟上升沿采样但要注意DUT的输出信号是在时钟上升沿之后才更新的所以monitor应该在时钟上升沿之后的某个时间点采样或者使用clocking block来定义采样时序。class my_monitor extends uvm_monitor; uvm_component_utils(my_monitor) virtual dut_if vif; uvm_analysis_port #(my_transaction) ap; function new(string name, uvm_component parent); super.new(name, parent); ap new(ap, this); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual dut_if)::get(this, , vif, vif)) uvm_fatal(MON, virtual interface not set!) endfunction virtual task run_phase(uvm_phase phase); my_transaction tr; forever begin (posedge vif.clk); if (vif.valid vif.ready) begin tr my_transaction::type_id::create(tr); tr.addr vif.addr; tr.data vif.data; tr.wr_en vif.wr_en; ap.write(tr); end end endtask endclassanalysis_port是monitor的标准输出接口它可以连接多个订阅者比如scoreboard的analysis_export和coverage collector的analysis_export。这种一对多的通信机制是UVM中decoupling的典型体现。3.2 scoreboard的比对策略scoreboard的核心工作是比较DUT的输出和reference model的输出。比较的时机有两种一种是在monitor每次发送transaction时立即比较另一种是等所有激励发完后统一比较。立即比较适合流式处理的DUT比如通信协议解析器统一比较适合有状态积累的DUT比如FIFO或缓存。我通常会在scoreboard里维护一个队列monitor每发来一个输出transaction就压入队列reference model每产生一个期望值也压入另一个队列然后在run_phase的某个时间点统一比对两个队列。class my_scoreboard extends uvm_scoreboard; uvm_component_utils(my_scoreboard) uvm_analysis_imp #(my_transaction, my_scoreboard) exp_imp; uvm_analysis_imp #(my_transaction, my_scoreboard) act_imp; my_transaction exp_queue[$]; my_transaction act_queue[$]; function new(string name, uvm_component parent); super.new(name, parent); exp_imp new(exp_imp, this); act_imp new(act_imp, this); endfunction virtual function void write_exp(my_transaction tr); exp_queue.push_back(tr); endfunction virtual function void write_act(my_transaction tr); act_queue.push_back(tr); endfunction virtual task run_phase(uvm_phase phase); my_transaction exp_tr, act_tr; forever begin wait(exp_queue.size() 0 act_queue.size() 0); exp_tr exp_queue.pop_front(); act_tr act_queue.pop_front(); if (!exp_tr.compare(act_tr)) begin uvm_error(SCB, $sformatf(Mismatch! exp: %s, act: %s, exp_tr.convert2string(), act_tr.convert2string())) end end endtask endclass这里用compare方法而不是逐字段比较是因为compare方法会调用do_compare而do_compare在用了uvm_field宏后会自动生成。如果某些字段不需要比较比如时间戳可以在do_compare中排除。3.3 覆盖率收集的实用技巧覆盖率是衡量验证完备性的关键指标。UVM中通常用covergroup来收集功能覆盖率用uvm_analysis_export把monitor的transaction导入covergroup。class my_coverage extends uvm_subscriber #(my_transaction); uvm_component_utils(my_coverage) my_transaction tr; covergroup cg; cp_addr: coverpoint tr.addr { bins low {[0:127]}; bins high {[128:255]}; } cp_wr_en: coverpoint tr.wr_en; cross_addr_wr: cross cp_addr, cp_wr_en; endgroup function new(string name, uvm_component parent); super.new(name, parent); cg new(); endfunction virtual function void write(my_transaction t); tr t; cg.sample(); endfunction endclassuvm_subscriber是UVM提供的一个便利类它内部已经包含了一个analysis_export你只需要实现write方法即可。这比手动创建analysis_imp要简洁得多。覆盖率收集有一个常见误区很多人把所有coverpoint都放在一个covergroup里结果cross覆盖率的组合爆炸导致仿真跑不完。正确的做法是按功能模块拆分covergroup每个covergroup只关注一个功能点cross也只cross相关的coverpoint。4. 仿真脚本和调试让平台真正跑起来4.1 Makefile的组织方式仿真脚本的核心是编译和运行两个步骤。我用的是Makefile因为它足够灵活而且大多数EDA工具都支持从命令行调用。UVM_HOME ? $(VCS_HOME)/etc/uvm-1.2 RTL_DIR ../rtl TB_DIR ../tb SIM_DIR . FILELIST filelist.f TOP tb_top TEST smoke_test SEED ? 1 VERDI ? 0 compile: vcs -full64 -sverilog -ntb_opts uvm-1.2 \ -timescale1ns/1ps \ -debug_accessall \ -f $(FILELIST) \ -l compile.log sim: ./simv UVM_TESTNAME$(TEST) ntb_random_seed$(SEED) -l sim.log run: compile sim verdi: verdi -ssf novas.fsdb clean: rm -rf simv* csrc *.log *.fsdb *.vpd novas* ucli.key-ntb_opts uvm-1.2告诉VCS使用内置的UVM库不需要手动指定UVM源码路径。-debug_accessall生成调试信息方便后续用Verdi看波形。UVM_TESTNAME是UVM标准参数用来指定运行哪个test。4.2 filelist的编写要点filelist.f决定了哪些文件参与编译。顺序很重要先编译interface再编译package最后编译top module。// filelist.f incdir../tb/pkg incdir../tb/agent incdir../tb/env incdir../tb/seq incdir../tb/test ../tb/interface/dut_if.sv ../tb/pkg/my_pkg.sv ../tb/top/tb_top.sv ../rtl/dut_top.vincdir指定include搜索路径这样在package里include文件时可以用相对路径。注意package必须在top module之前编译因为top module里会import package。4.3 常见编译错误和排查方法错误一uvm_component_utils宏报未定义这个错误99%是因为没有includeuvm_macros.svh或者include的顺序不对。确保在package的最开始package my_pkg; import uvm_pkg::*; include uvm_macros.svh // 其他include endpackage错误二virtual interface没有正确传递这个错误的表现是仿真开始时uvm_fatal报virtual interface not set。原因通常是uvm_config_db::set的路径和get的路径不匹配。set的时候用的是uvm_test_top或者*get的时候用的是this。// 在top module中set initial begin uvm_config_db#(virtual dut_if)::set(null, uvm_test_top.env.agt.drv, vif, dut_if_inst); run_test(); end用null作为第一个参数表示从根路径开始查找uvm_test_top.env.agt.drv是目标组件的层次路径。如果路径写错了get就会失败。调试时可以在build_phase里打印get_full_name()来确认当前组件的路径。错误三sequence没有启动如果仿真跑起来后driver一直拿不到transaction检查sequence是否在test的run_phase中启动virtual task run_phase(uvm_phase phase); smoke_seq seq; phase.raise_objection(this); seq smoke_seq::type_id::create(seq); seq.start(env.agt.sqr); phase.drop_objection(this); endtaskraise_objection和drop_objection必须成对出现否则仿真会在sequence还没跑完时就结束。这是UVM新手最容易犯的错误之一。4.4 波形调试的实用技巧Verdi是IC验证中最常用的波形调试工具。几个提高效率的技巧第一在test的build_phase中设置uvm_config_db#(int)::set(null, *, recording_detail, UVM_FULL)这样UVM会自动记录transaction的波形在Verdi中可以看到transaction级别的活动而不只是信号级别的跳变。第二用$add_wave或者Verdi的nWave界面把关键信号分组。比如把时钟和复位放在一组把总线信号放在一组把状态机信号放在一组。这样看波形时不用在几百个信号里翻找。第三在scoreboard报mismatch时用$sformatf把期望值和实际值都打印出来同时在波形中标记出错的时间点。Verdi支持在波形中添加marker方便快速定位。5. 平台复用和扩展的实战经验5.1 agent的active和passive模式一个agent可以工作在active模式或passive模式。active模式下agent的driver和sequencer都工作可以产生激励passive模式下只有monitor工作driver和sequencer不创建。这个机制在系统级验证中非常有用当你的DUT是SoC中的一个子系统时你可能只需要监控其他子系统的输出而不需要驱动它们。class my_agent extends uvm_agent; uvm_component_utils(my_agent) my_driver drv; my_monitor mon; my_sequencer sqr; function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual function void build_phase(uvm_phase phase); super.build_phase(phase); mon my_monitor::type_id::create(mon, this); if (get_is_active() UVM_ACTIVE) begin drv my_driver::type_id::create(drv, this); sqr my_sequencer::type_id::create(sqr, this); end endfunction virtual function void connect_phase(uvm_phase phase); super.connect_phase(phase); if (get_is_active() UVM_ACTIVE) begin drv.seq_item_port.connect(sqr.seq_item_export); end endfunction endclassget_is_active()的值由uvm_config_db#(uvm_active_passive_enum)::set在test中配置。默认是UVM_ACTIVE。5.2 寄存器模型的实际使用UVM寄存器模型RAL是验证平台中非常强大的一个功能但也是最容易用错的地方。寄存器模型的核心价值在于它提供了一种抽象的方式来访问DUT中的寄存器而不需要手动在driver里拼地址和数据。寄存器模型的使用分三步定义寄存器、创建寄存器块、把寄存器块集成到env中。class my_reg extends uvm_reg; uvm_object_utils(my_reg) rand uvm_reg_field data_field; function new(string name my_reg); super.new(name, 32, UVM_NO_COVERAGE); endfunction virtual function void build(); data_field uvm_reg_field::type_id::create(data_field); data_field.configure(this, 32, 0, RW, 0, 32h0, 1, 1, 0); endfunction endclass class my_reg_block extends uvm_reg_block; uvm_object_utils(my_reg_block) rand my_reg ctrl_reg; function new(string name my_reg_block); super.new(name, UVM_NO_COVERAGE); endfunction virtual function void build(); ctrl_reg my_reg::type_id::create(ctrl_reg); ctrl_reg.configure(this, null, ); ctrl_reg.build(); default_map create_map(default_map, 0, 4, UVM_LITTLE_ENDIAN); default_map.add_reg(ctrl_reg, 32h0, RW); endfunction endclass寄存器模型最常用的操作是read和write它们会自动生成总线transaction并通过sequencer发送。但要注意read和write是阻塞操作在sequence中调用时需要确保sequencer没有被其他sequence占用。提示寄存器模型的mirror值镜像值和desired值期望值是两个容易混淆的概念。mirror值反映的是最后一次read或write后寄存器的实际值desired值是你希望寄存器被设置成的值。调用update方法会把desired值写入DUT调用mirror方法会从DUT读回实际值更新mirror值。5.3 平台复用的经验总结一套UVM平台搭好后下一个项目怎么复用我的经验是把与DUT无关的部分抽出来做成基类库与DUT相关的部分做成派生类。具体来说base_driver、base_monitor、base_scoreboard这些基类只包含通用的逻辑比如get_next_item、analysis_port的创建、队列管理具体的信号驱动、协议解析、比对规则放在派生类中实现。这样新项目只需要写派生类基类库直接复用。另外interface的定义要尽量通用。比如一个AXI接口的interface不要在里面写死地址位宽和数据位宽而是用parameter来定义。这样同一个interface可以用在不同配置的AXI总线上。5.4 仿真性能优化的几个手段当验证平台规模变大后仿真速度会成为瓶颈。几个实用的优化手段第一减少不必要的uvm_info打印。uvm_info的verbosity等级设置为UVM_HIGH或UVM_FULL时大量打印会显著拖慢仿真速度。在回归测试中把verbosity设为UVM_LOW只保留关键信息。第二用uvm_config_db的set和get时尽量用具体的路径而不是通配符*。通配符会导致UVM遍历整个组件树来匹配组件越多性能越差。第三scoreboard的比对不要在每个transaction到达时立即做而是攒一批再比。这样可以减少进程切换的开销。第四如果仿真平台用了大量的fork-join考虑用fork-join_none配合event来替代减少线程创建的开销。6. 从跑通到跑好验证收敛的判断标准6.1 覆盖率不是唯一指标很多人以为覆盖率到了100%就说明验证完了这是一个危险的误解。覆盖率只说明你访问了所有的功能点但不说明你验证了所有的功能点。比如一个FIFO的coverpoint定义了满和空两种状态覆盖率100%只说明你让FIFO满过也空过但不说明你在满的时候写入数据是否正确、在空的时候读取数据是否报错。真正的验证收敛需要同时满足三个条件代码覆盖率行覆盖、条件覆盖、翻转覆盖达到目标、功能覆盖率covergroup达到目标、所有定向测试用例通过。这三个条件缺一不可。6.2 回归测试的组织回归测试是把所有test case跑一遍确认没有引入新的bug。回归测试的组织方式有两种一种是串行跑一个test跑完再跑下一个另一种是并行跑多个test同时跑。串行跑的好处是资源占用少坏处是耗时长。并行跑的好处是耗时短坏处是需要更多的license和计算资源。在实际项目中我通常会把test case分成几组每组内的test串行跑组与组之间并行跑。这样既控制了资源占用又缩短了总时间。回归测试的结果分析也很重要。不要只看pass/fail还要看每个test的覆盖率贡献。有些test可能pass了但覆盖率贡献为零说明这个test没有覆盖到新的功能点需要检查test的激励是否足够随机。6.3 验证平台的可维护性一个验证平台的生命周期通常比DUT本身还长。DUT可能改了好几版但验证平台一直在用。所以平台的可维护性非常重要。提高可维护性的几个做法第一所有类名、变量名、宏名都要有意义的命名不要用a、b、c这种。第二每个类都要有注释说明它的职责和用法。第三把常用的配置参数放在一个单独的文件中不要散落在各个类里。第四定期清理不再使用的代码和文件。我在实际项目中踩过的一个坑是平台搭好后没有及时写文档过了三个月自己都忘了某个config_db的set是在哪里做的。后来花了半天时间才找到。从那以后我养成了一个习惯每添加一个config_db的set就在一个专门的配置文档中记录一条。6.4 团队协作中的平台管理如果是多人协作开发验证平台版本管理就很重要。我推荐用Git来管理平台代码每个功能模块一个分支开发完成后合并到主分支。合并时要注意冲突解决。UVM平台中最容易冲突的文件是package文件因为所有人都要往里面加include。解决方法是把package文件拆成多个小package每个人只改自己的package最后在一个顶层package中import所有小package。另外仿真脚本也要纳入版本管理。Makefile、filelist.f、编译选项这些都要统一否则不同人跑出来的结果可能不一致。验证平台搭建这件事说到底是一个工程实践问题不是理论问题。你看再多UVM的书不如自己动手搭一个完整的平台跑一遍。我当初学UVM的时候光是搞明白config_db的路径匹配就花了两天但搞明白之后发现其实就那么回事。希望这篇内容能帮你少走一些弯路把更多时间花在验证DUT的功能上而不是跟平台的编译错误较劲。
返回列表