UVM 高阶用法

第 6 章 · 共 6 章

综合实战:搭建高阶验证环境

把五章积累下来的类整理进一个 package,加一个从来不清中断的第二场景——证明 COUNT 越过阈值之后还能继续计数,同时 irq 保持电平、monitor 不会重复上报,再用 uvm 第 10 章那套 factory override 的手法,不碰 run_phase,就能在命令行里切换它和第 5 章的场景。

regfile_env的每一块拼图都已经就位——axi_agentirq_agent(第 2、3 章)、协调它们的 virtual sequencer(第 4 章)、寄存器模型(第 5 章)。和 uvm 第 10 章一样,这一章更接近纯粹的组装,而不是引入新概念:把一切整理进一个 package,加一个真正从不同角度考验这套环境的第二场景,然后不改动任何已经跑通的东西,就能在命令行里切换这两个场景。

把一切整理进一个 package

跟 SV 第 16 章定下、uvm 第 10 章沿用的是同一个原则:一个类一个文件,用 `include 汇进一个 package,top module 只 import 一次。

`include "uvm_macros.svh"
 
package regfile_uvm_pkg;
  import uvm_pkg::*;
 
  `include "axil_regfile.sv"
  `include "axi4lite_if.sv"
  `include "irq_if.sv"
 
  `include "axi_txn.sv"
  `include "axi_driver.sv"
  `include "axi_monitor.sv"
  `include "axi_sequencer.sv"
  `include "axi_agent.sv"
  `include "axi_basic_seq.sv"
 
  `include "irq_txn.sv"
  `include "irq_monitor.sv"
  `include "irq_agent.sv"
  `include "irq_watcher.sv"
 
  `include "ctrl_reg.sv"
  `include "status_reg.sv"
  `include "data_reg.sv"
  `include "count_reg.sv"
  `include "regfile_reg_block.sv"
  `include "axi_reg_adapter.sv"
 
  `include "regfile_vsqr.sv"
  `include "regfile_env.sv"
  `include "regfile_virtual_seq.sv"
  `include "regfile_no_clear_seq.sv"
 
  `include "axi_smoke_test.sv"
  `include "regfile_no_clear_test.sv"
endpackage

二十二个文件,横跨五章——而且每一个要么从第一次出现到现在原封未动,要么正好只多了后面某一章明确说过的那一处添加(irq_monitor 在第 4 章多了一行,regfile_vsqr/regfile_env 每一章各自多一个字段)。没有哪一处需要回头重写前面某一章的设计。

第二个场景:从来不清中断

第 5 章的场景每次确认中断拉高之后,紧接着就清掉它。这一章的新 sequence 故意反着来——反复写过阈值,从不清除,检查寄存器模型和中断在持续拉高的状态下表现是不是完全正确:

class regfile_no_clear_seq extends regfile_virtual_seq;
  `uvm_object_utils(regfile_no_clear_seq)
 
  function new(string name = "regfile_no_clear_seq");
    super.new(name);
  endfunction
 
  task body();
    uvm_event          irq_event = uvm_event_pool::get_global("irq_event");
    irq_txn             ev_txn;
    uvm_status_e        status;
    uvm_reg_data_t       rdata;
    regfile_reg_block   regmodel = p_sequencer.regmodel;
 
    write(8'h00, 32'h1);   // CTRL: ENABLE=1
    write(8'h08, 32'hAA);  // #1
    write(8'h08, 32'hBB);  // #2
    write(8'h08, 32'hCC);  // #3
 
    fork
      write(8'h08, 32'hDD);   // #4 -> 越过 IRQ_THRESHOLD
      irq_event.wait_trigger();
    join
 
    if ($cast(ev_txn, irq_event.get_trigger_data()) && ev_txn.level == 1'b1)
      `uvm_info("VSEQ", "confirmed: irq asserted at write #4", UVM_LOW)
    else
      `uvm_error("VSEQ", "expected irq to assert at write #4")
 
    // 再过阈值写两次,全程没有任何 IRQ_CLR——irq 应该全程保持拉高,
    // 而且既然 irq_monitor 只在信号发生*变化*时才上报(第 3 章),
    // 这两次写都不应该再产生第二次 ASSERTED 事件。
    write(8'h08, 32'hEE);  // #5
    write(8'h08, 32'hFF);  // #6
 
    regmodel.COUNT.read(status, rdata, UVM_FRONTDOOR, regmodel.default_map, this);
    if (rdata == 32'd6)
      `uvm_info("VSEQ", $sformatf("confirmed: COUNT kept incrementing past the threshold to %0d, while irq stayed asserted (and silent) the whole time", rdata), UVM_LOW)
    else
      `uvm_error("VSEQ", $sformatf("expected COUNT=6 after 6 enabled DATA writes with no clear, got %0d", rdata))
  endtask
endclass

extends regfile_virtual_seq,而不是直接继承 uvm_sequence——原因和 uvm 第 10 章 my_sequence_exhaustive extends my_sequence 一样:继承下来的 write() 辅助任务和 `uvm_declare_p_sequencer 原样复用,只有 body() 变了。拿这个场景跑一遍真实的 DUT,能精确证实两件事:COUNT 确实到了 6,而且整个 sequence 期间只触发了一次 irq 事件——第 5、6 次写发生的时候 irq 已经拉高了,所以 irq_monitorvif.cb.irq !== last 检查(第 3 章)再也没看到值有变化可以上报。

第二个 test,不碰 run_phase

axi_smoke_test(第 2 到 5 章)完全没有变化。新 test 换的是到底跑哪一个 sequence 类,用的正是 uvm 第 6、10 章已经教过的那套机制:

class regfile_no_clear_test extends axi_smoke_test;
  `uvm_component_utils(regfile_no_clear_test)
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    regfile_virtual_seq::type_id::set_type_override(regfile_no_clear_seq::get_type());
    super.build_phase(phase);
  endfunction
endclass

axi_smoke_testrun_phase 通过 type_id::create(...) 建一个 regfile_virtual_seq 然后跑它——一字不改,原样继承。在 build_phase 里、那次 create() 调用真正执行之前设下的 factory override,才是让它实际构造出一个 regfile_no_clear_seq 的原因——和 uvm 第 10 章给 exhaustive_test 用的是完全相同的手法,只是这次换成了替换一个 virtual sequence 的类型,而不是一个普通 sequence 的类型。

跑起来这两个场景

module tb_top;
  // ... 和第 1-5 章一样的 DUT、axi4lite_if、irq_if、config_db 交接 ...
 
  initial begin
    uvm_config_db#(virtual axi4lite_if.tb_mp)::set(null, "*", "vif", axi_if);
    uvm_config_db#(virtual irq_if.mon_mp)::set(null, "*", "vif", irq_intf);
    run_test();
  end
endmodule

不带参数的 run_test()uvm 第 10 章)会从 +UVM_TESTNAME 读取要构建哪个 test:

+UVM_TESTNAME=axi_smoke_test          // 第 5 章的场景:拉高,然后清除
+UVM_TESTNAME=regfile_no_clear_test   // 这一章的场景:拉高,保持拉高,继续计数

同一份编译出来的环境,同一个 DUT,同一个 package——到底跑哪个场景是一个命令行参数,不是一次源码修改。

这个系列没有覆盖的

这套环境协调了两个 agent,通过 RAL 驱动了一个真实的寄存器映射,还跨越了一个被动观察者做了同步——确实比单 agent 的环境能做的多。但它检查的仍然只是这个系列手写出来的这两个场景。这里没有任何东西衡量这两个场景是不是够了——COUNT 每一个可达的值、每一种响应码、写和读之间每一种可能的先后顺序,是不是真的都被跑到过——也没有任何系统性的手段去找出一个这两个场景恰好都没触发的 bug。这正是 dv-methodology 明确的范围:覆盖率驱动的判断力(衡量并引导彻底性,而不只是检查两个手选的场景)和调试直觉(一旦这些检查里的某一个真的报错了,怎么系统地把它追出来)。和之前每一个 capstone 交接的方式一样:这里的一切都会被复用,没有任何东西被丢弃,下一个系列会把现在还在手工决定的东西标准化。

小结

  • 二十二个文件,一个基于 `include 的 package——和 SV 第 16 章、uvm 第 10 章同一套组织原则,每个文件要么从引入它的那一章起原封未动,要么正好只多了那一章明确说过的那一处添加。
  • regfile_no_clear_seq extends regfile_virtual_seq 复用继承来的 write() 辅助方法和 p_sequencer 声明,只改 body()——和 uvm 第 10 章 my_sequence_exhaustive extends my_sequence 是同一种写法。
  • 在真实 DUT 上验证过:六次使能的 DATA 写、全程不清除,COUNT 会到 6,整个过程只触发一次 irq 事件——irq_monitor(第 3 章)只在变化时才上报,意味着越过阈值之后的写不会产生重复事件。
  • regfile_no_clear_test extends axi_smoke_test,只重写了 build_phase,在继承来的 run_phasecreate() 调用真正跑之前装好 factory override——run_phase 本身完全没碰,和 uvm 第 6、10 章教过的手法一模一样。
  • run_test() 配合 +UVM_TESTNAME,在同一份编译好的 package 里,从命令行切换第 5 章的场景和这一章的场景。
  • 这个系列检查的是两个手选的场景;它从来没衡量过这样够不够,也没有系统性的手段去追一个这两个场景都没碰到的 bug。那是 dv-methodology 的工作。

为什么 regfile_no_clear_seq 继承的是 regfile_virtual_seq,而不是直接继承 uvm_sequence?

拿 regfile_no_clear_seq 跑真实的 DUT,irq_monitor 总共会上报几次 irq 事件,为什么?

不带任何参数调用哪个系统任务/函数,会从 +UVM_TESTNAME 命令行 plusarg 读取要构建哪个 test?