UVM 基础

第 10 章 · 共 10 章

综合实战:搭建完整的 UVM 验证环境

用第二个 test 兑现第 1 章第四条承诺——测试一个新场景只需要一个新类和一次 factory 替换,不需要重写;用 +UVM_TESTNAME 在命令行切换 test;把整个环境整理进一个 package;以及 uvm-advanced 与 dv-methodology 两个系列接下来要讲什么。

九章的内容,一点一点搭出了一套能跑通的环境:一个 transaction(第 4 章)、通过 config_db 传递的虚接口(第 5 章)、一个可以被 factory 替换的 driver(第 6 章)、一对 sequence/sequencer(第 7 章)、打包进 agent 的 monitor(第 8 章)、打包进 env 的 scoreboard(第 9 章)。uvm_agentuvm_env 已经很熟悉了——这一章不会引入任何新的 UVM 概念。剩下要做的,正是一个 capstone 该做的事:用第二个 test 兑现第 1 章第四条承诺,然后把这一切整理成真实项目该有的样子。

已经搭好的东西

部件章节角色
mux_transaction第 4 章,第 8 章扩展激励 + 观测到的输出
config_db 传递第 3、5 章vif 从顶层 module 传给每一个需要它的组件
my_driver第 4、6、7 章seq_item_port 拉到的 item 驱动 vif
my_monitor第 8 章采样 vif,广播一个填好字段的 mux_transaction
my_sequence第 7 章生产两个随机化的 item
my_agent第 8 章打包 sequencer + driver + monitor,支持 active/passive
my_scoreboard第 9 章检查每一个观测到的 transaction,不需要状态
my_env第 9 章打包 agent + scoreboard,把 monitor 接到 scoreboard 上
my_test第 3 章,之后每一章都在简化搭建 env,启动一个 sequence

第二个 sequence:穷举而不是随机

mux2 只有 3 个激励比特——小到可以直接测试每一种组合,而不只是随机抽几个:

class my_sequence_exhaustive extends my_sequence;
  `uvm_object_utils(my_sequence_exhaustive)
 
  function new(string name = "my_sequence_exhaustive");
    super.new(name);
  endfunction
 
  task body();
    for (int i = 0; i < 8; i++) begin
      req = mux_transaction::type_id::create("req");
      start_item(req);
      req.sel = i[0];
      req.a   = i[1];
      req.b   = i[2];
      finish_item(req);
    end
  endtask
endclass

不是每个 sequence 都需要 randomize()——只有 8 种可能的输入组合,直接把循环计数器 i 的每一位赋给一个字段,就能把它们各覆盖一次,比随机抽取更精确。my_sequence_exhaustive 继承的是 my_sequence,不是直接继承 uvm_sequence #(mux_transaction)——这不是风格选择,而是接下来这件事必须满足的条件。

第二个 test——一个新类,一行新代码

class exhaustive_test extends my_test;
  `uvm_component_utils(exhaustive_test)
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    my_sequence::type_id::set_type_override(my_sequence_exhaustive::get_type());
  endfunction
endclass

exhaustive_test 完全没有重写 run_phase——它原样继承了 my_test 在第 9 章写的那个版本,包括那行 seq = my_sequence::type_id::create("seq");exhaustive_test 自己的 build_phase 只加了一行代码,就是第 6 章的 factory 替换:从这一刻起,任何向 factory 索要 my_sequence 的地方,得到的都是一个 my_sequence_exhaustiveseq 依然被声明成 my_sequence——这正是为什么 my_sequence_exhaustive 必须继承 my_sequence,而不是只跟它共享一个父类:type_id::create() 的返回值必须能赋值给 seq 声明的类型。之后 seq.start(sqr) 会多态地(SV 第 11 章)调用 body()——因为 my_sequence_exhaustive 重写了 body(),实际跑的就是穷举版本,尽管调用它的代码一个字都没变。

这正是第 1 章第四条承诺的具体兑现:测试一个新场景只需要一个新的 sequence 类,加一个只有一行新代码的新 test 类——my_testmy_envmy_agent、driver、monitor、scoreboard 全都原封不动。

不改源码切换 test:+UVM_TESTNAME

把顶层 module 的 run_test("my_test") 改成不带参数的 run_test()

