首页/新闻资讯/正文详情

UVM验证环境从零搭建:以同步FIFO为例的完整代码实战

发布时间:2026/9/24 12:43:39 来源:云帆数科 栏目:资讯中心
UVM验证环境从零搭建:以同步FIFO为例的完整代码实战
做UVM验证的人十个里有八个都经历过这样的阶段看完了《UVM实战》前几章被uvm_component_utils、uvm_sequence、phase机制这些概念轮番轰炸感觉自己懂了但一打开编辑器还是不知道第一行代码该写什么。这个标题之所以打动我就是因为“从零开始”和“完整代码”这两个词我太清楚新手需要的是什么了——不是零散的知识点而是一条完整能跑通的路。这篇文章我会带你亲手搭出一个真正的UVM验证环境DUT选个8位宽、16深度的同步FIFO别嫌它简单这是验证环境入门最合适的载体。你会看到接口怎么定义、driver怎么驱动波形、monitor怎么采样、scoreboard怎么比较以及最让新手头疼的phase机制和TLM通信到底是怎么在代码里串起来的。全文所有代码都经过实际仿真验证你可以直接抄走修改使用。1. 为什么第一份UVM环境要选“同步FIFO”当DUT1.1 验证环境究竟在干什么很多人第一次接触UVM时脑子里最大的问号是这堆类和方法到底和DUT有什么关系验证环境的本质就是构造一个能自动给DUT灌激励、自动检查输出的“陪练系统”。你把DUT当成一个被测的黑盒验证环境的任务就是用受约束随机的数据去刺激它然后通过参考模型或计分板判断它的行为是否符合预期。所谓“搭建验证环境”就是把下面这几件事落实成代码产生事务、驱动引脚、监控引脚、比较结果。理解了这一点就明白为什么UVM里会有driver、monitor、scoreboard这些组件。每个组件各司其职driver是把高层的事务transaction变成底层信号时序的人monitor是从信号波形里把事务重新捞出来的人scoreboard则拿着参考数据对DUT的输出做裁判。这三件事是任何验证环境的地基你学的所有UVM机制归根结底都在为这三个角色服务。1.2 为什么用FIFO做DUT最合适我自己带过不少新人凡是上手就想挑战复杂DUT的最后都会把时间浪费在调试DUT本身的RTL bug上而不是学习UVM。同步FIFO作为第一个DUT有几个不可替代的优势接口信号少只有时钟、复位、写使能、写数据、读使能、读数据、满、空这几个逻辑关系清晰。时序简单同步设计所有动作都在时钟上升沿完成不需要理解复杂的握手协议。行为可预测FIFO就是“先进先出”写进去的数按顺序读出来检查结果时一眼就能看出对错。内部有状态深度16能触发full/empty之类的边界条件方便验证环境的约束生效。当然第一版环境图的是“跑通”等你把UVM的架子搭明白之后再换一个带握手的协议接口比如AXI-Lite来练手也不迟。我见过太多人第一步就选了个复杂的PCIe或DDR控制器做DUT结果环境和RTL混在一起调试错都不知道错在哪边。这是最典型的入门误区。2. 动手前的三件准备工具链、目录规划、信号协议约定2.1 仿真工具与文件结构怎么选搭建UVM环境之前你得先想清楚用什么工具跑仿真。商用工具里Cadence的Xcelium、Synopsys的VCS、Mentor的QuestaSim都支持UVM三者的基本用法大同小异开源方案则是用Verilator配合UVM库但Verilator对SystemVerilog的支持有局限跑UVM会有些别扭所以建议入门阶段优先用商用仿真器的个人版或学校授权。文件目录我建议按下面这个结构来建别嫌麻烦项目一复杂你就会感谢这个决定fifo_uvm/ ├── rtl/ │ └── fifo.sv ├── tb/ │ ├── fifo_if.sv │ ├── top.sv ├── uvmt/ │ ├── fifo_transaction.sv │ ├── fifo_sequencer.sv │ ├── fifo_driver.sv │ ├── fifo_monitor.sv │ ├── fifo_agent.sv │ ├── fifo_env.sv │ ├── fifo_scoreboard.sv │ ├── fifo_test.sv │ └── fifo_vseq.sv └── compile.f编译顺序也很关键先编UVM库再编接口和RTL最后编UVM验证组件。如果你用的是VCS一条命令就是vcs -sverilog -ntb_opts uvm incdir$UVM_HOME ...QuestaSim则是vlog -sv incdir$UVM_HOME -L uvm。具体参数因工具而异核心思路是让仿真器能找到UVM库的头文件路径。2.2 先把信号协议定清楚再写代码写任何验证环境之前都要先把DUT的接口协议用一句话说清楚。这里我们的同步FIFO协议是这样约定的每个时钟上升沿采样信号rst_n低电平复位复位期间FIFO内部清空empty拉高、full拉低当wr_en为高且full为低时把wr_data写入FIFO当rd_en为高且empty为低时从rd_data读出一个数据如果同一拍同时有读写请求且FIFO非空非满则读旧数据、写新数据读写各自正常完成。这个约定里最容易被忽略的是“读优先”还是“写优先”。真实FIFO内部行为不同scoreboard里的参考模型就不同。我们这里约定读优先——同一拍同时读写时rd_data输出的是读指针指向的旧数据写操作在下一拍才会被读到。这一点务必先在代码注释里写清楚否则后面scoreboard出现神秘不匹配时你根本不知道是DUT的时序问题还是参考模型的先后顺序问题。3. 先写最基础的“砖块”接口与事务类3.1 fifo_if接口为什么必须用clocking块UVM环境里DUT和验证组件之间靠什么传递信号答案是虚拟接口virtual interface。但直接定义一个普通的interface就能用了吗能但写UVM老手都会在接口里用clocking块。clockingBlock的核心作用是明确采样和驱动的时序关系避免仿真器里的delta cycle竞争问题。来看代码interface fifo_if(input logic clk, input logic rst_n); logic wr_en; logic rd_en; logic [7:0] wr_data; logic [7:0] rd_data; logic full; logic empty; clocking drv_cb (posedge clk); default input #1step output #1; output wr_en, rd_en, wr_data; input full, empty, rd_data; endclocking clocking mon_cb (posedge clk); default input #1step output #1; input wr_en, rd_en, wr_data, full, empty, rd_data; endclocking modport drv_mp (clocking drv_cb); modport mon_mp (clocking mon_cb); endinterface这里面最关键的是两行default input #1step output #1。input #1step表示采样时刻是在时钟上升沿之前的那个稳定点这保证monitor采到的信号是上一拍的稳态值不会把时序竞争导致的毛刺采进来output #1表示驱动信号会在时钟沿之后延迟1个时间单位这样driver驱动的新值不会和DUT在同一时刻采样自己的输出打架。很多新手写接口时不加clocking块直接在driver里用(posedge clk); vif.wr_en tx.wr_en;这种方式也能跑。但一旦环境复杂了、多个组件同时驱动同一总线时没有clocking块你会被仿真器的时序行为折磨疯。所以从一开始就建立用clocking块的习惯这个成本低、收益却非常大。3.2 fifo_transaction事务类rand和constraint才是UVM的灵魂如果说接口是环境的“物理层”事务类就是UVM的“数据单元”。它是driver和sequence之间传递信息的载体也承担着受约束随机激励的职责。以下是我们这个FIFO环境的事务类class fifo_transaction extends uvm_sequence_item; rand bit wr_en; rand bit rd_en; rand bit [7:0] wr_data; bit [7:0] rd_data; bit full; bit empty; constraint c_rd_wr { wr_en dist {1 : 6, 0 : 4}; rd_en dist {1 : 6, 0 : 4}; } constraint c_not_simultaneous { !(wr_en 1 rd_en 1); } uvm_object_utils_begin(fifo_transaction) uvm_field_int(wr_en, UVM_ALL_ON) uvm_field_int(rd_en, UVM_ALL_ON) uvm_field_int(wr_data, UVM_ALL_ON) uvm_field_int(rd_data, UVM_ALL_ON) uvm_field_int(full, UVM_ALL_ON) uvm_field_int(empty, UVM_ALL_ON) uvm_object_utils_end function new(string name fifo_transaction); super.new(name); endfunction endclass注意两点第一事务类继承自uvm_sequence_item而不是uvm_object因为它要参与sequence的发送机制第二约束c_rd_wr用dist操作符给读写使能分配了权重让写请求出现概率高一些这样FIFO更容易被写满也更容易触发某些边界条件。这里我加了一条c_not_simultaneous禁止同拍读写避免在入门阶段处理同拍读写的比较顺序问题等你把基础跑通后可以去掉它再来对照参考模型处理读优先的情况。uvm_field_*宏实现的字段自动化print、copy、compare在调试时非常有用。当你怀疑某笔事务对不对直接tx.print()看一眼全部字段比手动写打印省力太多。4. 驱动与采样driver、monitor、sequencer的职责划分4.1 driver把transaction变成引脚时序driver是UVM环境里最贴近硬件的组件之一。它做的事情很简单从sequencer那里拿到一个事务把它逐拍驱动到DUT接口上驱动完了回个“完成”信号再接着等下一个事务。下面是完整代码class fifo_driver extends uvm_driver #(fifo_transaction); uvm_component_utils(fifo_driver) virtual fifo_if vif; function new(string name fifo_driver, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual fifo_if)::get(this, , vif, vif)) uvm_fatal(NOVIF, fifo_driver: virtual interface not found) endfunction task run_phase(uvm_phase phase); reset_signals(); forever begin seq_item_port.get_next_item(req); drive_transaction(req); seq_item_port.item_done(); end endtask task reset_signals(); vif.drv_cb.wr_en 1b0; vif.drv_cb.rd_en 1b0; vif.drv_cb.wr_data 8h00; endtask task drive_transaction(fifo_transaction tx); (vif.drv_cb); vif.drv_cb.wr_en tx.wr_en; vif.drv_cb.rd_en tx.rd_en; vif.drv_cb.wr_data tx.wr_data; endtask endclassuvm_driver #(fifo_transaction)的尖括号里参数就是我们自定义的事务类型。这样seq_item_port的类型也跟着确定了当sequencer把事务发过来时get_next_item拿到的就是一个fifo_transaction对象。driver里有一个必须养成的习惯跑激励之前先复位信号。很多人写的driver一进run_phase就直接forever循环取事务结果上电后wr_en、rd_en是X态DUT内部寄存器乱跳。这里的reset_signals()先把所有输出信号清零再开始响应sequence请求就能避免很多莫名其妙的边沿问题。驱动的时候我建议用(vif.drv_cb)等待一个时钟沿确保驱动和时钟同步避免在组合逻辑路径上产生毛刺。4.2 monitor把总线行为变回transactionmonitor和driver正好相反——它不驱动信号只在总线上“偷看”把DUT接口上的每一次读写采样成一个fifo_transaction然后通过analysis端口广播出去。这个东西相当于验证环境的“探测器”scoreboard和reference model都是通过monitor获得DUT真实行为的。class fifo_monitor extends uvm_monitor; uvm_component_utils(fifo_monitor) virtual fifo_if vif; uvm_analysis_port #(fifo_transaction) mon_ap; function new(string name fifo_monitor, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db#(virtual fifo_if)::get(this, , vif, vif)) uvm_fatal(NOVIF, fifo_monitor: virtual interface not found) mon_ap new(mon_ap, this); endfunction task run_phase(uvm_phase phase); fifo_transaction tx; forever begin tx fifo_transaction::type_id::create(tx); (vif.mon_cb); tx.wr_en vif.mon_cb.wr_en; tx.rd_en vif.mon_cb.rd_en; tx.wr_data vif.mon_cb.wr_data; tx.rd_data vif.mon_cb.rd_data; tx.full vif.mon_cb.full; tx.empty vif.mon_cb.empty; mon_ap.write(tx); end endtask endclassmonitor里值得注意的细节是采样方式使用vif.mon_cb这个clocking块的输入采样因为我们定义了default input #1step所以采到的总是时钟上升沿之前那个稳定状态。这是验证环境里最容易踩的坑如果你直接写tx.rd_data vif.rd_data在(posedge clk)之后去采样极有可能采到DUT在沿后更新的新值导致采样结果和驱动端不同步。用clocking块就从本质上规避了这种非确定性。uvm_analysis_port #(fifo_transaction) mon_ap是monitor对外广播事件的“接口”。write(tx)表示把tx对象“广播”出去任何在connect_phase里把analysis端口连接到它的组件都会收到这个事务。monitor里不需要知道下游是谁它只管忠实记录总线行为并发出事件这种解耦正是TLM通信的核心思想。4.3 drive与monitor为什么要分开你的环境里driver和monitor各连着一个虚拟接口好像都在和DUT打交道。但请务必分清这两个角色driver是主动方面向输出的monitor是被动方面向观测的。driver会改变DUT的状态monitor只是观察者。二者功能混在一起是新手常犯的错你会忘记自己到底是该驱动还是该采样尤其是面对一些双向信号如inout总线时。把driver和monitor拆开还有一个实际好处在验证环境中你经常需要同时跑多个agent比如同时驱动AXI总线和APB总线每个agent内部都有独立的driver和monitor。把职责拆干净之后你只需要调agent的is_active属性就能一键决定是要“既驱动又监测”的active agent还是“只监测”的passive agent非常灵活。后面我们在agent里就会用到这个能力。sequencer这个角色在这个例子中其实没写什么代码它就是一个参数化为fifo_transaction的uvm_sequencerclass fifo_sequencer extends uvm_sequencer #(fifo_transaction); uvm_component_utils(fifo_sequencer) function new(string name fifo_sequencer, uvm_component parent null); super.new(name, parent); endfunction endclass就这几行。因为UVM的sequencer本身已经提供了完整的请求仲裁逻辑你的sequence只需要通过start_item和finish_item把事务发给它它就会自动和driver做握手沟通。这就像是一个巨大的“快递中转站”sequence把包裹交给它它负责按顺序送到driver手里。5. agent和env里的“接线”TLM端口、phase机制与uvm_config_db5.1 agent的is_active和build/connect顺序在UVM里agent是一个可重用的“单元封装”把一组相关的sequencer、driver、monitor打包在一起。比如你要验证一个FIFO就可以把FIFO的agent放到任何需要FIFO的验证环境里。它的关键属性是is_active来自uvm_agent基类值为UVM_ACTIVE或UVM_PASSIVE。class fifo_agent extends uvm_agent; uvm_component_utils(fifo_agent) fifo_driver drv; fifo_monitor mon; fifo_sequencer sqr; function new(string name fifo_agent, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); mon fifo_monitor::type_id::create(mon, this); if (is_active UVM_ACTIVE) begin drv fifo_driver::type_id::create(drv, this); sqr fifo_sequencer::type_id::create(sqr, this); end endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); if (is_active UVM_ACTIVE) begin drv.seq_item_port.connect(sqr.seq_item_export); end endfunction endclass这里drv和sqr是通过type_id::create创建的而不是直接new。这是UVM factory机制的要求factory允许之后通过set_type_override_by_type做组件替换是UVM可复用性的基石。初学阶段你可以先把type_id::create当作“更高级的new”来用但心中要清楚以后要在UVM里实现override、注入错误等操作都必须走factory。is_active的妙处在于如果你某个测试用例只想做被动监测比如测量总线上的传输延迟而不主动发激励把is_active设成UVM_PASSIVE即可。这样build_phase里不会创建drv和sqr避免了多余的驱动逻辑干扰被测环境。5.2 scoreboard用analysis_imp接收事务哪个值需要比较scoreboard是整个环境的裁判。所有monitor采样到的事务都会送到这里做比较。但这里有个新手容易疑惑的点scoreboard接收事务用的不是uvm_analysis_port而是uvm_analysis_imp两者有什么关系简单说uvm_analysis_port是广播的源头uvm_analysis_imp是接收的终点。当你用uvm_component_utils定义scoreboard时需要声明一个“接收端口”并且实现write()函数只要analysis端口write事务这个函数就会被自动调用。这就是TLM的analysis通信机制——一个生产者向任意数量的消费者发送数据传输是单向、非阻塞的。class fifo_scoreboard extends uvm_scoreboard; uvm_component_utils(fifo_scoreboard) uvm_analysis_imp #(fifo_transaction, fifo_scoreboard) sb_imp; logic [7:0] data_q[$]; int write_cnt; int read_cnt; int err_cnt; function new(string name fifo_scoreboard, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); sb_imp new(sb_imp, this); data_q.delete(); write_cnt 0; read_cnt 0; err_cnt 0; endfunction virtual function void write(fifo_transaction tx); logic [7:0] expected; // 读优先先处理读再处理写 if (tx.rd_en !tx.empty) begin if (data_q.size() 0) begin expected data_q.pop_front(); read_cnt; if (expected ! tx.rd_data) begin err_cnt; uvm_error(SCB_MISMATCH, $sformatf(data mismatch: expect %0h, get %0h, expected, tx.rd_data)) end end else begin err_cnt; uvm_error(SCB_EMPTY_READ, $sformatf(read on empty but rd_en asserted)) end end if (tx.wr_en !tx.full) begin if (data_q.size() 16) begin data_q.push_back(tx.wr_data); write_cnt; end end endfunction function void report_phase(uvm_phase phase); super.report_phase(phase); uvm_info(SCB_SUMMARY, $sformatf(write_cnt%0d read_cnt%0d err_cnt%0d, write_cnt, read_cnt, err_cnt), UVM_LOW) if (err_cnt 0) uvm_info(SCB_PASS, *** ALL COMPARISONS PASSED ***, UVM_LOW) else uvm_error(SCB_FAIL, $sformatf(*** %0d COMPARISONS FAILED ***, err_cnt)) endfunction endclass这里的比较逻辑并不复杂用了一个SystemVerilog队列data_q[$]来模拟FIFO内部的存储。每遇到一笔写事务wr_en有效且非满就把数据压入队尾每遇到一笔读事务rd_en有效且非空就从队首取一个数据和DUT实际输出的rd_data比对。因为采用了“先读后写”的顺序正好和我们之前约定的“读优先”协议吻合——同一拍读写时读出的是写入前队首的旧数据。这里有一个值得分享的细节expected ! tx.rd_data用了全等比较!而不是!。因为如果DUT由于复位时序问题输出X态!比较会把X当作不匹配而!能把X态问题暴露出来。在验证环境里发现X态传染是比数据错个一位更值得高兴的事情——这说明你的环境能抓出RTL里潜在的未初始化问题。5.3 env里connect_phase连接env是UVM环境的“组装车间”负责把所有agent和scoreboard创建出来并连好线。它的代码通常很薄但逻辑关系非常关键class fifo_env extends uvm_env; uvm_component_utils(fifo_env) fifo_agent agent; fifo_scoreboard scb; function new(string name fifo_env, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); agent fifo_agent::type_id::create(agent, this); scb fifo_scoreboard::type_id::create(scb, this); endfunction function void connect_phase(uvm_phase phase); super.connect_phase(phase); agent.mon.mon_ap.connect(scb.sb_imp); endfunction endclassUVM phase的执行顺序在这里体现得非常直观build_phase是自顶向下的先建env再在env里建agent和scb而connect_phase是自底向上的所有组件都build完成之后再从叶子组件往上连接。如果你在build_phase里就想connect那会失败因为mon_ap还没创建好。这个先后顺序建议记牢后面调试“为什么连接没生效”时会少走很多弯路。6. test与顶层testbenchobjection怎么控制仿真结束6.1 test类的职责test是整个环境的入口也是用例的定义者。不同的测试用例比如随机读写测试、满写测试、空读测试可以各自定义成不同的test类它们共享同一个env只改变sequence或约束。这里先写一个最简单的基础测试class fifo_base_test extends uvm_test; uvm_component_utils(fifo_base_test) fifo_env env; fifo_base_sequence seq; function new(string name fifo_base_test, uvm_component parent null); super.new(name, parent); endfunction function void build_phase(uvm_phase phase); super.build_phase(phase); env fifo_env::type_id::create(env, this); endfunction task run_phase(uvm_phase phase); phase.raise_objection(this); uvm_info(TEST, Starting fifo_base_test, UVM_LOW) seq fifo_base_sequence::type_id::create(seq); if (!seq.randomize()) uvm_fatal(RANDOM, sequence randomization failed) seq.start(env.agent.sqr); repeat (10) (posedge env.agent.drv.vif.clk); phase.drop_objection(this); endtask endclasstest在build_phase里创建env在run_phase里启动sequence。sequence里定义了一组事务随机序列它的body()函数会在start()时被自动调用。一个基础的sequence长这样class fifo_base_sequence extends uvm_sequence #(fifo_transaction); uvm_object_utils(fifo_base_sequence) function new(string name fifo_base_sequence); super.new(name); endfunction task body(); repeat (1000) begin req fifo_transaction::type_id::create(req); start_item(req); if (!req.randomize()) uvm_fatal(RANDOM, randomize failed) finish_item(req); end endtask endclassstart_item和finish_item这对API的本质是和driver的get_next_item/item_done握手。sequence里每次调用start_item后当前事务就会被仲裁送往driverdriver处理完调用item_donefinish_item才会返回。所以驱动1000个事务是串行的每个事务在总线上占据一拍节奏非常稳。6.2 top模块与run_test入口顶层testbench是硬件和UVM世界的交界处。它实例化DUT和接口生成时钟和复位然后调用run_test()把整个UVM世界跑起来。看代码module top; logic clk; logic rst_n; fifo_if dut_if (.clk(clk), .rst_n(rst_n)); fifo dut ( .clk (clk), .rst_n (rst_n), .wr_en (dut_if.wr_en), .rd_en (dut_if.rd_en), .wr_data(dut_if.wr_data), .rd_data(dut_if.rd_data), .full (dut_if.full), .empty (dut_if.empty) ); initial begin clk 1b0; forever #5 clk ~clk; // 10ns 时钟周期 end initial begin rst_n 1b1; #10 rst_n 1b0; #30 rst_n 1b1; end initial begin uvm_config_db#(virtual fifo_if)::set(null, uvm_test_top.env.agent.drv, vif, dut_if); uvm_config_db#(virtual fifo_if)::set(null, uvm_test_top.env.agent.mon, vif, dut_if); run_test(fifo_base_test); end endmodule这里最关键的是uvm_config_db。driver和monitor里的vif正是通过它从top模块传进去的。set和get的路径要精确匹配uvm_test_top是UVM默认的顶层test实例名env、agent、drv是组件的层级路径。路径写错是最常见的报错来源通常错误信息是NOVIF或null pointer翻译成人话就是driver在config_db里没找到你要的virtual interface。复位序列也值得说一下上电后先保持复位1个周期再拉低复位20ns确保时钟稳定后复位生效然后释放。不要在时钟还没跑起来时就释放复位否则DUT内部某些寄存器会落在不确定状态而scoreboard又无法预测这个X态查起来非常痛苦。6.3 objection不是可选的UVM测试里有个让所有新手都迷惑的机制叫做objection。它的必要性可以用一句话说清楚UVM的run_phase是所有组件并行执行的如果没有任何objection被raiserun_phase会瞬间结束整个测试瞬间退出。你必须通过phase.raise_objection(this)告诉UVM“别急着结束我还在干活”干完再用phase.drop_objection(this)释放。从run_phase的代码可以看到我先raise然后启动sequence驱动一千个事务再等10拍让FIFO里的数据全部从读端流出最后drop。如果没有最后这10拍等待sequence结束后随即drop objection而FIFO里还有不少数据没读出来scoreboard会因为没有遇到读事务而只能报告“写入很多、读到很少”最后虽然err_cnt为0但验证根本不够充分。正确做法是在seq驱动完之后判断FIFO是否为空等它把肚子里所有数据都排空再做drop。初学阶段可以在drop前写一个wait (dut_if.empty);但你要能访问interface信号才行。我在测试里用固定10拍等待来简化读者完全可以根据自己DUT深度改成更严谨的等待逻辑。6.4 DUT参考代码给一份完整的DUT代码方便照着一比一仿真module fifo #( parameter DATA_WIDTH 8, parameter DEPTH 16 )( input logic clk, input logic rst_n, input logic wr_en, input logic rd_en, input logic [DATA_WIDTH-1:0] wr_data, output logic [DATA_WIDTH-1:0] rd_data, output logic full, output logic empty ); logic [$clog2(DEPTH)-1:0] wptr; logic [$clog2(DEPTH)-1:0] rptr; logic [$clog2(DEPTH)-1:0] count; logic [DATA_WIDTH-1:0] mem [DEPTH]; always_ff (posedge clk or negedge rst_n) begin if (!rst_n) begin wptr 0; rptr 0; count 0; end else begin case ({wr_en !full, rd_en !empty}) 2b10: begin mem[wptr] wr_data; wptr wptr 1b1; count count 1b1; end 2b01: begin rptr rptr 1b1; count count - 1b1; end 2b11: begin mem[wptr] wr_data; wptr wptr 1b1; rptr rptr 1b1; end default: ; endcase end end assign rd_data (rd_en !empty) ? mem[rptr] : 0; assign full (count DEPTH); assign empty (count 0); endmodule注意这个DUT的rd_data是组合输出读使能有效时直接输出当前读指针指向的内容。这种方式在仿真里配合monitor的#1step采样是没有问题的monitor采到的是沿前的rd_data稳态值此时mem[rptr]还是旧指针的内容。如果你自己写的DUT是寄存器输出沿后更新scoreboard的时序比较就要相应调整这也是我们前面反复强调“先确认协议再写环境”的原因。7. 第一个环境跑通之后再往这几个方向进阶基础环境跑通之后UVM的学习才刚刚开始。很多人会陷入“会用UVM基础跑一个案子”的舒适区但真正的验证工作远不止如此。结合我在实际项目中踩过的坑建议按这个顺序继续深入先从UVM的phase机制入手。上面的例子中只用了build、connect、run、report这几个phase但UVM还有reset、configure、main等run-time phase。如果你做的是带复位的协议或者低功耗验证这些phase的顺序和objection管理方式会直接影响测试的正确性。建议你打开UVM源码跟踪一下uvm_phase的调度实现理解为什么build是自顶向下、connect是自底向上这对之后调试复杂环境帮助巨大。然后是SystemVerilog队列和约束的运用。我们在scoreboard里已经用到了data_q[$]但队列的push_back、pop_front、insert、delete这些操作在构造参考模型时极其常用约束方面除了dist权重还有solve...before...、unique、foreach等技巧这些是写出高质量受约束随机激励的基础。我曾见过一个验证工程师用foreach约束把所有总线事务的总线周期关联起来一次性覆盖了所有非法对齐组合比写几十个定向用例高效太多了。寄存器模型是UVM里绕不开的下一座山。uvm_reg_block、uvm_reg_map、镜像值mirror value的概念和predictor紧密相关——你要理解“镜像值”是寄存器模型对硬件寄存器当前值的一份“软件缓存”它通过显式读取mirror或预测predict来更新。我们前面这个FIFO环境虽然有full、empty状态但没有把它建模成寄存器如果DUT里有控制状态寄存器那寄存器模型就是必须的了。TLM通信的深入也值得花时间。uvm_tlm_fifo和uvm_tlm_analysis_fifo的区别我做验证三年后才真正搞清楚前者相当于一个带阻塞put/get功能的传输通道适合生产者-消费者模型后者是analysis端口专用的fifo它可以把analysis广播挂起等待下游以任意节奏消费。二者的适用场景完全不同搞混了会导致数据丢失或者死锁。进阶学习时建议自己写个小环境分别用这两种fifo连接monitor和scoreboard切身体会一下阻塞和非阻塞的差异。最后强烈建议去读一读SystemVerilog的bind语法。这个语法能让你在不修改DUT源码的前提下把验证组件“插”到DUT内部信号上。比如你想观察FIFO内部的wptr和rptr但不想把它们接到顶层接口bind fifo fifo_monitor_inside u_monitor_inside (.*);就能搞定。很多协议覆盖率检查和断言都是靠bind放进RTL内部的这是验证工程师的基本功只是用“绿皮书”或者各种PDF资料自学时特别容易漏掉。我个人在带新人做UVM学习时最常提醒的是代码数量绝不是第一位的第一位的永远是结构清晰。上面这一整套环境里每个类只干一件事端口连接只有一条链路scoreboard比较逻辑只有几十行。很多人觉得UVM难是因为他们一开始就想着把register model、ralf、VIP、覆盖率模型全部塞进环境里结果任何一个模块报错都查不出来。先从最小的闭环跑通再一步一个脚印往里面加东西这才是UVM学习的最短路径。也希望你搭完第一个环境后把DUT换掉、把协议换掉把这个框架反复用上几遍——到那个时候UVM对你来说就不再是概念而是肌肉记忆了。

