ARTICLE DETAIL

资讯详情

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

UVM验证平台搭建实战:从架构设计到覆盖率驱动

UVM验证平台搭建实战:从架构设计到覆盖率驱动 1. UVM验证平台搭建的整体设计思路1.1 为什么选择UVM而不是传统定向测试做数字IC验证这行的朋友应该都有体会十年前大家还在用Verilog写定向测试用例一个模块几十个case跑一遍仿真等半天覆盖率报告出来一看分支覆盖率还不到70%。后来VMM、OVM陆续出现直到UVM 1.0正式发布并成为IEEE 1800.2标准整个行业才算有了统一的验证方法学。我最初从定向测试转向UVM的时候最大的感受就是前期搭建平台的投入确实大但一旦跑通后续加case的成本几乎可以忽略不计。UVM的核心价值在于它提供了一套标准化的验证组件库和通信机制。driver、monitor、sequencer、agent、env、test这六大组件构成了一个层次分明的结构每个组件各司其职。相比传统定向测试把所有逻辑塞在一个initial块里UVM的模块化设计让验证平台具备了可复用性——今天验证AXI总线明天验证DDR控制器agent稍作修改就能直接搬过去用。另一个关键考量是覆盖率驱动验证。UVM内建了覆盖率的收集机制功能覆盖率和代码覆盖率可以统一管理。这意味着你不需要手动去追踪哪些场景还没测到工具会自动告诉你哪里还有漏洞。我做过一个对比同一个DUT定向测试写了120个case才达到85%的功能覆盖率而UVM平台用30个sequence就达到了95%以上效率差距非常明显。1.2 验证平台的层次架构拆解一个标准的UVM验证平台从顶到底可以分成这么几层Test层最顶层负责配置环境、启动sequence、控制仿真流程。不同的test对应不同的测试场景比如smoke_test只跑基本读写stress_test跑满带宽压力测试。Env层封装所有验证组件提供统一的接口给test调用。env里面通常包含多个agent、scoreboard、coverage collector等。Agent层对应一个物理接口内部包含driver、monitor、sequencer。agent分为active和passive两种模式active模式下driver主动驱动信号passive模式下只做监测。Driver/Monitor层driver负责把transaction转换成pin级信号monitor负责把pin级信号还原成transaction。Sequence/Sequence_item层sequence定义激励的生成方式sequence_item定义transaction的数据结构。这种分层架构的好处是职责清晰。我曾经接手过一个别人搭的平台driver里面混了检查逻辑monitor里面又去驱动信号改一个地方牵一发而动全身。后来按照标准UVM架构重构之后每个组件的代码量都不超过200行维护起来轻松很多。1.3 仿真工具选型与版本匹配仿真工具这块目前主流的就是Synopsys VCS、Cadence Xcelium和Siemens Questa原Mentor ModelSim/QuestaSim。三家的工具我都用过各有特点工具优势注意事项VCS编译速度快对SystemVerilog支持好需要配合Verdi看波形license成本高Xcelium多核仿真性能强适合大规模SoC对UVM版本有要求建议1.2以上Questa上手简单图形界面友好仿真速度相对慢一些适合中小规模版本匹配是个容易被忽视的坑。UVM 1.2和VCS 2019之前的版本配合时uvm_config_db的set/get有时会出现时序问题。我一般建议用UVM 1.2配合VCS 2020.03以后的版本或者UVM 1.1d配合Questa 10.7以上。另外如果DUT里面有加密IP还要确认仿真工具是否支持对应的加密格式。2. 核心组件的实现细节与代码要点2.1 Transaction与Sequence的定义技巧Transaction是整个验证平台的数据载体它的字段设计直接决定了后续sequence的灵活度。以AXI总线为例一个写transaction通常包含class axi_write_trans extends uvm_sequence_item; rand bit [31:0] addr; rand bit [31:0] data; rand bit [3:0] strb; rand bit [7:0] len; rand bit [2:0] size; rand bit [1:0] burst; uvm_object_utils_begin(axi_write_trans) uvm_field_int(addr, UVM_ALL_ON) uvm_field_int(data, UVM_ALL_ON) uvm_field_int(strb, UVM_ALL_ON) uvm_field_int(len, UVM_ALL_ON) uvm_field_int(size, UVM_ALL_ON) uvm_field_int(burst, UVM_ALL_ON) uvm_object_utils_end constraint c_addr_align { addr % (1 size) 0; } constraint c_burst_type { burst inside {2b01, 2b10}; // INCR和WRAP } function new(string name axi_write_trans); super.new(name); endfunction endclass这里有几个经验点值得说。第一constraint要分层写把地址对齐、burst类型这些硬性约束和业务场景约束分开方便后续在sequence里用constraint_mode()动态开关。第二uvm_field_int宏虽然方便但在大transaction上会影响仿真性能如果字段超过20个建议手写do_copy、do_compare和convert2string。第三convert2string一定要实现不然打印log的时候只能看到一堆地址排查问题非常痛苦。Sequence的定义要注意body()任务的阻塞语义。我见过有人在body()里写了个死循环结果仿真跑了一天都没结束。正确的做法是用repeat或者foreach控制循环次数并且在每次发送transaction之间加适当的延时。2.2 Driver的实现与时钟同步Driver的核心任务是从sequencer拿到transaction然后按照接口时序驱动信号。以AXI写通道为例class axi_master_driver extends uvm_driver #(axi_write_trans); virtual axi_if vif; uvm_component_utils(axi_master_driver) 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 axi_if)::get(this, , vif, vif)) uvm_fatal(DRV, Virtual interface not set!) endfunction virtual task run_phase(uvm_phase phase); vif.awvalid 1b0; vif.wvalid 1b0; vif.bready 1b0; forever begin seq_item_port.get_next_item(req); drive_write(req); seq_item_port.item_done(); end endtask virtual task drive_write(axi_write_trans tr); // 驱动地址通道 (posedge vif.aclk); vif.awaddr tr.addr; vif.awlen tr.len; vif.awsize tr.size; vif.awburst tr.burst; vif.awvalid 1b1; do (posedge vif.aclk); while (!vif.awready); vif.awvalid 1b0; // 驱动数据通道 for (int i 0; i tr.len; i) begin (posedge vif.aclk); vif.wdata tr.data i * 4; vif.wstrb tr.strb; vif.wlast (i tr.len); vif.wvalid 1b1; do (posedge vif.aclk); while (!vif.wready); end vif.wvalid 1b0; vif.wlast 1b0; // 等待响应 (posedge vif.aclk); vif.bready 1b1; do (posedge vif.aclk); while (!vif.bvalid); vif.bready 1b0; endtask endclass时钟同步是driver最容易出问题的地方。上面代码里所有的信号赋值都放在(posedge vif.aclk)之后这是为了避免竞争冒险。如果用非阻塞赋值信号会在时钟沿之后更新正好被DUT在下一个时钟沿采样到。如果用阻塞赋值信号会立即更新可能导致DUT在同一个时钟沿采样到不稳定的值。另一个坑是复位处理。很多新手忘记在driver里处理复位信号结果仿真一开始DUT还没复位完driver就开始驱动transaction了。正确的做法是在run_phase开头等待复位释放或者在build_phase里检查复位状态。2.3 Monitor的采样策略与覆盖率收集Monitor负责监测接口上的信号把pin级活动还原成transaction然后发送给scoreboard和coverage collector。Monitor的采样策略有两种基于时钟沿采样和基于事件触发采样。基于时钟沿采样适合同步接口每个时钟沿检查valid和ready信号。基于事件触发采样适合异步接口用(posedge valid)这样的方式触发。我一般推荐用时钟沿采样因为这样更容易和DUT的时序对齐。class axi_monitor extends uvm_monitor; virtual axi_if vif; uvm_analysis_port #(axi_write_trans) ap; uvm_component_utils(axi_monitor) function new(string name, uvm_component parent); super.new(name, parent); ap new(ap, this); endfunction virtual task run_phase(uvm_phase phase); forever begin axi_write_trans tr; collect_write(tr); ap.write(tr); end endtask virtual task collect_write(output axi_write_trans tr); tr axi_write_trans::type_id::create(tr); // 等待地址通道握手 (posedge vif.aclk); while (!(vif.awvalid vif.awready)) (posedge vif.aclk); tr.addr vif.awaddr; tr.len vif.awlen; tr.size vif.awsize; tr.burst vif.awburst; // 采集数据通道 for (int i 0; i tr.len; i) begin (posedge vif.aclk); while (!(vif.wvalid vif.wready)) (posedge vif.aclk); if (i 0) tr.data vif.wdata; end // 等待响应通道 (posedge vif.aclk); while (!(vif.bvalid vif.bready)) (posedge vif.aclk); endtask endclass覆盖率收集是monitor的隐藏职责。很多人把coverage collector单独做成一个组件其实monitor采集完transaction之后直接调用covergroup的sample()方法更高效。covergroup的定义要注意地址覆盖按区域划分bin比如低地址段、中地址段、高地址段每个段再细分。数据覆盖关注特殊值全0、全1、0xAAAA_AAAA、0x5555_5555这些边界值。长度覆盖AXI的burst长度从1到256不可能每个都测一般选1、2、4、8、16、256这几个典型值。交叉覆盖地址区域和burst长度的交叉这个最容易发现边界问题。2.4 Scoreboard的比对逻辑与参考模型Scoreboard是验证平台的“裁判”负责比对DUT输出和参考模型的预期输出。参考模型的实现方式有三种事务级模型用SystemVerilog写一个行为级模型直接处理transaction。速度快但精度有限。周期级模型精确到每个时钟周期的行为可以捕捉时序问题。开发工作量大。C参考模型用C/C写模型通过DPI-C接口调用。适合复杂算法但调试麻烦。我一般推荐事务级模型因为大部分数字逻辑的功能正确性可以在事务级别验证时序问题交给静态时序分析和门级仿真。事务级模型的写法class axi_scoreboard extends uvm_scoreboard; uvm_component_utils(axi_scoreboard) uvm_analysis_imp #(axi_write_trans, axi_scoreboard) wr_imp; uvm_analysis_imp #(axi_read_trans, axi_scoreboard) rd_imp; bit [31:0] mem[bit [31:0]]; function new(string name, uvm_component parent); super.new(name, parent); wr_imp new(wr_imp, this); rd_imp new(rd_imp, this); endfunction virtual function void write_write(axi_write_trans tr); for (int i 0; i tr.len; i) begin mem[tr.addr i * (1 tr.size)] tr.data i * 4; end endfunction virtual function void write_read(axi_read_trans tr); bit [31:0] expected; for (int i 0; i tr.len; i) begin if (!mem.exists(tr.addr i * (1 tr.size))) begin uvm_error(SCB, $sformatf(Read uninitialized address: 0x%0h, tr.addr)) continue; end expected mem[tr.addr i * (1 tr.size)]; if (tr.data[i] ! expected) begin uvm_error(SCB, $sformatf(Data mismatch at addr 0x%0h: exp0x%0h, got0x%0h, tr.addr, expected, tr.data[i])) end end endfunction endclassScoreboard的常见坑是比对时机。如果monitor在DUT输出有效之前就把transaction发给了scoreboardscoreboard会误报错误。解决办法是在monitor里加适当的延时或者用uvm_analysis_imp的阻塞版本。另外内存模型要初始化不然读未初始化地址会报一堆假错误。3. 仿真环境的搭建与运行流程3.1 文件组织与编译脚本一个规范的UVM项目文件目录结构应该是这样的project/ ├── rtl/ # DUT源代码 │ ├── axi_slave.v │ └── top.v ├── tb/ # 验证平台 │ ├── agents/ │ │ ├── axi_master_agent/ │ │ └── axi_slave_agent/ │ ├── env/ │ ├── tests/ │ └── top/ ├── sim/ # 仿真脚本 │ ├── Makefile │ └── filelist.f └── doc/ # 文档编译脚本用Makefile管理最方便UVM_HOME /tools/uvm-1.2 VCS_HOME /tools/vcs-2020.03 FILELIST ./sim/filelist.f TESTNAME axi_smoke_test SEED 1 compile: vcs -full64 -sverilog -ntb_opts uvm-1.2 \ -timescale1ns/1ps \ -debug_accessall \ -f $(FILELIST) \ -l compile.log sim: ./simv UVM_TESTNAME$(TESTNAME) \ ntb_random_seed$(SEED) \ -l sim.log verdi: verdi -ssf novas.fsdb clean: rm -rf simv* csrc *.log *.fsdb novas.*filelist.f的写法有讲究顺序很重要# 先编译UVM库 incdir$(UVM_HOME)/src $(UVM_HOME)/src/uvm_pkg.sv # 再编译接口 ./tb/top/axi_if.sv # 然后编译验证组件 ./tb/agents/axi_master_agent/axi_write_trans.sv ./tb/agents/axi_master_agent/axi_master_driver.sv ./tb/agents/axi_master_agent/axi_monitor.sv ./tb/agents/axi_master_agent/axi_master_agent.sv # 最后编译env和test ./tb/env/axi_env.sv ./tb/tests/axi_smoke_test.sv # DUT放最后 ./rtl/axi_slave.v ./rtl/top.v编译顺序错了会报“type not found”错误这是新手最常遇到的问题。SystemVerilog是顺序编译的后面的文件引用前面的类型所以被引用的文件必须放在前面。3.2 仿真运行与波形调试仿真运行命令里UVM_TESTNAME指定要跑的testntb_random_seed指定随机种子。种子管理是个大学问我一般会跑一组种子比如1到100看看有没有种子相关的失败。如果某个种子必现失败那就是真bug如果只是偶发可能是竞争冒险或者初始化顺序问题。波形调试用Verdi最方便verdi -ssf novas.fsdb -nologo 在Verdi里看UVM的transaction需要加载UVM的调试插件。VCS编译时加-debug_accessall仿真时加UVM_TRANSACTION_RECORDING这样Verdi就能显示transaction的层次结构了。看波形的几个技巧用CtrlG快速跳转到指定信号比在层次树里翻快得多。用Event窗口看UVM的phase跳转确认每个phase是否正常执行。用Transaction窗口看sequence的发送记录确认激励是否符合预期。用Signal窗口的Bus模式看总线信号比单个bit看直观很多。3.3 回归测试与覆盖率分析单个test跑通只是第一步回归测试才是验证工作的主体。回归测试的流程把所有test列在一个文件里每行一个test名。用脚本批量跑每个test跑多个种子。收集所有test的覆盖率合并成一个总覆盖率报告。分析覆盖率漏洞补充新的test。覆盖率合并用urg命令urg -dir simv.vdb -dir simv2.vdb -report coverage_report覆盖率分析要看三个维度代码覆盖率行覆盖率、分支覆盖率、条件覆盖率、翻转覆盖率。一般要求行覆盖率100%分支覆盖率95%以上。功能覆盖率covergroup定义的覆盖点。这个和验证计划直接对应要求100%。断言覆盖率SVA断言的成功/失败统计。这个反映设计的时序正确性。我见过很多项目代码覆盖率很高但功能覆盖率很低说明测试用例没有覆盖到关键场景。功能覆盖率才是验证充分性的真正指标。4. 常见问题排查与实战避坑指南4.1 编译与运行阶段的典型错误问题一uvm_config_db的set/get不匹配这是UVM新手遇到最多的错误。典型现象是driver里的virtual interface是null仿真一跑就报“null pointer dereference”。// 错误写法set和get的路径不匹配 uvm_config_db#(virtual axi_if)::set(null, uvm_test_top.env.agt.drv, vif, vif); uvm_config_db#(virtual axi_if)::get(this, , vif, vif); // 路径不对 // 正确写法用this指针自动匹配路径 uvm_config_db#(virtual axi_if)::set(this, drv, vif, vif); uvm_config_db#(virtual axi_if)::get(this, , vif, vif);排查技巧在build_phase里加uvm_config_db#(virtual axi_if)::dump()可以看到当前层次下所有的config设置。问题二Phase执行顺序混乱UVM的phase执行顺序是固定的build → connect → end_of_elaboration → start_of_simulation → run → extract → check → report。如果build_phase里依赖了connect_phase才建立的连接就会出问题。排查技巧在base_test里重载每个phase加打印信息确认phase执行顺序。问题三仿真不收敛仿真发散现象是仿真跑着跑着就卡住了或者波形上出现大量X。常见原因组合逻辑环路A依赖BB又依赖A没有时钟同步。复位未释放DUT一直处于复位状态driver却在驱动信号。时钟未启动DUT的时钟由testbench产生但testbench忘记启动时钟。排查技巧用Verdi的X-Propagation功能追踪X的来源或者用$dumpvars只dump关键信号缩小排查范围。4.2 常见问题速查表问题现象可能原因解决方法Driver收不到transactionsequencer和driver未连接检查connect_phase里的drv.seq_item_port.connect(seqr.seq_item_export)Monitor采不到数据采样时机不对改用时钟沿采样或在valid信号上沿触发Scoreboard误报比对时机太早在monitor里加延时或改用阻塞式analysis port覆盖率上不去激励不够随机检查constraint是否过紧增加rand字段仿真速度慢打印信息太多降低verbosity关闭不必要的uvm_info波形文件太大dump了所有信号只dump关键信号或用$dumpvars分层dump随机种子相关失败竞争冒险检查driver的赋值时序用非阻塞赋值UVM_FATAL报错config_db未设置用uvm_config_db::dump()排查4.3 独家避坑经验分享经验一Transaction的打印要克制uvm_info的verbosity默认是UVM_MEDIUM如果每个transaction都打印仿真log会爆炸。我一般把transaction的打印设为UVM_HIGH只在调试时打开。另外uvm_object_utils里的uvm_field_int宏会自动生成print方法但打印格式很丑建议手写convert2string。经验二Sequence的启动方式要统一UVM启动sequence有三种方式start()、default_sequence、config_db。我推荐统一用start()因为这样最灵活可以在test里控制sequence的启动时机和数量。default_sequence虽然方便但调试时不容易追踪。经验三覆盖率收集要趁早不要等到所有test写完才开始收集覆盖率。我一般会在第一个test跑通后就把covergroup加进去这样后续每加一个test都能看到覆盖率的变化。覆盖率收集的代码量不大但收益很高。经验四回归测试要自动化手动跑test是验证工程师最大的时间浪费。我一般用Python脚本管理回归测试import subprocess import os tests [axi_smoke_test, axi_stress_test, axi_error_test] seeds range(1, 11) for test in tests: for seed in seeds: cmd f./simv UVM_TESTNAME{test} ntb_random_seed{seed} -l {test}_{seed}.log result subprocess.run(cmd, shellTrue, capture_outputTrue) if result.returncode ! 0: print(fFAIL: {test} seed{seed}) else: print(fPASS: {test} seed{seed})经验五版本管理要规范UVM平台的代码量不小一定要用Git管理。我一般会分三个分支main放稳定版本dev放开发版本feature/xxx放新功能。每次跑回归测试前先commit这样出问题可以快速回滚。经验六文档和注释不能省验证平台的代码是团队协作的产物没有文档和注释别人根本看不懂。我一般要求每个class头部写清楚功能说明每个task/function写清楚输入输出和副作用。特别是sequence的约束一定要注释清楚每个约束的业务含义。经验七仿真性能优化大规模SoC的仿真可能跑几个小时甚至几天性能优化很重要。几个技巧用defineSIMULATION宏关掉DUT里的调试逻辑。用$dumpvars只dump关键信号不要dump整个层次。用VCS的-fgp选项开启细粒度并行多核加速。用uvm_info的verbosity控制打印量生产环境设为UVM_LOW。经验八跨平台移植要注意如果验证平台需要在VCS和Xcelium之间切换要注意几个兼容性问题$fsdbDumpfile是Verdi专用的Xcelium要用$shm_open。uvm_config_db的路径分隔符在两家工具里都是.但有些版本对*通配符的支持不一样。宏定义UVM_NO_DEPRECATED可以屏蔽掉废弃的API提高跨平台兼容性。5. 验证平台的扩展与进阶方向5.1 寄存器模型RAL的集成UVM寄存器模型是验证平台进阶的必经之路。它的核心价值是提供一种抽象的方式来访问DUT的寄存器不用再手动拼地址和数据。寄存器模型的搭建流程用IP-XACT或Excel描述寄存器列表。用工具如Synopsys的ralgen自动生成寄存器模型代码。在env里实例化寄存器模型连接到adapter。在test里用reg_model.ctrl_reg.write()这样的方式访问寄存器。寄存器模型的镜像值mirror value是个容易混淆的概念。镜像值是寄存器模型内部保存的寄存器值它和DUT的实际值可能不一致。当你调用write()时模型会更新镜像值当你调用read()时模型会从DUT读回实际值并更新镜像。如果DUT的寄存器被硬件自动修改了镜像值就会和实际值不一致这时候需要调用mirror()来同步。5.2 基于Verilator的加速仿真Verilator是一个开源的Verilog仿真器它把Verilog编译成C代码仿真速度比传统事件驱动仿真器快10到100倍。但Verilator只支持可综合的Verilog不支持SystemVerilog的验证特性。所以通常的做法是DUT用Verilator编译testbench用SystemVerilog写通过DPI-C接口通信。这种方案适合大规模回归测试可以大幅缩短仿真时间。但搭建复杂度较高需要处理接口转换和数据类型映射。我一般只在项目后期回归测试量很大的时候才考虑引入Verilator。5.3 形式验证与UVM的结合形式验证Formal Verification和UVM仿真不是替代关系而是互补关系。形式验证擅长证明设计的数学性质比如“FIFO永远不会溢出”UVM仿真擅长验证复杂场景比如“总线在压力下的性能”。我一般用形式验证来验证控制逻辑用UVM来验证数据通路。把形式验证和UVM结合的一个技巧是用形式验证生成覆盖率然后把覆盖率导入UVM的回归测试看看哪些场景是形式验证覆盖了但仿真没覆盖的。这样可以避免重复劳动提高验证效率。5.4 UVM平台的持续集成持续集成CI是现代验证流程的标配。我一般用Jenkins或GitLab CI来管理UVM的回归测试每次代码提交触发编译和冒烟测试。每天夜间跑完整回归测试。每周生成覆盖率报告分析趋势。失败case自动发送邮件通知。CI的配置文件如.gitlab-ci.yml大概长这样stages: - compile - smoke - regression compile: stage: compile script: - make compile smoke: stage: smoke script: - make sim TESTNAMEaxi_smoke_test regression: stage: regression script: - python run_regression.py only: - schedules这套流程跑通之后验证工程师只需要关注失败case的分析重复性的跑仿真工作全部自动化效率提升非常明显。我个人在实际操作中的体会是UVM验证平台的搭建不是一蹴而就的前期投入可能占整个验证周期的30%到40%但后期加case和调试的效率会成倍提升。最关键的是架构要清晰driver、monitor、scoreboard各司其职不要为了图省事把逻辑混在一起。另外覆盖率驱动是验证工作的核心方法论不要盲目追求case数量要看覆盖率漏洞在哪里有针对性地补充激励。最后再分享一个小技巧每次跑回归测试前先用一个小种子集比如3个种子快速验证平台是否正常确认没问题再跑全量种子这样可以避免因为平台bug浪费大量仿真时间。
返回列表