`include "uvm_macros.svh"
import uvm_pkg::*;
import mux_uvm_pkg::*;
 
module tb_top;
  mux2_if bus_if();
  mux2 dut (bus_if);
 
  initial begin
    uvm_config_db#(virtual mux2_if.tb_mp)::set(null, "*", "vif", bus_if);
    run_test();
  end
endmodule

不带参数时,run_test() 会从命令行的 +UVM_TESTNAME 参数里读取该构造哪个 test,而不是用写死的字符串。在仿真器命令行(或者 EDA Playground 的运行参数栏)里:+UVM_TESTNAME=my_test 跑原来那个两个随机 item 的 test,+UVM_TESTNAME=exhaustive_test 跑全部八种组合——同一份编译好的 testbench,同一批源文件,只是运行时选了不同的 test。这正是第 1 章点名、整个系列一路铺垫过来的回报:测试一个不同的场景,是一个命令行参数,不是一次代码修改。

把一切整理起来:一个 package

SV 第 16 章讲过用 package 组织跨文件的代码。真正的 UVM 环境用的是同一个思路——每个类一个文件,全部拉进一个 package,顶层 module 只 import 一次:

package mux_uvm_pkg;
  import uvm_pkg::*;
  `include "uvm_macros.svh"
 
  `include "mux_transaction.sv"
  `include "my_sequence.sv"
  `include "my_sequence_exhaustive.sv"
  `include "my_driver.sv"
  `include "my_driver_alt.sv"
  `include "my_monitor.sv"
  `include "my_agent.sv"
  `include "my_scoreboard.sv"
  `include "my_env.sv"
  `include "my_test.sv"
  `include "exhaustive_test.sv"
endpackage

package 内部用 `include(而不是让这些文件散落着各自编译),能让列表里的每个类都按依赖顺序对其他类可见,并通过一次 import mux_uvm_pkg::*; 让顶层 module 都能用到——这正是 SV 第 16 章教的组织原则,第一次用在一个有不止一个类的环境上。

这个系列没有覆盖的东西

这套环境检查的是正确性——它不衡量测试是否足够彻底,而且从头到尾只驱动了一个 agent。协调不止一个 agent(真实的 DUT 通常不止一个接口)、把多个 sequence 分层组合、以及给 testbench 配一套共享的寄存器模型,都是更大的环境会遇到的真实问题——而这正是下一个系列 uvm-advanced 的内容:virtual sequence、多 agent 协同,以及 UVM 寄存器抽象层(RAL)。这里也没有任何东西在追踪某一次运行到底跑到了 8 种输入组合里的哪几种,两个 test 加起来是不是覆盖了所有值得覆盖的场景,或者还有什么没测到——衡量这些、以及判断"测得够不够"这套纪律,是 dv-methodology 的工作。和这个系列第 1 章的交接方式一样:这里的一切都会被复用,没有任何东西被丢弃,接下来的系列会把现在还在临时应付、或者还没做的东西标准化。

小结

  • my_sequence_exhaustive extends my_sequence(不是直接继承 uvm_sequence #(mux_transaction))通过直接赋值而不是 randomize(),驱动出 mux2 全部 8 种可能的输入组合——适合输入空间小到可以穷举覆盖的场景。
  • 在一个新 test 的 build_phase 里设置的 factory 替换(第 6 章)——my_sequence::type_id::set_type_override(my_sequence_exhaustive::get_type())——改变了一个完全没改动、原样继承的 run_phase 最终跑的是哪个 sequence。
  • 不带参数的 run_test() 会从命令行的 +UVM_TESTNAME 参数读取该构建哪个 test,所以切换 test 是一个运行时的参数,不是一次源码修改。
  • 真正的 UVM 环境,类是一个文件一个地存在,用 `include 拉进一个 package,由顶层 module 统一 import——和 SV 第 16 章教的组织原则一样。
  • 这个系列只用一个 agent 检查了正确性,从来没有协调过不止一个 agent、没有分层组合过 sequence,也没有衡量过彻底性。多 agent 协同和共享寄存器模型是 uvm-advanced 的工作;衡量彻底性是 dv-methodology 的工作——就像这整个系列曾经是 UVM 对 SV 基础系列 capstone 没能标准化的东西给出的答案一样。

为什么 my_sequence_exhaustive 必须继承 my_sequence,而不是直接继承 uvm_sequence #(mux_transaction)?

exhaustive_test 完全没有重写 run_phase。它是怎么跑出八个 transaction,而不是 my_test 原来的两个的?

run_test('my_test') 和不带参数的 run_test() 配合命令行 +UVM_TESTNAME=my_test,实际区别是什么?

这个系列的环境检查了什么?又没有衡量什么——而这正是 dv-methodology 关注的重点?