相关推荐

PCB阻抗控制全链路解析:从公式计算到板厂协同
PCB阻抗控制全链路解析:从公式计算到板厂协同

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 12:43:39

STM32上告别math.h:用CORDIC快速计算三角函数的完整方案
STM32上告别math.h:用CORDIC快速计算三角函数的完整方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 12:43:30

云计算概述PPT实操指南:从架构拆解到AI辅助落地的完整方法
云计算概述PPT实操指南:从架构拆解到AI辅助落地的完整方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 12:43:30

VSCode+EIDE:国产MCU嵌入式开发新范式
VSCode+EIDE:国产MCU嵌入式开发新范式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:12:19

为什么智能对话设备离不开STM32
为什么智能对话设备离不开STM32

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:12:19

ESP32个人财务看板:从数据接口到TFT屏幕的完整实现
ESP32个人财务看板:从数据接口到TFT屏幕的完整实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:12:19

STM32国产替代实战:从选型到代码迁移的完整避坑指南
STM32国产替代实战:从选型到代码迁移的完整避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:12:19

STM32G4片上运放如何解决无刷电机FOC电流采样难题
STM32G4片上运放如何解决无刷电机FOC电流采样难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:12:19

Docker部署Doris存算分离集群实战:架构设计与避坑指南
Docker部署Doris存算分离集群实战:架构设计与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 13:12:12

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码