ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

30分钟搭建首个UVM验证环境:从零跑通加法器验证全流程

30分钟搭建首个UVM验证环境:从零跑通加法器验证全流程 1. 项目概述从零到一跑通你的第一个UVM验证环境如果你是一名刚接触芯片验证的工程师或者是一名在校学生面对UVMUniversal Verification Methodology这座大山感觉无从下手那么这篇文章就是为你准备的。我见过太多新手一上来就试图去啃《UVM实战》这种大部头或者直接看公司里庞大而复杂的验证平台结果被里面层层叠叠的类、眼花缭乱的机制搞得晕头转向很快就失去了信心。学习UVM最忌讳的就是一开始就陷入细节的泥潭。最有效的方法恰恰是反其道而行之先别管那么多为什么亲手把一个最简单的、能跑的UVM环境搭建起来并看到它成功运行。这个过程就像学开车你得先坐进驾驶室点火、挂挡、松手刹让车动起来哪怕只是在空地上转两圈那种“我能控制它”的感觉是看再多说明书也换不来的。今天我就带你完整地跑一个UVM的demo。这个demo麻雀虽小五脏俱全它包含了一个最简单的DUTDesign Under Test待测设计、一个完整的UVM验证环境结构、以及一键执行的脚本。我们的目标非常明确让你在30分钟内看到第一个UVM测试用例Test Case从编译、仿真到打印出“UVM_INFO”和“UVM_ERROR”的全过程。你会看到transaction是如何被sequence产生通过sequencer发送给driver驱动到DUT接口再由monitor采集回来送给scoreboard进行比对的全流程。当你看到终端上打印出“UVM TEST PASSED”时你对UVM的抽象认知就第一次落到了实处。这个demo的价值在于“可复现”。我提供的源码和脚本已经处理好了环境依赖和路径问题你只需要准备好最基本的EDA工具比如Mentor的QuestaSim、Cadence的Xcelium或Synopsys的VCS和UVM库按照步骤执行几条命令就能看到结果。这能极大降低你的起步门槛建立初步的信心和兴趣。接下来我们就从最基础的环境准备开始。2. 环境准备与工具链配置在开始运行任何代码之前一个稳定、干净的环境是成功的第一步。很多新手在这里就会踩坑比如工具版本不兼容、环境变量没设置、或者UVM库路径不对。我会把每一步都拆开讲清楚确保你能一次搞定。2.1 基础软件与工具选择首先你需要一个Linux操作系统。芯片验证工具链几乎都构建在Linux之上推荐使用CentOS 7/8或者Ubuntu 18.04/20.04这些比较稳定的发行版。Windows用户可以通过WSL2Windows Subsystem for Linux来获得一个接近原生Linux的体验这已经足够用于学习和运行demo。核心工具是仿真器。对于个人学习我强烈推荐Mentor Graphics的QuestaSim现在属于Siemens EDA。它的起步版QuestaSim Starter Edition功能齐全对UVM支持良好而且网上资源丰富遇到问题容易找到解决方案。你可以从Siemens官网申请免费license或者找到学习版本。作为备选Synopsys VCS功能强大但配置更复杂Cadence Xcelium亦然。我们这个demo以QuestaSim为例进行讲解但提供的Makefile脚本通常也兼容VCS的基本命令。除了仿真器你还需要一个代码编辑器。VSCode是绝佳选择安装SystemVerilog和UVM相关的插件如“SystemVerilog”插件后可以获得语法高亮、代码跳转等功能极大提升编码和阅读效率。2.2 UVM库的获取与配置UVM不是一个软件而是一个用SystemVerilog编写的类库。仿真器需要知道这个库在哪里才能正确编译和链接。通常你的仿真器安装包里会自带UVM库。以QuestaSim为例UVM库的路径可能类似于/opt/mentor/questasim/verilog_src/uvm-1.2。你需要找到这个路径。关键的一步是设置环境变量UVM_HOME让它指向UVM库的根目录。同时将仿真器的可执行文件路径比如/opt/mentor/questasim/bin添加到系统的PATH环境变量中。这样你才能在终端里直接使用vsim等命令。# 假设你的QuestaSim安装在/opt/mentor/questasimUVM库在其子目录下 export MENTOR_HOME/opt/mentor/questasim export UVM_HOME$MENTOR_HOME/verilog_src/uvm-1.2 export PATH$MENTOR_HOME/bin:$PATH # 你可以将以上命令添加到你的 ~/.bashrc 文件中这样每次打开终端都会自动设置 echo ‘export MENTOR_HOME/opt/mentor/questasim’ ~/.bashrc echo ‘export UVM_HOME$MENTOR_HOME/verilog_src/uvm-1.2’ ~/.bashrc echo ‘export PATH$MENTOR_HOME/bin:$PATH’ ~/.bashrc source ~/.bashrc注意UVM有多个版本如1.1d, 1.2, 1800.2-2017。我们这个demo基于广泛使用的UVM-1.2编写确保你的UVM_HOME指向正确的版本目录。版本不匹配是导致编译错误的一个常见原因。2.3 Demo源码结构与获取一个清晰的目录结构能让项目管理变得轻松。我们的demo采用如下结构uvm_demo/ ├── README.md # 项目说明和快速启动指南 ├── Makefile # 一键编译运行脚本 ├── dut/ # 待测设计DUT目录 │ ├── simple_adder.sv # 一个简单的加法器设计 │ └── simple_adder_if.sv # 加法器的接口定义 ├── tb/ # 测试平台Testbench目录 │ ├── uvm_tb_pkg.sv # 包含所有UVM组件的Package │ ├── simple_adder_test.sv # 顶层测试模块 │ └── ... (其他组件文件下面会详述) └── run/ # 运行目录 ├── compile.f # 编译文件列表 └── waves.do # 波形查看脚本你可以通过我提供的链接直接下载这个压缩包或者使用git clone命令获取源码。拿到源码后第一件事就是浏览README.md它包含了最简短的依赖说明和运行命令。然后用tree命令如果系统没有用find . -type f代替查看一下整体结构建立初步印象。3. Demo核心组件与UVM架构解析现在我们深入这个demo的“心脏”看看一个最小化的UVM验证平台是如何构建的。理解每个组件的职责和它们之间的连接关系比死记硬背UVM基类的方法更重要。3.1 待测设计DUT一个简单的加法器为了聚焦于UVM本身我们选用一个极其简单的DUT一个带有时钟和复位信号的32位加法器。// file: dut/simple_adder.sv module simple_adder ( input logic clk, input logic rst_n, input logic [31:0] in_a, input logic [31:0] in_b, input logic in_valid, output logic [31:0] out_sum, output logic out_valid ); always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin out_sum 32‘h0; out_valid 1‘b0; end else if (in_valid) begin out_sum in_a in_b; // 核心计算逻辑 out_valid 1‘b1; end else begin out_valid 1‘b0; end end endmodule同时我们为这个DUT定义一个SystemVerilog接口Interface这是连接DUT和UVM验证环境的关键桥梁。接口将一组相关的信号封装在一起便于在验证环境中传递和操作。// file: dut/simple_adder_if.sv interface simple_adder_if (input logic clk, input logic rst_n); logic [31:0] in_a; logic [31:0] in_b; logic in_valid; logic [31:0] out_sum; logic out_valid; // 可以在这里定义clocking block和modport用于驱动和采样时序 clocking driver_cb (posedge clk); default input #1ns output #1ns; output in_a, in_b, in_valid; input out_sum, out_valid; endclocking clocking monitor_cb (posedge clk); default input #1ns; input in_a, in_b, in_valid, out_sum, out_valid; endclocking endinterface实操心得即使对于如此简单的DUT也坚持使用Interface。这不仅是UVM的推荐做法更能让你从一开始就养成好习惯。在复杂设计中Interface能极大简化连接关系并支持虚接口Virtual Interface机制这是UVM动态环境与静态RTL模块通信的基石。3.2 UVM验证环境全景图我们的UVM测试平台Testbench将包含以下经典组件它们共同构成了一个层次化的结构Transactionsimple_adder_transaction数据建模的最小单位代表一次加法操作包含in_a、in_b以及预期的exp_sum等字段。Sequencesimple_adder_sequence负责产生一系列有意义的Transaction。在我们的demo里它会随机生成若干对操作数。Sequenceruvm_sequencer #(simple_adder_transaction)作为Sequence和Driver之间的仲裁器和管道按顺序将Sequence产生的Transaction送给Driver。Driversimple_adder_driver从Sequencer获取Transaction按照DUT接口的时序协议本例中是在in_valid为高时驱动in_a和in_b将数据驱动到DUT的输入端口。Monitorsimple_adder_monitor被动观察DUT的接口信号当检测到一次完整的加法操作完成即out_valid拉高时将采集到的输入输出数据封装成一个新的Transaction并发送出去。Agentsimple_adder_agent一个容器组件将Driver、Sequencer和Monitor封装在一起并配置它们是否处于活跃Active模式。活跃模式下Agent会驱动激励被动模式下它只做监测。Scoreboardsimple_adder_scoreboard验证的“裁判”。它订阅Monitor发送过来的Transaction实际结果并与Sequence产生的原始Transaction预期结果进行比对判断功能是否正确。Environmentsimple_adder_env顶层的容器实例化并连接上述所有组件Agent, Scoreboard等构建出完整的验证环境。Testsimple_adder_base_test及其派生类测试场景的顶层。它负责创建Environment并配置其中的参数比如生成多少笔测试数据最后启动顶层的Sequence。这个数据流是单向且清晰的Test - Sequence - Sequencer - Driver - DUT - Monitor - Scoreboard。Scoreboard是数据流的终点也是判断测试成败的关键。3.3 关键组件源码深度解读让我们挑几个核心组件看看其代码实现的关键点。首先看Transaction它是所有数据流动的载体。// file: tb/simple_adder_transaction.sv class simple_adder_transaction extends uvm_sequence_item; rand bit [31:0] in_a; rand bit [31:0] in_b; bit [31:0] out_sum; // 实际输出由Monitor填充 bit [31:0] exp_sum; // 预期输出由Sequence计算 // UVM的字段自动化宏极大简化了copy、compare、print等方法的实现 uvm_object_utils_begin(simple_adder_transaction) uvm_field_int(in_a, UVM_ALL_ON) uvm_field_int(in_b, UVM_ALL_ON) uvm_field_int(out_sum, UVM_ALL_ON) uvm_field_int(exp_sum, UVM_ALL_ON) uvm_object_utils_end // 约束为了防止加法溢出我们简单约束操作数范围这只是demo实际约束更复杂 constraint c_operands { in_a inside {[0:100]}; in_b inside {[0:100]}; } function new(string name “simple_adder_transaction”); super.new(name); endfunction // 一个方便的函数用于计算预期结果 function void pre_randomize(); exp_sum in_a in_b; endfunction endclass接下来看Driver。Driver的核心任务是在run_phase中不断地通过seq_item_port.get_next_item(req)向Sequencer“索要”Transaction然后按照协议驱动到接口上。// file: tb/simple_adder_driver.sv class simple_adder_driver extends uvm_driver #(simple_adder_transaction); virtual simple_adder_if vif; // 虚接口指向顶层传入的实际接口 uvm_component_utils(simple_adder_driver) function new(string name, uvm_component parent); super.new(name, parent); endfunction virtual task run_phase(uvm_phase phase); forever begin simple_adder_transaction tr; // 1. 获取transaction seq_item_port.get_next_item(tr); uvm_info(get_type_name(), $sformatf(“Driving transaction: a%0d, b%0d“, tr.in_a, tr.in_b), UVM_MEDIUM) // 2. 驱动到接口使用interface中的clocking block (vif.driver_cb); vif.driver_cb.in_a tr.in_a; vif.driver_cb.in_b tr.in_b; vif.driver_cb.in_valid 1‘b1; (vif.driver_cb); vif.driver_cb.in_valid 1‘b0; // 只保持一个周期有效 // 3. 告知sequencer当前item处理完成 seq_item_port.item_done(); end endtask endclass注意事项get_next_item()和item_done()必须成对出现。item_done()的调用意味着Driver已经处理完这个TransactionSequencer可以释放它并准备下一个。忘记调用item_done()是导致Sequence卡住、仿真挂起的常见原因。最后我们看看“裁判”Scoreboard。它通常通过UVM的Analysis Port/TLMTransaction Level Modeling通信机制来接收数据。Monitor和Scoreboard之间是“发布-订阅”模式解耦了数据产生和消费。// file: tb/simple_adder_scoreboard.sv class simple_adder_scoreboard extends uvm_scoreboard; uvm_analysis_imp #(simple_adder_transaction, simple_adder_scoreboard) item_collected_export; simple_adder_transaction ref_q[$]; // 一个队列用于缓存从Sequence端发送过来的预期transaction uvm_component_utils(simple_adder_scoreboard) function new(string name, uvm_component parent); super.new(name, parent); item_collected_export new(“item_collected_export“, this); endfunction // 这是一个write函数由analysis port自动调用 virtual function void write(simple_adder_transaction tr); simple_adder_transaction ref_tr; // 1. 检查队列里是否有对应的预期transaction可以通过ID或时间戳匹配demo里简化处理 if (ref_q.size() 0) begin ref_tr ref_q.pop_front(); // 2. 比对实际输出和预期输出 if (tr.out_sum ref_tr.exp_sum) begin uvm_info(get_type_name(), $sformatf(“PASS! a%0d, b%0d, out%0d, exp%0d“, tr.in_a, tr.in_b, tr.out_sum, ref_tr.exp_sum), UVM_LOW) end else begin uvm_error(get_type_name(), $sformatf(“FAIL! a%0d, b%0d, out%0d, exp%0d“, tr.in_a, tr.in_b, tr.out_sum, ref_tr.exp_sum)) end end else begin uvm_warning(get_type_name(), “Received a transaction but no expected reference found.“) end endfunction // 一个外部函数供Sequence或其它组件将预期transaction送入队列 function void add_expected(simple_adder_transaction tr); ref_q.push_back(tr); endfunction endclass4. 一键编译与仿真全流程实操理解了各个部件现在让我们把它们组装起来并让整个系统跑起来。这一步我们会用到Makefile和仿真脚本实现真正的“一键复现”。4.1 Makefile与编译脚本解析一个设计良好的Makefile能自动化编译、仿真和清理过程。我们的Makefile主要目标有compile,sim,wave,clean。# file: Makefile PROJECT uvm_demo TOP simple_adder_test VSIM vsim VLOG vlog UVM_ARGS -L $(UVM_HOME)/lib/uvm_pkg incdir$(UVM_HOME)/src SV_ARGS -sv DEFINES defineUVM_NO_DPI COMPILE_ARGS $(UVM_ARGS) $(SV_ARGS) $(DEFINES) -f ./run/compile.f SIM_ARGS -c -do “run -all; quit -f“ UVM_TESTNAMEsimple_adder_smoke_test .PHONY: all compile sim wave clean all: compile sim compile: $(VLOG) $(COMPILE_ARGS) -l compile.log sim: $(VSIM) $(TOP) $(SIM_ARGS) -l simulation.log wave: $(VSIM) $(TOP) -do ./run/waves.do -gui clean: rm -rf work *.log *.wlf transcript vsim.wlfcompile目标调用vlog编译器使用-f选项指定一个文件列表compile.f其中按顺序列出了所有需要编译的SystemVerilog文件。顺序很重要依赖项如package、基类必须先编译。sim目标调用vsim仿真器加载顶层模块simple_adder_test。-c表示命令行模式无图形界面。-do后面跟的是仿真执行命令run -all运行直到所有测试完成quit -f强制退出。最关键的是UVM_TESTNAMEsimple_adder_smoke_test这个仿真参数告诉UVM运行哪个测试类。wave目标以图形界面模式启动仿真并加载一个波形配置文件waves.do自动添加关键信号到波形窗口。compile.f文件内容示例# UVM库文件 $(UVM_HOME)/src/uvm_pkg.sv # DUT文件 ./dut/simple_adder_if.sv ./dut/simple_adder.sv # TB文件 - 注意顺序先transaction再组件最后是顶层test和module ./tb/simple_adder_transaction.sv ./tb/simple_adder_sequence.sv ./tb/simple_adder_driver.sv ./tb/simple_adder_monitor.sv ./tb/simple_adder_agent.sv ./tb/simple_adder_scoreboard.sv ./tb/simple_adder_env.sv ./tb/simple_adder_base_test.sv ./tb/simple_adder_smoke_test.sv # 这是我们指定的测试 ./tb/uvm_tb_pkg.sv # 这个package包含了上面大部分组件 ./tb/simple_adder_test.sv # 顶层module包含interface实例化和DUT例化4.2 执行步骤与结果分析现在打开终端进入项目根目录uvm_demo/。检查环境首先确认环境变量已设置。echo $UVM_HOME which vsim这两条命令应该能正确打印出路径。编译执行make compile。终端会滚动输出编译信息。如果一切顺利最后会看到类似** Note: $finish : ./tb/simple_adder_test.sv(xx)的提示这只是因为编译了顶层模块并非错误。同时会生成一个work库目录和compile.log文件。务必检查compile.log末尾是否有Error或Critical Warning。常见的错误包括文件路径不对、语法错误、UVM宏未定义需要defineUVM_NO_DPI等。运行仿真执行make sim。这是最激动人心的时刻。仿真会在命令行中快速运行并打印出大量的UVM日志。你需要关注最后几行UVM_INFO 1050000: reporter [TEST_DONE] ‘simple_adder_smoke_test‘ Test Completed. UVM_INFO 1050000: reporter [UVM/REPORT/CATCHER] --- UVM Report Summary --- ** Report counts by severity UVM_INFO : 85 UVM_WARNING : 0 UVM_ERROR : 0 UVM_FATAL : 0 ** Report counts by id ...看到UVM_ERROR : 0和UVM_FATAL : 0并且有TEST_DONE的信息基本就意味着测试通过了日志中你会看到Sequence生成数据、Driver驱动数据、Monitor采集数据、Scoreboard比对成功的详细信息。查看波形可选如果你想直观地看到信号跳动可以执行make wave。QuestaSim图形界面会打开并自动加载预设好的波形。你可以看到clk、rst_n、in_a、in_b、in_valid、out_sum、out_valid等信号的时序关系加深对协议和数据流的理解。4.3 修改测试以触发错误一个只会报PASS的测试平台不是好平台。让我们主动注入一个错误看看Scoreboard如何工作。修改simple_adder_sequence.sv中的body任务在发送某些transaction时故意将错误的预期值填入Scoreboard的队列。// 在sequence的body任务中某处添加 tr simple_adder_transaction::type_id::create(“tr“); assert(tr.randomize()); if (num_transactions 5) begin // 假设第5笔数据注入错误 scb.add_expected(tr); // 先添加正确的预期 // 再创建一个错误的预期对象放入队列模拟预期错误 wrong_tr simple_adder_transaction::type_id::create(“wrong_tr“); wrong_tr.copy(tr); wrong_tr.exp_sum tr.exp_sum 1; // 故意将预期结果加1 scb.add_expected(wrong_tr); // 这里应该是错误的预期但为了演示我们直接覆盖队列 // 更常见的做法是在scoreboard的add_expected函数里做手脚或者修改transaction本身 end else begin scb.add_expected(tr); end start_item(tr); finish_item(tr);重新编译运行(make clean make compile make sim)你会在日志中看到对应的UVM_ERROR报告实际输出与预期不匹配。这证明了你的验证平台具备错误检测能力。5. 常见问题排查与深度调试技巧即便按照步骤操作你也可能会遇到一些问题。这里我总结了一些新手常踩的坑和排查方法。5.1 编译阶段常见错误*Error: (vlog-19) Failed to access library ‘uvm_pkg‘ at “uvm_pkg“* 这是最常见的问题意味着仿真器找不到UVM库。解决方案确认UVM_HOME环境变量设置正确且已source。在编译命令中显式指定库路径如-L $UVM_HOME/lib/uvm_pkg。检查QuestaSim版本有些版本需要额外编译UVM库。进入$UVM_HOME目录执行vlib work和vlog -work work src/uvm_pkg.sv。*Error: Cannot find ‘uvm_object_utils_begin‘ in ‘simple_adder_transaction‘* 说明UVM宏没有定义。确保在编译命令中加入了defineUVM_NO_DPI。如果使用UVM-1.2可能还需要defineUVM_CMDLINE_NO_DPI。*Error: Undefined variable: ‘vif‘ in driver* 虚接口virtual interface没有正确传递。检查你的Agent或Env中是否在connect_phase将顶层Test中获取到的实际interface指针赋值给了Driver和Monitor的vif变量。这是连接静态世界module中的interface和动态世界class中的component的关键。5.2 仿真运行时常见问题仿真挂起不结束检查Sequence确认Sequence的body()任务有正确的结束条件比如使用了repeat()循环并且最终会执行phase.drop_objection(this)。在UVM中raise_objection和drop_objection用于控制仿真运行时间。检查Driver确认Driver在调用get_next_item()后一定会调用item_done()。检查Scoreboard如果Scoreboard的TLM通信或队列处理逻辑有死循环也可能导致挂起。UVM_WARNING或UVM_ERROR过多/过少 通过UVM_VERBOSITYUVM_LOW等参数可以控制信息打印的详细程度。在调试时设为UVM_DEBUG在回归时设为UVM_ERROR或UVM_NONE。可以在命令行中指定UVM_VERBOSITYUVM_HIGH。Scoreboard没有收到数据或者比对不上TLM连接检查在Env的connect_phase中确认Monitor的analysis_port连接到了Scoreboard的analysis_export。monitor.ap.connect(scoreboard.item_collected_export)。Transaction匹配逻辑在复杂的测试中Monitor送出的transaction和Sequence产生的transaction可能需要通过唯一ID或时间戳来匹配。Demo中的简单队列弹出方式在并发场景下会出错。这是你需要根据实际测试场景完善的地方。时序问题Monitor采集数据的时机可能不对。确保在out_valid有效后的下一个时钟沿采集out_sum并考虑设计潜在的输出延迟。5.3 波形调试与日志分析使用UVM的uvm_info进行调试在关键位置如Sequence生成item、Driver驱动、Monitor采集、Scoreboard比对添加不同UVM_MEDIUM或UVM_HIGH级别的uvm_info通过改变verbosity来选择性查看。使用UVM_CONFIG_DB_TRACE在运行时添加这个参数可以打印出所有通过uvm_config_db进行配置操作的信息对于调试配置问题非常有用。波形调试当逻辑复杂时波形是最直观的。在Test中可以使用uvm_config_db设置虚接口并在Driver/Monitor中获取它来驱动和采样。在仿真时使用make wave查看。重点关注控制信号如valid和数据信号如in_a,in_b,out_sum的时序关系以及Transaction在组件间传递的ID。6. 从Demo到实战下一步学习路径建议成功运行这个demo只是你UVM之旅的第一步。它帮你建立了最基础的感性认识。接下来你应该像搭积木一样逐步丰富和复杂化这个平台并深入学习背后的原理。丰富Transaction和Sequence在Transaction中添加更多约束constraints比如让in_a和in_b互为质数或者让它们的和落在某个特定区间。学习编写更复杂的Sequence分层Sequencevirtual sequence、有依赖关系的Sequence、从文件读取激励的Sequence。完善验证平台组件功能覆盖率Coverage在Monitor或单独的Coverage Collector中添加覆盖点coverpoint和交叉覆盖cross收集in_a、in_b、out_sum的取值分布以及in_valid和out_valid的时序关系。这是衡量验证完备性的关键。寄存器模型Register Model这是UVM中相对复杂但极其重要的部分。尝试为你的加法器虚拟出一组控制寄存器比如使能位、清零位并搭建UVM寄存器模型。学习前门访问front-door和后门访问back-door以及寄存器镜像值mirror value和预测predict的概念。更复杂的Scoreboard实现基于参考模型Reference Model的比对。即用一个用SystemVerilog或C写的算法模型模拟DUT的理想行为Scoreboard比较DUT输出和参考模型输出。学习UVM机制Phase机制理解build_phase、connect_phase、run_phase、main_phase等的执行顺序和用途。Configuration机制熟练使用uvm_config_db来动态配置验证环境比如在Test中设置Agent是否为active模式或者传递虚接口。Factory机制理解type_id::create和工厂重载override这是实现可重用性和灵活性的核心。搭建自动化回归测试编写多个不同的Test如随机测试、边界测试、错误注入测试并用一个脚本Shell或Python自动编译、运行所有测试并收集覆盖率报告和错误日志。这个demo的源码就是你探索上述所有知识的起点。我建议你不要复制粘贴而是亲手一个字一个字敲出来。在敲代码的过程中你会遇到各种编译错误去解决它们这才是最有效的学习。当你把这个demo彻底吃透并能根据自己的想法随意修改和扩展后你就算真正推开了UVM世界的大门了。
返回列表