跳到主要内容
UVM验证

UVM 工程化实践

从验证计划、平台装配、连接机制到检查闭环与覆盖收敛的 UVM 工程实践

UVM 验证平台 工程实践

UVM 工程化实践

本文面向已经掌握 SystemVerilog 和 UVM 基础概念的读者。 如果尚不熟悉 Component、Sequence Item、Sequence 和 Sequencer,请先阅读《UVM 基础入门》。

工程化验证平台不是若干 UVM 类的集合,而是一条可以追踪、诊断和收敛的验证闭环:

需求 → 检查点 → 激励 → DUT 可观察行为 → 预测与比对 → 覆盖证据 → 收敛结论

全文使用同一套协议无关命名。代码片段用于说明职责和接口契约,可以按章节组合, 但不代表一套绑定特定 DUT 的完整可运行工程。

通用名称工程职责
request_item表示输入侧已经发生或准备驱动的事务
response_item表示输出侧已经观察到的事务
cycle_sample表示周期精确模型使用的一次完整采样
protocol_if封装时钟、复位、协议引脚和 clocking block
agent_config保存 Agent 模式、virtual interface 和策略配置
protocol_agent封装一个协议接口的激励与观测能力
predictor根据已观察输入生成 expected
scoreboard对齐并比较 expected 与 actual
verification_env组装 Agent、Predictor、Scoreboard 和 Coverage
base_test配置环境并管理测试结束条件

1. 从验证计划推导平台架构

1.1 先定义证据,再定义组件

平台设计应从“怎样证明需求成立”开始,而不是先创建 Driver、Monitor 和 Scoreboard。 每条需求至少要落到一项可执行检查或一项可度量覆盖;仅有激励而没有判定标准, 不能形成验证闭环。

建议维护如下追踪表:

需求/风险激励条件可观察事件检查机制覆盖证据
正常传输合法请求输入/输出握手Predictor + Scoreboard操作类型 coverpoint
反压保持ready 拉低valid/data 保持SVA反压长度 bin
复位清理事务中插入 reset状态与输出归零SVA + Scoreboard flushreset × 活动状态 cross
边界计数最小/最大长度beat 数与 lastScoreboard长度边界 bins
并发或乱序多个未完成请求响应 ID/顺序ID 匹配 Scoreboardoutstanding 数 cross

这张表同时决定:

  • Transaction 需要携带哪些字段;
  • Monitor 应在哪个事件上发布事务;
  • Predictor 是否需要保存状态;
  • Scoreboard 使用 FIFO 还是 ID/tag 匹配;
  • 哪些性质适合 SVA,哪些必须做端到端比较;
  • 测试何时才算真正结束。

1.2 可控性、可观测性和可判定性

一个检查点可以落地,需要同时满足三个条件:

条件问题平台对应能力
可控性能否制造目标场景?Sequence、约束、错误注入、寄存器配置
可观测性能否知道 DUT 实际看到了什么?Input Monitor、Output Monitor、内部绑定或 RAL
可判定性能否判断观察结果正确与否?SVA、Predictor、Scoreboard、Coverage

如果某个需求无法转成可观察事件,应该先调整接口、绑定点或 transaction 抽象, 而不是在 Test 中读取层次路径临时补洞。

1.3 平台的四条路径

UVM 平台可以按四条相互独立的路径理解:

激励路径:Sequence → Sequencer → Driver → Interface → DUT
观测路径:DUT/Interface → Monitor → observed transaction
检查路径:Input Monitor → Predictor → expected
          Output Monitor ─────────────→ actual → Scoreboard
控制路径:Test/Config → Env/Agent;Factory → 可替换实现;Phase → 生命周期

连接设计的核心原则是:

  • 激励意图和实际观察分离;
  • expected 只从规格和实际输入推导;
  • actual 只从 DUT 输出观察;
  • 数据通过明确的 TLM 端口流动;
  • 配置通过配置对象向下分发;
  • Test 选择场景,不承担协议时序和结果比较。

1.4 组件职责边界

组件应负责不应负责
Sequence生成场景、事务组合和约束直接驱动信号、读取 DUT 输出
Driver把事务翻译为协议时序预测 DUT 结果、判断 PASS/FAIL
Monitor只观察并重构事实修正协议错误、复用同一快照对象
Predictor按规格生成 expected读取 actual 后反推结果
Scoreboard对齐、比较、统计完整性生成激励、模拟引脚时序
Coverage度量场景是否到达代替正确性检查
Test配置环境、启动场景、控制结束直接跨层连接底层组件

2. 静态装配:从类型依赖到组件拓扑

2.1 推荐的依赖顺序

工程首先要解决编译期依赖,再解决 UVM 组件树。一个常见顺序是:

协议类型与常量

transaction / config

sequence / sequencer / driver / monitor

agent

predictor / scoreboard / coverage

environment / test

顶层 module、interface 实例和 run_test()

Package 负责组织 class、typedef 和公共函数;Interface 属于静态硬件结构,通常在 package 外声明。编译顺序必须保证被引用类型先可见,章节顺序不能替代编译依赖。

package verification_pkg;
  import uvm_pkg::*;
  `include "uvm_macros.svh"

  typedef enum {READ_OP, WRITE_OP} operation_e;

  // 依赖顺序:item/config → agent → checker → env → test
endpackage

2.2 Transaction 是组件间的数据契约

输入和输出字段通常不同,不应为了省类而强行共用一个 transaction。 激励控制字段也不应进入功能比较。

class request_item extends uvm_sequence_item;
  rand operation_e      op;
  rand bit [31:0]       address;
  rand bit [31:0]       data;
  rand int unsigned     idle_cycles;
  rand int unsigned     beat_count = 1;

  longint unsigned      sample_cycle;
  int unsigned          reset_epoch;
  int unsigned          transaction_id;

  `uvm_object_utils(request_item)

  function new(string name = "request_item");
    super.new(name);
  endfunction

  function void do_copy(uvm_object rhs);
    request_item rhs_item;
    if (!$cast(rhs_item, rhs))
      `uvm_fatal("COPY", "request_item type mismatch")

    super.do_copy(rhs);
    op             = rhs_item.op;
    address        = rhs_item.address;
    data           = rhs_item.data;
    idle_cycles    = rhs_item.idle_cycles;
    beat_count     = rhs_item.beat_count;
    sample_cycle   = rhs_item.sample_cycle;
    reset_epoch    = rhs_item.reset_epoch;
    transaction_id = rhs_item.transaction_id;
  endfunction

  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    request_item rhs_item;
    if (!$cast(rhs_item, rhs))
      return 0;

    return super.do_compare(rhs, comparer) &&
           op             == rhs_item.op &&
           address        == rhs_item.address &&
           data           == rhs_item.data &&
           beat_count     == rhs_item.beat_count &&
           reset_epoch    == rhs_item.reset_epoch &&
           transaction_id == rhs_item.transaction_id;
  endfunction

  function string convert2string();
    return $sformatf(
      "id=%0d epoch=%0d cycle=%0d op=%s beats=%0d addr=%08h data=%08h",
      transaction_id, reset_epoch, sample_cycle,
      op.name(), beat_count, address, data);
  endfunction
endclass

class response_item extends uvm_sequence_item;
  bit [31:0]            data;
  bit [1:0]             status;

  longint unsigned      sample_cycle;
  longint unsigned      earliest_cycle;
  longint unsigned      latest_cycle;
  int unsigned          reset_epoch;
  int unsigned          transaction_id;

  `uvm_object_utils(response_item)

  function new(string name = "response_item");
    super.new(name);
  endfunction

  function void do_copy(uvm_object rhs);
    response_item rhs_item;
    if (!$cast(rhs_item, rhs))
      `uvm_fatal("COPY", "response_item type mismatch")

    super.do_copy(rhs);
    data           = rhs_item.data;
    status         = rhs_item.status;
    sample_cycle   = rhs_item.sample_cycle;
    earliest_cycle = rhs_item.earliest_cycle;
    latest_cycle   = rhs_item.latest_cycle;
    reset_epoch    = rhs_item.reset_epoch;
    transaction_id = rhs_item.transaction_id;
  endfunction

  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    response_item rhs_item;
    if (!$cast(rhs_item, rhs))
      return 0;

    return super.do_compare(rhs, comparer) &&
           data           == rhs_item.data &&
           status         == rhs_item.status &&
           reset_epoch    == rhs_item.reset_epoch &&
           transaction_id == rhs_item.transaction_id;
  endfunction

  function string convert2string();
    return $sformatf(
      "id=%0d epoch=%0d cycle=%0d window=[%0d:%0d] status=%0h data=%08h",
      transaction_id, reset_epoch, sample_cycle,
      earliest_cycle, latest_cycle, status, data);
  endfunction
endclass

class cycle_sample extends uvm_sequence_item;
  bit                  reset_active;
  bit                  request_handshake;
  longint unsigned     sample_cycle;
  int unsigned         reset_epoch;

  `uvm_object_utils(cycle_sample)

  function new(string name = "cycle_sample");
    super.new(name);
  endfunction
endclass

字段可以分成三类:

  • 功能字段:地址、数据、操作类型、状态;
  • 激励控制字段:空闲周期、错误注入策略、节流方式;
  • 对齐元数据:周期号、事务 ID、reset epoch。

只有实际可观察或由规格定义的字段才能进入 expected/actual 比较。

对于关键 transaction,建议显式实现 do_copy()do_compare()convert2string()。Field Macro 适合短小对象,但大型工程中显式实现更容易控制 比较字段、日志格式和性能。

上例的 do_compare() 不比较 idle_cyclessample_cycle:前者只是激励控制, 后者应由 Scoreboard 按延迟契约单独检查,不能混入功能数据比较。

Field Macro 与显式方法

Field Macro 可以自动生成 copy、compare、print、pack 和 record 行为:

class compact_item extends uvm_sequence_item;
  rand bit [31:0] data;
  rand int unsigned delay_cycles;
  longint unsigned sample_cycle;

  `uvm_object_utils_begin(compact_item)
    `uvm_field_int(data,         UVM_DEFAULT)
    `uvm_field_int(delay_cycles, UVM_NOCOMPARE)
    `uvm_field_int(sample_cycle, UVM_NOCOMPARE | UVM_NOPRINT)
  `uvm_object_utils_end
endclass

常用 flag:

Flag含义
UVM_DEFAULT使用默认自动化操作
UVM_NOCOPYcopy 时忽略
UVM_NOCOMPAREcompare 时忽略
UVM_NOPRINTprint/sprint 时忽略
UVM_NOPACKpack/unpack 时忽略

Field Macro 适合字段很少、语义直接的对象。以下情况优先显式实现:

  • 比较需要 mask、容差或顺序无关;
  • 日志只希望打印诊断字段;
  • 对象很大或调用频繁,性能敏感;
  • 动态数组、队列和嵌套对象有特殊所有权;
  • 某些元数据需要 copy 但不能 compare。

不要同时用 Field Macro 和手写 do_compare() 比较同一字段,否则维护者很难判断最终 规则来自哪里。

2.3 Interface 负责信号边界和采样语义

Interface 不只是信号打包,还应明确 Driver 和 Monitor 在哪个时钟区域访问哪些信号。

interface protocol_if(input logic clk, input logic rst_n);
  logic        req_valid;
  logic        req_ready;
  logic        req_write;
  logic [31:0] req_address;
  logic [31:0] req_data;
  logic [31:0] req_id;

  logic        rsp_valid;
  logic        rsp_ready;
  logic [31:0] rsp_data;
  logic [1:0]  rsp_status;
  logic [31:0] rsp_id;
  logic        rsp_last;

  clocking drv_cb @(posedge clk);
    default input #1step output #0;
    input  rst_n;
    input  req_ready;
    output req_valid;
    output req_write;
    output req_address;
    output req_data;
    output req_id;
    output rsp_ready;
  endclocking

  clocking mon_cb @(posedge clk);
    default input #1step;
    input rst_n;
    input req_valid, req_ready, req_write, req_address, req_data, req_id;
    input rsp_valid, rsp_ready, rsp_data, rsp_status, rsp_id, rsp_last;
  endclocking

  modport drv_mp(clocking drv_cb, input rst_n);
  modport mon_mp(clocking mon_cb, input rst_n);
endinterface

Clocking block 把“何时采样”和“何时驱动”写成接口契约,可减少 Driver、Monitor 与 DUT 在同一时刻读写导致的 race。所有握手判断都应基于同一个采样视图。

2.4 配置对象集中表达变化点

不要让每个组件分别从 uvm_config_db 获取零散标量。Agent 配置对象可以集中表达 接口句柄、主动/被动模式和协议策略。

class agent_config extends uvm_object;
  virtual protocol_if.drv_mp drv_vif;
  virtual protocol_if.mon_mp mon_vif;

  uvm_active_passive_enum is_active = UVM_ACTIVE;
  bit checks_enable = 1;
  bit coverage_enable = 1;
  bit backpressure_enable = 0;
  int unsigned max_stall_cycles = 0;
  int unsigned response_timeout_cycles = 1000;

  `uvm_object_utils(agent_config)

  function new(string name = "agent_config");
    super.new(name);
  endfunction
endclass

配置的所有权建议如下:

  • Top 创建并绑定真实 virtual interface;
  • Test 创建配置对象并决定策略;
  • Environment 把配置传给对应 Agent;
  • Agent 将同一个配置句柄分发给 Driver 和 Monitor;
  • 子组件在 build phase 获取配置,缺少必需配置时立即 fatal。

2.5 Agent 封装一个协议接口

Passive Agent 始终可以观察接口;Active Agent 在此基础上增加 Sequencer 和 Driver。

class protocol_agent extends uvm_agent;
  `uvm_component_utils(protocol_agent)

  agent_config cfg;
  protocol_sequencer sequencer;
  protocol_driver    driver;
  protocol_monitor   monitor;

  function new(string name = "protocol_agent",
               uvm_component parent = null);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    if (!uvm_config_db#(agent_config)::get(this, "", "cfg", cfg))
      `uvm_fatal("NO_CFG", "protocol_agent requires agent_config")

    monitor = protocol_monitor::type_id::create("monitor", this);
    uvm_config_db#(agent_config)::set(this, "monitor", "cfg", cfg);

    if (cfg.is_active == UVM_ACTIVE) begin
      sequencer = protocol_sequencer::type_id::create("sequencer", this);
      driver    = protocol_driver::type_id::create("driver", this);
      uvm_config_db#(agent_config)::set(this, "driver", "cfg", cfg);
    end
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (cfg.is_active == UVM_ACTIVE)
      driver.seq_item_port.connect(sequencer.seq_item_export);
  endfunction
endclass

Agent 内部连接由 Agent 自己负责;跨 Agent 的数据流由 Environment 负责。这个边界可以 避免 Test 依赖 env.agent.driver... 一类脆弱层次路径。

2.6 Environment 是集成边界

Environment 创建验证组件并完成跨组件连接,不负责生成具体测试场景。

class verification_env extends uvm_env;
  `uvm_component_utils(verification_env)

  protocol_agent input_agent;
  protocol_agent output_agent;
  predictor      pred;
  scoreboard     scb;
  coverage_collector cov;

  function new(string name = "verification_env",
               uvm_component parent = null);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    input_agent  = protocol_agent::type_id::create("input_agent", this);
    output_agent = protocol_agent::type_id::create("output_agent", this);
    pred         = predictor::type_id::create("pred", this);
    scb          = scoreboard::type_id::create("scb", this);
    cov          = coverage_collector::type_id::create("cov", this);
  endfunction
endclass

典型拓扑是一个 Active 输入 Agent 加一个 Passive 输出 Agent。对于同一双向接口,也可以 只使用一个 Agent,并由一个 Monitor 发布输入、输出两条 analysis stream。

2.7 Test 和 Top 的边界

Top 属于静态世界,负责:

  • 时钟与复位;
  • DUT 和 Interface 实例化;
  • virtual interface 注入;
  • 调用 run_test()

Test 属于 UVM 动态世界,负责:

  • 创建并配置 Environment;
  • 选择 Factory Override;
  • 启动 Sequence 或 Virtual Sequence;
  • 管理 objection、超时和结束条件。

两者通过 virtual interface 和配置对象相连,不应由 class 直接使用层次路径访问 DUT。

2.8 Package、宏和 Factory 注册

工程中通常把 transaction、sequence 和 component 放在一个或多个 package 中。推荐先 导入 UVM 类型,再包含宏定义,最后按依赖顺序包含项目 class。

package verification_pkg;
  import uvm_pkg::*;
  `include "uvm_macros.svh"

  `include "request_item.svh"
  `include "response_item.svh"
  `include "agent_config.svh"
  `include "protocol_sequence.svh"
  `include "protocol_sequencer.svh"
  `include "protocol_driver.svh"
  `include "protocol_monitor.svh"
  `include "protocol_agent.svh"
  `include "predictor.svh"
  `include "scoreboard.svh"
  `include "verification_env.svh"
  `include "base_test.svh"
endpackage

import uvm_pkg::*; 导入类、函数和枚举;uvm_macros.svh 提供 uvm_component_utilsuvm_object_utils、report macro 等预处理宏。二者作用不同, 不能互相替代。

Component 和 Object 的注册、构造与创建形式也不同:

类型注册宏构造函数Factory 创建
Componentuvm_component_utils(T)new(name, parent)T::type_id::create(name, parent)
Objectuvm_object_utils(T)new(name)T::type_id::create(name)
class protocol_driver extends uvm_driver #(request_item);
  `uvm_component_utils(protocol_driver)

  function new(string name = "protocol_driver",
               uvm_component parent = null);
    super.new(name, parent);
  endfunction
endclass

class protocol_sequence extends uvm_sequence #(request_item);
  `uvm_object_utils(protocol_sequence)

  function new(string name = "protocol_sequence");
    super.new(name);
  endfunction
endclass

注册只是让 Factory 知道类型;真正进入 UVM component tree 仍需要在 build phase 中调用 type_id::create()。Sequence、Transaction 和 Config 是 Object,不会出现在 component topology 中。

2.9 Virtual Interface 的完整绑定链

Virtual interface 的工程路径应完整覆盖三个阶段:

Top 中的真实 interface 实例
  ↓ uvm_config_db set
Test 获得 virtual interface
  ↓ 写入 agent_config
Agent 分发给 Driver/Monitor

Driver/Monitor 通过 clocking block 访问引脚

Top 只注入接口句柄,不创建 UVM component:

module top;
  import uvm_pkg::*;
  import verification_pkg::*;

  logic clk;
  logic rst_n;

  protocol_if input_if (clk, rst_n);
  protocol_if output_if(clk, rst_n);

  initial begin
    uvm_config_db#(virtual protocol_if.drv_mp)::set(
      null,
      "uvm_test_top",
      "input_drv_vif",
      input_if.drv_mp
    );

    uvm_config_db#(virtual protocol_if.mon_mp)::set(
      null,
      "uvm_test_top",
      "input_mon_vif",
      input_if.mon_mp
    );

    uvm_config_db#(virtual protocol_if.mon_mp)::set(
      null,
      "uvm_test_top",
      "output_mon_vif",
      output_if.mon_mp
    );

    run_test();
  end
endmodule

Test 把三个接口视角装入两个 Agent 配置:

function void base_test::build_phase(uvm_phase phase);
  super.build_phase(phase);

  // input_cfg/output_cfg 是 base_test 的成员,派生 Test 可通过 hook 修改。
  input_cfg  = agent_config::type_id::create("input_cfg");
  output_cfg = agent_config::type_id::create("output_cfg");

  input_cfg.is_active  = UVM_ACTIVE;
  output_cfg.is_active = UVM_PASSIVE;

  if (!uvm_config_db#(virtual protocol_if.drv_mp)::get(
        this, "", "input_drv_vif", input_cfg.drv_vif))
    `uvm_fatal("NO_VIF", "input driver interface was not configured")

  if (!uvm_config_db#(virtual protocol_if.mon_mp)::get(
        this, "", "input_mon_vif", input_cfg.mon_vif))
    `uvm_fatal("NO_VIF", "input monitor interface was not configured")

  if (!uvm_config_db#(virtual protocol_if.mon_mp)::get(
        this, "", "output_mon_vif", output_cfg.mon_vif))
    `uvm_fatal("NO_VIF", "output monitor interface was not configured")

  configure_environment();

  uvm_config_db#(agent_config)::set(
    this, "env.input_agent", "cfg", input_cfg);
  uvm_config_db#(agent_config)::set(
    this, "env.output_agent", "cfg", output_cfg);

  env = verification_env::type_id::create("env", this);
endfunction

这里必须同时匹配:

  • setget 的参数化类型;
  • modport 类型;
  • field name;
  • component 路径;
  • interface 实例方向。

即使底层 interface 类型相同,输入和输出 Monitor 也不能通过全局通配符取得同一个实例。

2.10 Config DB 四个参数与路径规则

uvm_config_db 的基本形式是:

uvm_config_db#(T)::set(cntxt, inst_name, field_name, value);
uvm_config_db#(T)::get(cntxt, inst_name, field_name, value);
参数含义示例
T保存值的精确类型agent_configvirtual protocol_if.mon_mp
cntxt相对路径的起点this 或 Top 使用的 null
inst_name从起点出发的目标路径"env.input_agent"
field_name配置键名"cfg""input_mon_vif"
value写入或返回的值配置对象或 interface 句柄

get(this, "", "cfg", cfg) 表示从当前组件开始查找名为 cfg 的配置。路径匹配发生 在 UVM component 层次中,不是 SystemVerilog 的 class 变量路径。

常用原则:

  • 在父组件创建子组件之前完成面向子组件的 set
  • 子组件在 build phase 尽早 get
  • 必需配置获取失败立即 fatal;
  • 多实例环境优先使用准确路径;
  • 配置项较多时传递一个 config object;
  • 不用 config_db 传递高频 transaction 数据。

配置失败时依次检查:

  1. T 是否完全一致,特别是 virtual interface 的 modport;
  2. Top 的 set 是否发生在 run_test() 之前;
  3. Test、Env、Agent 的实例名是否与路径一致;
  4. field name 大小写是否一致;
  5. 是否有更宽泛的配置覆盖了精确配置;
  6. 目标组件是否在预期 build phase 获取。

2.11 Component Tree 与连接所有权

一个典型层次如下:

uvm_test_top
└── env
    ├── input_agent
    │   ├── sequencer
    │   ├── driver
    │   └── monitor
    ├── output_agent
    │   └── monitor
    ├── pred
    ├── scb
    └── cov

每个父组件只创建自己的直接子组件:

  • Test 创建 Environment;
  • Environment 创建 Agent、Predictor、Scoreboard 和 Coverage;
  • Agent 创建 Driver、Monitor 和 Sequencer;
  • Object 由真正拥有或使用它的组件创建。

连接所有权同样分层:

连接负责者
Driver ↔ SequencerAgent
Monitor → Predictor/CoverageEnvironment
Predictor → ScoreboardEnvironment
Output Monitor → ScoreboardEnvironment
Virtual Sequencer 的子 sequencer 句柄Environment 或 Test 的统一配置层

Environment 通常不需要 run phase;只有环境本身承担集中式 watchdog、跨 Agent 协调或 全局状态管理时,才需要长期线程。

在 end of elaboration 阶段打印 topology,可以立即发现组件漏建、Active/Passive 配置错误 和实例名与 config path 不一致:

function void base_test::end_of_elaboration_phase(uvm_phase phase);
  super.end_of_elaboration_phase(phase);
  uvm_top.print_topology();
endfunction

3. 四条连接路径及其契约

3.1 激励路径:意图变成引脚时序

Sequence → request_item → Sequencer → Driver → clocking block → DUT

Sequence 只描述“发什么”和“以什么组合发”,Driver 负责“怎样满足协议时序”。

class protocol_sequence extends uvm_sequence #(request_item);
  `uvm_object_utils(protocol_sequence)

  task body();
    request_item req;
    int unsigned next_id;

    repeat (20) begin
      req = request_item::type_id::create("req");
      start_item(req);
      if (!req.randomize())
        `uvm_fatal("RAND", "request_item randomization failed")
      req.transaction_id = next_id++;
      finish_item(req);
    end
  endtask
endclass

Driver 必须保证每次 get_next_item() 最终都有一次 item_done(),且只有在真实握手 发生后才认为该 beat 被 DUT 接收。

task protocol_driver::run_phase(uvm_phase phase);
  request_item req;

  drive_idle();
  forever begin
    seq_item_port.get_next_item(req);
    drive_request(req);
    seq_item_port.item_done();
  end
endtask

task protocol_driver::drive_request(request_item req);
  repeat (req.idle_cycles) @(cfg.drv_vif.drv_cb);

  cfg.drv_vif.drv_cb.req_valid <= 1'b1;
  cfg.drv_vif.drv_cb.req_write <= (req.op == WRITE_OP);
  cfg.drv_vif.drv_cb.req_address <= req.address;
  cfg.drv_vif.drv_cb.req_data  <= req.data;
  cfg.drv_vif.drv_cb.req_id    <= req.transaction_id;

  forever begin
    @(cfg.drv_vif.drv_cb);

    if (!cfg.drv_vif.drv_cb.rst_n) begin
      drive_idle();
      return;
    end

    if (cfg.drv_vif.drv_cb.req_ready)
      break;
  end

  cfg.drv_vif.drv_cb.req_valid <= 1'b0;
endtask

反压期间 Driver 必须保持协议要求稳定的控制和数据,不能因为等待 ready 而换成下一个 transaction。

3.2 观测路径:信号变成不可篡改的事实

DUT/Interface → Monitor → observed transaction → analysis_port fanout

Monitor 发布的是接口上已经发生的事实,不是 Sequence 原本想发送的内容。握手型接口应 在 valid && ready 成立时采样。

class protocol_monitor extends uvm_monitor;
  `uvm_component_utils(protocol_monitor)

  agent_config cfg;
  uvm_analysis_port #(request_item)  request_ap;
  uvm_analysis_port #(response_item) response_ap;
  longint unsigned cycle_count;
  int unsigned reset_epoch;
  bit in_reset;

  function new(string name = "protocol_monitor",
               uvm_component parent = null);
    super.new(name, parent);
    request_ap  = new("request_ap", this);
    response_ap = new("response_ap", this);
  endfunction

  task run_phase(uvm_phase phase);
    request_item  req_snapshot;
    response_item rsp_snapshot;

    forever begin
      @(cfg.mon_vif.mon_cb);
      cycle_count++;

      if (!cfg.mon_vif.mon_cb.rst_n) begin
        if (!in_reset)
          reset_epoch++;
        in_reset = 1'b1;
        continue;
      end
      in_reset = 1'b0;

      if (cfg.mon_vif.mon_cb.req_valid &&
          cfg.mon_vif.mon_cb.req_ready) begin
        req_snapshot = request_item::type_id::create("req_snapshot");
        req_snapshot.op           = cfg.mon_vif.mon_cb.req_write
                                  ? WRITE_OP : READ_OP;
        req_snapshot.address      = cfg.mon_vif.mon_cb.req_address;
        req_snapshot.data         = cfg.mon_vif.mon_cb.req_data;
        req_snapshot.sample_cycle = cycle_count;
        req_snapshot.reset_epoch  = reset_epoch;
        req_snapshot.transaction_id = cfg.mon_vif.mon_cb.req_id;
        request_ap.write(req_snapshot);
      end

      if (cfg.mon_vif.mon_cb.rsp_valid &&
          cfg.mon_vif.mon_cb.rsp_ready) begin
        rsp_snapshot = response_item::type_id::create("rsp_snapshot");
        rsp_snapshot.data         = cfg.mon_vif.mon_cb.rsp_data;
        rsp_snapshot.status       = cfg.mon_vif.mon_cb.rsp_status;
        rsp_snapshot.sample_cycle = cycle_count;
        rsp_snapshot.reset_epoch  = reset_epoch;
        rsp_snapshot.transaction_id = cfg.mon_vif.mon_cb.rsp_id;
        response_ap.write(rsp_snapshot);
      end
    end
  endtask
endclass

每次发布都创建新对象。Analysis port 广播的是对象句柄,不会自动为每个订阅者保存历史 深拷贝;复用同一对象会让队列中的旧事务被后续采样覆盖。

3.3 检查路径:事实生成 expected,再与 actual 对齐

正确数据流是:

Input Monitor  → observed request → Predictor → expected ──┐
Output Monitor → observed response ───────────→ actual   ──┴→ Scoreboard

禁止使用以下捷径:

Sequence request ───────────────→ Predictor
actual response → 反推 expected → Scoreboard

前者绕过了 Driver 和接口握手,后者让参考模型复制 DUT 的错误。Predictor 只能依赖规格、 配置以及 Input Monitor 已观察到的输入。

3.4 控制路径:配置、替换和生命周期

Top → virtual interface
Test → environment/agent config
Factory → component/object implementation
Phase scheduler → create/connect/run/check/report

uvm_config_db 适合向下传递配置,不适合作为无边界的全局变量。路径应尽量准确, 尤其当存在多个同类型 Agent 时。

uvm_config_db#(agent_config)::set(
  this, "env.input_agent", "cfg", input_cfg);

uvm_config_db#(agent_config)::set(
  this, "env.output_agent", "cfg", output_cfg);

宽泛的 "*" 会让多个组件意外获得同一个 interface 或策略对象,应只在确实希望所有 后代共享配置时使用。

3.5 Environment 中的完整连接

function void verification_env::connect_phase(uvm_phase phase);
  super.connect_phase(phase);

  input_agent.monitor.request_ap.connect(pred.request_imp);
  input_agent.monitor.request_ap.connect(cov.request_imp);

  pred.expected_ap.connect(scb.expected_fifo.analysis_export);
  output_agent.monitor.response_ap.connect(scb.actual_fifo.analysis_export);
endfunction

连接矩阵应在编码前确定:

发布端接收端数据类型目的
Input MonitorPredictorrequest_item生成 expected
Input MonitorCoveragerequest_item统计实际输入场景
PredictorScoreboard expected FIFOresponse_item保存预期结果
Output MonitorScoreboard actual FIFOresponse_item保存实际结果
Output MonitorCoverageresponse_item统计实际输出场景

Analysis port 支持一对多广播;连接顺序不应承担功能顺序。若消费者需要等待、缓存或节流, 应使用 FIFO 或独立线程,而不是依赖某个 write() 先执行。

3.6 Driver 与 Sequencer 的握手契约

Driver 和 Sequencer 之间最常用的是 pull 模式:

Sequence: start_item(req)
  ↓ 等待 arbitration grant
Sequence: randomize / 填充 req
Sequence: finish_item(req)

Driver: get_next_item(req)
Driver: 完成协议驱动
Driver: item_done()

关键 API 的含义如下:

API调用者含义
start_item(req)Sequence申请 Sequencer 仲裁
finish_item(req)Sequence提交填充完成的 request 并等待 Driver 接收
get_next_item(req)Driver阻塞等待下一个 request
item_done()Driver表示当前 request 已处理完
item_done(rsp)Driver完成 request 并同时返回 response
put_response(rsp)Driver独立发送 response
get_response(rsp)Sequence等待 Driver 返回的 response

如果 Sequence 需要 response,Driver 应复制 request 的 sequence ID 和 transaction ID:

task protocol_driver::complete_with_response(
  request_item req,
  response_item rsp
);
  rsp.set_id_info(req);
  seq_item_port.item_done(rsp);
endtask

这里的 UVM sequence ID 与协议的 transaction_id 不是同一个概念:

  • sequence ID 用于把 Driver response 路由回正确 Sequence;
  • transaction ID 是协议字段,用于 DUT outstanding/乱序匹配。

Driver 的 reset 策略必须明确。常见选择有:

  1. reset 到来时取消当前事务,驱动 idle,并正常 item_done()
  2. reset 解除后重新发送当前事务;
  3. 返回带 cancelled 状态的 response;
  4. 把 reset 视为 fatal,适用于不允许运行期 reset 的协议。

无论选择哪一种,都不能因为 reset 跳出线程而漏掉 item_done(),否则 Sequencer 会永久 等待当前 item 完成。

多 beat Driver 应只在握手成功时推进 beat index:

for (int unsigned beat = 0; beat < req.beat_count; ) begin
  drive_one_beat(req, beat);
  @(cfg.drv_vif.drv_cb);

  if (!cfg.drv_vif.drv_cb.rst_n) begin
    cancel_current_request(req);
    break;
  end

  if (cfg.drv_vif.drv_cb.req_valid &&
      cfg.drv_vif.drv_cb.req_ready)
    beat++;
end

反压期间保持 data、ID、last 和 byte enable 稳定,是 Driver 的协议责任;Sequence 不应 感知逐周期 ready。

3.7 Monitor 的事务边界与多 beat 重组

Monitor 的核心不是“读取信号”,而是依据协议定义事务何时开始、何时结束以及什么事件 才算有效。

对于单 beat ready/valid 协议,握手就是 transaction 边界。对于 burst 或 frame 协议, Monitor 需要一个局部重组状态机:

task protocol_monitor::collect_burst();//类外实现
  response_item rsp;
  bit collecting = 0;
  int unsigned beat_index = 0;

  forever begin
    @(cfg.mon_vif.mon_cb);

    if (!cfg.mon_vif.mon_cb.rst_n) begin
      collecting = 0;
      beat_index = 0;
      continue;
    end

    if (!(cfg.mon_vif.mon_cb.rsp_valid &&
          cfg.mon_vif.mon_cb.rsp_ready))
      continue;

    if (!collecting) begin
      rsp = response_item::type_id::create("rsp");
      rsp.transaction_id = cfg.mon_vif.mon_cb.rsp_id;
      rsp.reset_epoch = reset_epoch;
      collecting = 1;
    end

    append_beat(rsp, cfg.mon_vif.mon_cb.rsp_data, beat_index);
    beat_index++;

    if (cfg.mon_vif.mon_cb.rsp_last) begin
      rsp.sample_cycle = cycle_count;
      response_ap.write(rsp);
      collecting = 0;
      beat_index = 0;
    end
  end
endtask

多 beat transaction 应保存或检查:

  • transaction ID;
  • beat index;
  • 每拍有效字节或 strobe;
  • response/status;
  • last 是否出现在合法位置;
  • 首拍与末拍周期;
  • reset 是否在中间打断。

Monitor 还应在发布前处理 X/Z。控制信号出现未知值通常说明协议或 reset 问题,不应通过 if (signal) 默默当成 false:

if ($isunknown({
      cfg.mon_vif.mon_cb.rsp_valid,
      cfg.mon_vif.mon_cb.rsp_ready
    }))
  `uvm_error("MON_X", "response handshake contains X/Z")

数据字段的 X 策略取决于协议:可以立即 error,也可以把四态值保留到 transaction,由 Scoreboard 进行掩码比较。策略必须显式,不能在赋给二态 bit 时无声丢失 X。

输入 Monitor 和输出 Monitor 的职责不同:

  • Input Monitor 表示 DUT 实际接受的输入,是 Predictor 的事实来源;
  • Output Monitor 表示 DUT 实际产生的输出,是 Scoreboard 的 actual 来源;
  • 两者可以使用不同 transaction 类型和不同事务边界;
  • 不要从 Driver 镜像输入,也不要从 Predictor 生成 actual。

3.8 Agent 的对外接口

Agent 可以直接暴露内部 Monitor 的 analysis port,也可以建立自己的 analysis port 作为 稳定出口。后者能隐藏 Monitor 实例名,使 Environment 不依赖 Agent 内部结构。

class protocol_agent extends uvm_agent;
  uvm_analysis_port #(request_item)  request_ap;
  uvm_analysis_port #(response_item) response_ap;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    request_ap  = new("request_ap", this);
    response_ap = new("response_ap", this);
    // 创建 monitor,以及 Active 模式下的 driver/sequencer
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);

    monitor.request_ap.connect(request_ap);
    monitor.response_ap.connect(response_ap);

    if (cfg.is_active == UVM_ACTIVE)
      driver.seq_item_port.connect(sequencer.seq_item_export);
  endfunction
endclass

Monitor 在 Active 和 Passive 模式下都应存在,因为观察能力是 Agent 复用和检查闭环的 基础。只有 Driver 和 Sequencer 受 is_active 控制。

一个 Agent 通常对应一个协议接口实例,而不是一个 DUT。多端口 DUT 可以创建多个同类型 Agent,每个实例拥有独立配置、virtual interface、ID 空间和 Coverage 维度。

3.9 TLM 接口类型与连接方向

TLM 接口根据通信语义选择:

接口数据方向是否阻塞典型用途
uvm_seq_item_pull_port/exportDriver 从 Sequencer 拉取激励仲裁
uvm_analysis_port一对多广播否,零时间Monitor/ Predictor 发布
uvm_analysis_export转发 analysis 调用FIFO 或层次透传
uvm_analysis_imp终止连接并实现 write()Predictor、Coverage
uvm_tlm_analysis_fifoanalysis 写入、阻塞读取读可阻塞解耦到达时间
uvm_blocking_put/get_port点对点传输有明确生产消费关系的线程

Port 是发起调用的一端,export 转发实现,imp 提供最终方法。Analysis 连接的典型方向是:

publisher.analysis_port.connect(subscriber.analysis_imp);
publisher.analysis_port.connect(fifo.analysis_export);

一个类需要接收多个同类型 analysis stream 时,可以用后缀宏生成多个独立的 write_*()

`uvm_analysis_imp_decl(_expected)
`uvm_analysis_imp_decl(_actual)

class scoreboard extends uvm_scoreboard;
  uvm_analysis_imp_expected #(response_item, scoreboard) expected_imp;
  uvm_analysis_imp_actual   #(response_item, scoreboard) actual_imp;

  function new(string name = "scoreboard",
               uvm_component parent = null);
    super.new(name, parent);
    expected_imp = new("expected_imp", this);
    actual_imp   = new("actual_imp", this);
  endfunction

  function void write_expected(response_item item);
    // 缓存 expected
  endfunction

  function void write_actual(response_item item);
    // 缓存 actual
  endfunction
endclass

uvm_analysis_imp_decl 的生成规则

普通 uvm_analysis_imp #(T, IMP) 最终调用接收组件中的:

function void write(T item);

如果 Scoreboard 同时接收 expected 和 actual,并且两路都是 response_item,两个普通 Analysis Imp 都只能调用同名 write(),接收端无法从回调名称区分数据来源。

uvm_analysis_imp_decl 通过后缀生成新的 Imp 类型和回调函数名:

宏调用生成的 Imp 类型要求接收组件实现
uvm_analysis_imp_decl(_expected)uvm_analysis_imp_expectedwrite_expected()
uvm_analysis_imp_decl(_actual)uvm_analysis_imp_actualwrite_actual()
uvm_analysis_imp_decl(_request)uvm_analysis_imp_requestwrite_request()

后缀通常以 _ 开头,因为宏会直接拼接名称。例如 _expected 最终形成 uvm_analysis_imp_expectedwrite_expected()

宏应声明在接收 class 外部、生成类型被使用之前,通常放在 package 作用域:

package verification_pkg;
  import uvm_pkg::*;
  `include "uvm_macros.svh"

  `uvm_analysis_imp_decl(_expected)
  `uvm_analysis_imp_decl(_actual)

  class scoreboard extends uvm_scoreboard;
    // 此处可以使用 uvm_analysis_imp_expected/actual
  endclass
endpackage

宏调用本身定义的是类型,不会自动:

  • 创建 expected_impactual_imp 实例;
  • 连接 Analysis Port;
  • 复制 transaction;
  • 建立 expected/actual 队列;
  • 执行 Scoreboard 比较。

这些工作仍然分别属于 constructor、Environment 和 Scoreboard。

多路 Analysis Imp 的完整调用链

Environment 连接两条数据流:

predictor.expected_ap.connect(scb.expected_imp);
output_agent.response_ap.connect(scb.actual_imp);

Predictor 发布 expected 时:

predictor.expected_ap.write(expected)

scb.expected_imp

scb.write_expected(expected)

Output Monitor 发布 actual 时:

output_agent.response_ap.write(actual)

scb.actual_imp

scb.write_actual(actual)

write_expected()write_actual() 都是 function,必须在零时间内返回。适合做:

  • clone 或取得只读快照;
  • 推入内部 queue;
  • 写入 mailbox/FIFO;
  • 更新轻量计数;
  • 执行不耗时的即时比较。

不能在其中使用 @#delay 或阻塞等待。

Analysis Imp 与 Analysis FIFO 的选择

需求推荐方式
收到事务后立即分类或入队多个 uvm_analysis_imp_decl
expected/actual 到达时间不同,需要阻塞等待多个 uvm_tlm_analysis_fifo
需要不同的 write_*() 处理策略多个 Analysis Imp
希望在 run phase 中统一取数、对齐和比较Analysis FIFO

使用 Imp 时,Scoreboard 自己管理队列:

function void write_expected(response_item item);
  response_item snapshot;
  if (!$cast(snapshot, item.clone()))
    `uvm_fatal("CLONE", "expected clone failed")
  expected_queue.push_back(snapshot);
endfunction

使用 FIFO 时,由 FIFO 缓存对象句柄:

predictor.expected_ap.connect(
  scb.expected_fifo.analysis_export);

output_agent.response_ap.connect(
  scb.actual_fifo.analysis_export);

无论选择哪一种,都要明确 transaction 所有权。Analysis 通信传递的是对象句柄, 不会自动深拷贝。

使用两个 imp 适合即时入队;使用两个 analysis FIFO 适合在 run phase 中阻塞等待。二者 解决的是同一数据入口问题,不应在没有明确所有权时混用两套缓存。

3.10 连接完整性的检查方法

平台连接完成后,至少检查:

  • Active Agent 的 seq_item_port 已连接;
  • 关键 analysis port 的连接数非零;
  • expected 和 actual 没有接反;
  • Input Monitor 同时连接 Predictor 和 Coverage;
  • Output Monitor 连接 Scoreboard actual 入口;
  • Predictor 输出连接 Scoreboard expected 入口;
  • 多实例 Agent 没有共享错误的 vif 或 config。
function void verification_env::end_of_elaboration_phase(
  uvm_phase phase
);
  super.end_of_elaboration_phase(phase);

  if (pred.expected_ap.size() == 0)
    `uvm_fatal("NO_CONNECT", "predictor expected_ap is unconnected")

  if (output_agent.response_ap.size() == 0)
    `uvm_fatal("NO_CONNECT", "output response stream is unconnected")
endfunction

连接存在只说明类型和拓扑正确;还需要通过单事务定向测试证明 transaction 确实沿每条 路径流动。


4. 生命周期与时间语义

4.1 Phase 是平台生命周期契约

Phase主要工作常见错误
build创建组件、获取配置在此连接 TLM,或漏掉 super.build_phase
connect连接 port/export/imp创建本应在 build 存在的组件
end_of_elaboration打印和检查最终拓扑继续改变核心拓扑
start_of_simulation设置日志、最终静态检查启动永久线程
run驱动、采样、预测、比较忘记结束条件或 objection
extract/check汇总统计、检查余量仍等待新事务
report给出最终 PASS/FAIL 摘要只打印信息而不报告错误

build phase 自顶向下创建,connect phase 在完整层次存在后完成绑定;所有 component 的 run phase 并发执行。不能根据源代码书写顺序推断运行先后。

4.2 Function 与 Task 的时间边界

  • function 不能消耗仿真时间,适合 build/connect、即时 write() 和纯计算;
  • task 可以等待时钟、事件和 FIFO,适合 Driver、Monitor 和阻塞式 Scoreboard;
  • Analysis port 的 write() 是零时间广播,订阅者不应在其中等待;
  • 耗时预测应先入 FIFO,再由独立 task 处理。
function void predictor::write_request(request_item observed);
  request_fifo.write(observed);
endfunction

task predictor::run_phase(uvm_phase phase);
  request_item observed;
  forever begin
    request_fifo.get(observed);
    calculate_prediction(observed);
  end
endtask

4.3 Clocking block、NBA 与周期定义

周期精确平台必须统一三个问题:

  1. Monitor 在哪个边沿采样;
  2. Driver 的输出在该边沿前还是后生效;
  3. Predictor 发布的值对应当前周期还是下一周期。

建议为所有 sample 增加单调周期号,并在文档中明确:

cycle N 采样输入
→ 使用 cycle N 开始前的模型状态生成当前可见 expected
→ 根据 cycle N 的握手计算 next state
→ 提交状态,供 cycle N+1 使用

这种顺序与 RTL 非阻塞赋值的“先读旧值,再统一提交新值”一致。

function void predictor::advance_one_cycle(cycle_sample s);
  model_state_t next_state;
  response_item expected;

  next_state = state;

  expected = response_item::type_id::create("expected");
  expected.data         = state.visible_output;
  expected.sample_cycle = s.sample_cycle;
  expected.reset_epoch  = s.reset_epoch;
  expected_ap.write(expected);

  if (s.reset_active)
    next_state = RESET_STATE;
  else if (s.request_handshake)
    next_state.count = saturating_add(state.count, 1);

  state = next_state;
endfunction

4.4 Reset 是协议事件,不是后台细节

必须在架构阶段定义:

  • reset 同步还是异步;
  • reset 期间 Driver 驱动什么;
  • Monitor 是否发布 reset sample;
  • Predictor 何时清状态;
  • Scoreboard 丢弃、保留还是标记未完成事务;
  • reset 解除后的第一个合法采样周期;
  • reset 前后的相同 ID 是否可能冲突。

使用 reset_epoch 可以把 reset 前后的事务分区,避免旧 expected 与新 actual 错配。 若规格规定 reset 取消所有未完成事务,Scoreboard 应在观察到 reset 后显式 flush,并统计 被取消数量,而不是静默清空。

4.5 Objection、drain 和超时

Sequence 结束只代表最后一个 request 已交给 Driver,不代表 DUT 的最后一个 response 已经被 Monitor 和 Scoreboard 处理。

可靠结束条件通常同时包含:

  • 激励 Sequence 已完成;
  • Scoreboard 已达到预期比较数,或所有 outstanding 已归零;
  • expected/actual FIFO 均为空;
  • 没有等待中的模型任务;
  • 全局 watchdog 未超时。
task base_test::run_phase(uvm_phase phase);
  protocol_sequence seq;

  phase.raise_objection(this);
  seq = protocol_sequence::type_id::create("seq");
  seq.start(env.input_agent.sequencer);

  fork
    env.scb.wait_until_drained();
    begin
      #(test_timeout);
      `uvm_fatal("TIMEOUT", "verification environment did not drain")
    end
  join_any
  disable fork;

  phase.drop_objection(this);
endtask

Timeout 必须报告当前 outstanding、FIFO 深度、最后事务 ID 和最后活动周期,以便区分 “DUT 未响应”“Monitor 未发布”“Predictor 未预测”和“Scoreboard 未匹配”。

4.6 Phase 的执行与遍历顺序

常用 phase 可以分成三组:

类别Phase时间特性
构建build、connect、end_of_elaboration、start_of_simulationfunction,不消耗时间
运行run,或 reset/configure/main/shutdown 等 runtime phasetask,可以消耗时间
收尾extract、check、report、finalfunction,不再等待新数据

Component tree 中的遍历方向很重要:

  • build phase 自顶向下,使父组件可以先写配置,再创建子组件;
  • connect phase 自底向上,使子组件已经存在后再由父层连接;
  • end_of_elaboration/start_of_simulation 用于最终静态检查;
  • run phase 中所有 component 线程并发;
  • check/report 在运行结束后执行,不应继续等待 transaction。
function void verification_env::build_phase(uvm_phase phase);
  super.build_phase(phase);
  input_agent = protocol_agent::type_id::create("input_agent", this);
  pred        = predictor::type_id::create("pred", this);
  scb         = scoreboard::type_id::create("scb", this);
endfunction

function void verification_env::connect_phase(uvm_phase phase);
  super.connect_phase(phase);
  input_agent.request_ap.connect(pred.request_imp);
  pred.expected_ap.connect(scb.expected_fifo.analysis_export);
endfunction

不要通过源文件中的代码先后顺序推断 phase 顺序。UVM scheduler 根据 component tree 和 phase 规则调用回调。

UVM 还提供 reset、configure、main、shutdown 等 runtime phase。团队应选择一种统一策略:

  • 简单 block-level 平台只使用 run phase,并在内部明确 reset/traffic/drain 顺序;
  • 大型平台使用 runtime phase 分离复位、配置、主流量和收尾;
  • 不要一部分组件只用 run phase、另一部分依赖 main phase objection,却没有统一约定。

4.7 线程并发与 fork/join

Driver、Monitor、Predictor 和 Scoreboard 的 run phase 同时执行。一个 component 内也常用 多个永久线程处理独立通道:

task protocol_monitor::run_phase(uvm_phase phase);
  fork
    monitor_request_channel();
    monitor_response_channel();
    monitor_reset();
  join
endtask

fork...joinjoin_anyjoin_none 的差异:

形式父线程何时继续
join所有子线程结束
join_any任一子线程结束
join_none启动子线程后立即继续

join_any 通常与 disable fork 配合实现“完成条件与 timeout 竞争”。必须注意 disable fork 会终止当前作用域中仍运行的所有子线程,最好用局部命名 block 限定范围:

fork : wait_or_timeout
  begin
    env.scb.wait_until_drained();
  end
  begin
    #(test_timeout);
    `uvm_fatal("TIMEOUT", "scoreboard did not drain")
  end
join_any
disable wait_or_timeout;

永久后台线程通常不 raise objection,由 Test 或负责顶层场景的 Virtual Sequence 控制 测试寿命。否则 Driver/Monitor 的永久循环会使仿真永不结束。

4.8 Analysis write、Delta Cycle 与阻塞边界

Analysis port 的 write() 是 function 调用,在当前时间和调用栈中依次通知订阅者。 它不创建新仿真线程,也不允许订阅者等待时钟。

这带来三个工程结论:

  1. write() 中只做快速复制、入队或纯函数预测;
  2. 耗时处理通过 FIFO 交给 run phase;
  3. 不能依赖订阅者调用顺序实现功能。
function void write_request(request_item observed);
  request_item snapshot;

  if (!$cast(snapshot, observed.clone()))
    `uvm_fatal("CLONE", "request clone failed")

  request_fifo.write(snapshot);
endfunction

task run_phase(uvm_phase phase);
  request_item observed;

  forever begin
    request_fifo.get(observed);
    run_expensive_model(observed);
  end
endtask

如果 Monitor 在 clocking block 采样后立即调用 write(),Predictor 和 Coverage 会在同一 仿真时间收到 transaction。Transaction 中的 sample_cycle$time 更适合判断 跨时钟或同一时间多个采样点的顺序。

4.9 Reset 状态传播

仅在各组件中分别读取 rst_n,容易产生 reset 清理周期不一致。常用方案有:

  • Monitor 在每个 transaction 中携带 reset_epoch
  • 独立 Reset Monitor 发布 reset event;
  • Environment 把 reset event 广播给 Predictor、Scoreboard 和 Coverage;
  • runtime reset phase 统一控制有明确 phase 支持的平台。
class reset_event extends uvm_sequence_item;
  bit asserted;
  longint unsigned sample_cycle;
  int unsigned reset_epoch;

  `uvm_object_utils(reset_event)
endclass

Scoreboard reset 时的处理策略必须来自规格:

规格语义Scoreboard 行为
reset 取消所有未完成事务flush,并记录 cancelled
reset 后仍必须返回响应保留 outstanding,继续等待
reset 前后 ID 空间重启用 reset epoch 扩展匹配键
reset 期间输出无效Monitor 不发布 actual,但 SVA 检查静默要求

Predictor 和 Scoreboard 必须依据同一个 reset 事件更新状态。若各自独立轮询信号,可能在 同一 reset 边界上相差一个周期。

4.10 Drain Time 的适用范围

UVM phase drain time 可以在最后一个 objection drop 后额外等待一段时间,但它不是正确 完成条件的替代品:

phase.phase_done.set_drain_time(this, 200ns);

固定 drain time 的问题是:

  • 太短会丢掉慢响应;
  • 太长会浪费每个测试的运行时间;
  • DUT 丢响应时仍不能解释缺少哪个事务;
  • 不同配置下最大延迟可能不同。

优先使用 Scoreboard 的 outstanding/drained 条件,固定 drain time 只作为已知管线尾部的 安全余量。无论哪种方法,都必须配套全局 timeout。


5. 检查闭环:预测、对齐与完整性

5.1 Predictor 的独立性

Predictor 回答的是:

在已经观察到这些输入和配置的前提下,按照规格应该产生什么输出?

为了避免 DUT 与模型出现同源错误,Predictor 应尽量满足:

  • 从规格、公式或独立算法推导,不翻译 RTL 语句;
  • 不读取 DUT 内部状态或 actual 输出;
  • 输入来自 Monitor,而不是 Sequence;
  • 功能算法与时序适配分层;
  • 对 reset、饱和、边界和非法输入有显式策略;
  • 可以由少量手算向量独立校准。

推荐把模型拆成两层:

observed request

功能 Oracle:输入值 → 正确结果

时序适配器:延迟、顺序、窗口、reset、有效周期

expected response

功能 Oracle 可以是纯 SystemVerilog 函数、C/C++、Python 或其他可信模型。时序适配器 留在 UVM 中,负责把功能结果映射到协议可观察的时间和事务边界。

function automatic bit [31:0] golden_function(
  operation_e op,
  bit [31:0]  lhs,
  bit [31:0]  rhs
);
  case (op)
    READ_OP:  return lhs;
    WRITE_OP: return lhs ^ rhs;
    default:  return 'x;
  endcase
endfunction

5.2 Predictor 的接收与发布

即时、零时间的预测可以用 analysis imp 直接接收;耗时模型、外部模型或需要阻塞等待的 模型应使用 analysis FIFO 解耦。

`uvm_analysis_imp_decl(_request)

class predictor extends uvm_component;
  `uvm_component_utils(predictor)

  uvm_analysis_imp_request #(request_item, predictor) request_imp;
  uvm_analysis_port #(response_item) expected_ap;

  function new(string name = "predictor",
               uvm_component parent = null);
    super.new(name, parent);
    request_imp = new("request_imp", this);
    expected_ap = new("expected_ap", this);
  endfunction

  function void write_request(request_item observed);
    response_item expected;

    expected = response_item::type_id::create("expected");
    expected.data           = predict_data(observed);
    expected.transaction_id = observed.transaction_id;
    expected.reset_epoch    = observed.reset_epoch;
    expected.sample_cycle   = expected_cycle(observed);

    expected_ap.write(expected);
  endfunction
endclass

如果一个输入驱动多个独立功能,可以使用多个 Predictor。每个 Predictor 只负责一个明确 契约,Scoreboard 汇总各路 expected。这样单个模型不会演变成难以维护的“大而全”类。

5.3 Scoreboard 先选择匹配策略

Scoreboard 的第一项架构决策不是比较函数,而是 expected 与 actual 如何配对。

DUT 行为推荐策略必要元数据
严格顺序、无丢弃双 FIFO 顺序匹配reset epoch,可选周期号
固定或可变延迟、仍顺序FIFO + 延迟检查输入/输出周期号
支持乱序响应ID/tag 关联数组唯一 transaction ID
一个输入产生多个输出每 ID 的队列或计数器ID、beat index、last
输出可能被取消outstanding 状态机ID、取消原因、reset epoch

不要在确认顺序保证之前默认使用 FIFO。错误的匹配策略会把平台问题伪装成 DUT 数据错误。

5.4 顺序 Scoreboard

uvm_tlm_analysis_fifo 可以把零时间 analysis 广播转换成阻塞式消费线程,使 expected 和 actual 独立到达。

class scoreboard extends uvm_scoreboard;
  `uvm_component_utils(scoreboard)

  uvm_tlm_analysis_fifo #(response_item) expected_fifo;
  uvm_tlm_analysis_fifo #(response_item) actual_fifo;

  int unsigned compared_count;
  int unsigned mismatch_count;

  function new(string name = "scoreboard",
               uvm_component parent = null);
    super.new(name, parent);
    expected_fifo = new("expected_fifo", this);
    actual_fifo   = new("actual_fifo", this);
  endfunction

  task run_phase(uvm_phase phase);
    response_item expected;
    response_item actual;

    forever begin
      expected_fifo.get(expected);
      actual_fifo.get(actual);

      if (expected.reset_epoch != actual.reset_epoch)
        `uvm_fatal("RESET_SKEW", "expected/actual reset epoch mismatch")

      if (expected.transaction_id != actual.transaction_id)
        `uvm_error("ID_SKEW", $sformatf(
          "expected id=%0d actual id=%0d",
          expected.transaction_id, actual.transaction_id))

      compare_one(expected, actual);
    end
  endtask
endclass

如果平台按周期发布 sample,每个 Predictor 对每个输入 sample 都应发布一个 prediction, 即使该周期功能无效;否则其中一路少一个对象,就会让后续所有周期错位。周期号检查应在 数据比较前进行。

5.5 乱序 Scoreboard

乱序 DUT 应使用 ID/tag 建立 outstanding 集合,而不是等待“下一个 expected”:

response_item expected_by_id[int unsigned];

function void write_expected(response_item expected);
  if (expected_by_id.exists(expected.transaction_id))
    `uvm_fatal("DUP_ID", "duplicate expected transaction id")

  expected_by_id[expected.transaction_id] = expected;
endfunction

function void write_actual(response_item actual);
  response_item expected;

  if (!expected_by_id.exists(actual.transaction_id)) begin
    `uvm_error("UNEXPECTED_RSP", $sformatf(
      "no expected item for id=%0d", actual.transaction_id))
    return;
  end

  expected = expected_by_id[actual.transaction_id];
  expected_by_id.delete(actual.transaction_id);
  compare_one(expected, actual);
endfunction

当 ID 可以复用时,必须把 generation 或 reset epoch 纳入键值;多 beat 响应还需要检查 beat index、last 和总 beat 数。

5.6 比较应产生可诊断证据

一次 mismatch 至少应报告:

  • transaction ID、reset epoch;
  • expected/actual 的关键字段;
  • 输入采样周期和输出采样周期;
  • 预期延迟与实际延迟;
  • 相关配置或模式;
  • 前后若干事务的摘要。
function void scoreboard::compare_one(
  response_item expected,
  response_item actual
);
  compared_count++;

  if (!actual.compare(expected)) begin
    mismatch_count++;
    `uvm_error("MISMATCH", $sformatf(
      "id=%0d epoch=%0d exp_cycle=%0d act_cycle=%0d\nEXP %s\nACT %s",
      expected.transaction_id,
      expected.reset_epoch,
      expected.sample_cycle,
      actual.sample_cycle,
      expected.convert2string(),
      actual.convert2string()))
  end
endfunction

只打印“expected != actual”会显著增加波形定位成本。

5.7 对象所有权和快照

UVM TLM 传递的是句柄。发布端在 write() 返回后如果还会修改对象,必须发布 clone; 接收端如果需要长期保存,也应明确是否取得独立副本。

request_item snapshot;

if (!$cast(snapshot, observed.clone()))
  `uvm_fatal("CLONE", "request_item clone failed")

request_ap.write(snapshot);

工程中应为每条 analysis stream 写清所有权规则:

行为安全条件
发布新对象后不再修改可直接广播
发布端复用工作对象广播前 clone
多订阅者只读可共享同一快照
任一订阅者会修改订阅者先 clone
FIFO 长期保存确认上游不会继续修改

5.8 结束时的完整性检查

Scoreboard 的 check_phase 不只检查 mismatch 数,还要证明没有遗漏:

function void scoreboard::check_phase(uvm_phase phase);
  super.check_phase(phase);

  if (expected_fifo.used() != 0)
    `uvm_error("EXP_REMAINDER", $sformatf(
      "%0d expected items remain", expected_fifo.used()))

  if (actual_fifo.used() != 0)
    `uvm_error("ACT_REMAINDER", $sformatf(
      "%0d actual items remain", actual_fifo.used()))

  if (expected_by_id.num() != 0)
    `uvm_error("OUTSTANDING", $sformatf(
      "%0d transactions remain unmatched", expected_by_id.num()))

  if (compared_count == 0)
    `uvm_error("NO_CHECK", "no transaction was compared")
endfunction

“零错误但零比较”不是通过。最终 PASS 条件至少应包含:有检查发生、无 mismatch、 无 unexpected actual、无 outstanding、无 FIFO remainder、无 timeout。

5.9 无状态、有状态与周期精确模型

参考模型可以按状态复杂度分层:

模型类型特征实现重点
无状态每个输出只依赖当前输入纯函数、输入输出一一对应
事务级有状态依赖历史 transaction队列、寄存器镜像、窗口状态
周期精确依赖每周期握手和旧状态cycle sample、next-state、流水延迟
并发/乱序多事务同时在途ID、资源模型、完成顺序

无状态模型适合直接在 write() 中预测:

function void predictor::write_request(request_item observed);
  response_item expected;

  expected = response_item::type_id::create("expected");
  expected.data = golden_transform(observed.data);
  expected.transaction_id = observed.transaction_id;
  expected.reset_epoch = observed.reset_epoch;
  expected_ap.write(expected);
endfunction

有状态模型应明确区分旧状态和下一状态:

function void predictor::accept_request(request_item observed);
  model_state_t next_state;
  response_item expected;

  next_state = state;
  expected = predict_from_old_state(state, observed);
  update_next_state(next_state, observed);

  expected.transaction_id = observed.transaction_id;
  expected.reset_epoch = observed.reset_epoch;
  expected_ap.write(expected);

  state = next_state;
endfunction

不要边计算 expected 边直接修改多个状态变量,否则条件优先级和 RTL 的非阻塞赋值语义 容易错位。

5.10 流水线和可变延迟

功能结果正确但出现周期偏移,仍然是时序错误。模型需要区分:

  • result value;
  • earliest/expected/latest completion cycle;
  • valid 是否连续;
  • backpressure 是否暂停输出;
  • pipeline flush 条件。

固定延迟可以在 expected 中记录目标周期:

expected.sample_cycle =
  observed.sample_cycle + configured_pipeline_latency;

可变延迟应记录允许窗口,而不是让 Scoreboard 完全忽略时间:

expected.earliest_cycle = observed.sample_cycle + min_latency;
expected.latest_cycle   = observed.sample_cycle + max_latency;

Scoreboard 收到 actual 时检查:

if (actual.sample_cycle < expected.earliest_cycle ||
    actual.sample_cycle > expected.latest_cycle)
  `uvm_error("LATENCY", "response arrived outside allowed window")

如果输出受 ready 反压,功能模型可以先产生“可用结果”,时序适配器再根据观察到的 ready/valid 计算实际可见周期。不要把反压逻辑混进独立算法 Oracle。

5.11 多 Predictor 与周期锁步

一个输出 transaction 可能包含多个相互独立的指标。可以让多个 Predictor 订阅同一 Input Monitor:

input_agent.request_ap.connect(data_predictor.request_imp);
input_agent.request_ap.connect(status_predictor.request_imp);

data_predictor.expected_ap.connect(scb.data_expected_fifo.analysis_export);
status_predictor.expected_ap.connect(scb.status_expected_fifo.analysis_export);

周期级平台中,每个 Predictor 对每个 sample 都应产生一份 prediction,包括“本周期无效” 这一状态。否则某一路跳过一个 sample,后续 FIFO 会整体错位。

forever begin
  actual_fifo.get(actual);
  data_expected_fifo.get(data_expected);
  status_expected_fifo.get(status_expected);

  if (actual.sample_cycle != data_expected.sample_cycle || actual.sample_cycle != status_expected.sample_cycle)
    `uvm_fatal("PREDICT_SKEW", "prediction streams are not aligned")

  compare_data(actual, data_expected);
  compare_status(actual, status_expected);
end

事务级 Predictor 不必每周期发布,但必须对每个已接受输入明确产生、取消或合并 expected,不能静默漏掉。

5.12 掩码、四态值和比较策略

并非所有输出 bit 在所有模式下都有效。Scoreboard 可以使用显式 compare mask:

function bit masked_equal(
  logic [31:0] actual,
  logic [31:0] expected,
  logic [31:0] compare_mask
);
  return ((actual ^ expected) & compare_mask) === '0;
endfunction

使用 ===== 还是掩码比较必须依据验证意图:

策略适用场景
==二态数据且 X 应传播为未知结果
===X/Z 也是被检查值的一部分
mask compare协议明确存在 don’t-care 字段
uvm_comparer结构化 transaction 和统一报告

不要为了让测试通过而把所有 X 位都 mask 掉。控制信号的 X 通常应由 SVA 或 Monitor 立即报告,数据 don’t-care 则应由规格给出明确 mask。

5.13 Reference Model、Scoreboard 与 RAL Predictor

三者都可能被称为 Predictor,但职责不同:

组件输入输出/状态用途
Functional Predictor已观察功能输入expected transaction预测 DUT 功能输出
Scoreboardexpected + actualPASS/FAIL 与完整性统计对齐和比较
RAL Predictor已观察总线访问register mirror 更新维护寄存器抽象模型

Functional Predictor 不应直接修改 Scoreboard 的内部队列,应该通过 analysis port 发布。 RAL Predictor 不应代替功能模型,它只解释寄存器访问对 mirror 的影响。

5.14 Checker 的计数守恒

可以用计数守恒快速发现平台漏数:

observed_input
  = predicted
  + cancelled_by_reset
  + intentionally_filtered

predicted
  = compared
  + expected_outstanding

observed_output
  = compared
  + unexpected_actual

report phase 应打印这些计数,并验证等式成立。单独检查 FIFO 是否为空,无法发现上游 Monitor 根本没有发布 transaction 的情况。


6. 场景设计与验证收敛

6.1 Test、Sequence 与 Environment 的分工

建议建立稳定的层次:

base_test
  ├── 创建配置与 environment
  ├── 提供统一 timeout、reset 和 drain 策略
  └── 派生 feature_test
        └── 启动一个或多个 scenario sequence

Base Test 固化平台政策;派生 Test 只改变配置、Factory Override 和场景选择。Sequence 应组合 transaction,而不是修改 Environment 内部状态。

6.2 Sequence 分层

层次目的示例
单事务 Sequence产生一个合法操作单次读、单次写
流量 Sequence产生一类分布连续传输、随机空闲
场景 Sequence制造明确边界反压、reset 中断、满 outstanding
Virtual Sequence协调多个 Agent配置接口与数据接口并发

随机测试之前应先有可手算的定向场景。定向场景可以验证平台本身是否按照预期采样、 预测和比较。

6.3 必备场景矩阵

维度最小场景
长度最小、典型、最大、边界相邻值
数据全零、全一、单 bit、交替位、随机
握手无反压、短反压、长反压、周期性交错
reset空闲时、请求前、事务中、最后一拍、响应等待中
并发0、1、最大 outstanding
顺序顺序返回、允许范围内乱序
延迟最小、最大、边界外超时
非法行为X/Z、非法编码、重复 ID、缺失 last

多 beat 事务必须明确:

  • 长度字段表示 beat 数还是最后 beat 索引;
  • 是否应发送 lenlen + 1 个 beat;
  • 只有握手成功的 beat 才计数;
  • last 必须与最终有效握手同时成立;
  • reset 或错误响应如何终止剩余 beat。

6.4 SVA、Sanity、Scoreboard 与 Coverage 的职责

机制主要回答适合检查
SVA每个周期是否遵守局部规则?握手稳定、时序窗口、one-hot、禁止条件
Sanity平台是否真的在工作?接口非空、计数非零、无 X、连接存在
Scoreboard端到端功能结果是否正确?数据、顺序、延迟、丢失、重复
Functional Coverage目标场景是否到达?模式、边界、组合、状态迁移
Code CoverageRTL 结构是否被执行?line、branch、toggle、FSM

这些机制互补,不能互相替代。Coverage 命中只说明场景发生,不说明结果正确; Scoreboard 全部通过也不说明所有计划场景都已覆盖。

6.5 Assertion 放在协议边界

局部时序性质应尽量靠近 Interface:

property p_request_stable_under_backpressure;
  @(posedge clk) disable iff (!rst_n)
    req_valid && !req_ready
    |=> req_valid && $stable(req_data);
endproperty

a_request_stable_under_backpressure:
  assert property (p_request_stable_under_backpressure);

SVA 可以比 transaction Monitor 更早指出错误周期;Monitor 和 Scoreboard 则负责重建 跨多个周期的事务含义。

6.6 Coverage 从验证计划生成

Coverage 应采样实际观察到的 transaction,而不是随机化成功的 request。否则 Driver 错误、reset 中断和反压未握手仍可能被错误计为覆盖。

covergroup request_cg with function sample(request_item tr);
  cp_op: coverpoint tr.op;

  cp_address_region: coverpoint tr.address {
    bins low  = {[32'h0000_0000:32'h0000_0FFF]};
    bins high = {[32'hFFFF_F000:32'hFFFF_FFFF]};
  }

  cp_epoch: coverpoint tr.reset_epoch;

  op_x_region: cross cp_op, cp_address_region;
endgroup

每个 coverpoint 和 cross 都应能回指验证计划中的风险。无法解释用途的覆盖项只会增加 数据库和收敛成本。

6.7 独立 Oracle 与 Mutation Testing

参考模型正确性不能仅靠“与 DUT 一致”证明。至少采用以下方法:

  1. 用手算定向向量校准 Predictor;
  2. 对边界值建立独立期望表;
  3. 对关键算法使用不同实现方式交叉检查;
  4. 故意注入错误,确认检查器必然失败;
  5. 检查错误是否在合理位置被报告。

可注入的 mutation 包括:

  • 把最大长度改为最大长度减一;
  • 忽略 ready,只按 valid 计数;
  • >= 改成 >
  • 漏掉最后一个 beat;
  • 交换两个字段;
  • 延迟偏移一个周期;
  • reset 时不清某个状态。

如果 mutation 后测试仍通过,应检查激励是否到达、Monitor 是否采样、Predictor 是否独立、 Scoreboard 是否比较以及 Coverage 是否只统计了“想发送”的事务。

6.8 收敛标准

回归完成不等于验证收敛。建议同时满足:

  • 验证计划中的检查点均有实现和测试;
  • 所有必需定向场景稳定通过;
  • 随机回归在规定 seeds 和配置矩阵下无错误;
  • 功能覆盖达到目标,未命中项有分析或 waiver;
  • 代码覆盖缺口完成归因;
  • Assertion 无未解释失败;
  • Scoreboard 无 remainder、unexpected 或零比较;
  • Mutation Testing 能检出代表性错误;
  • 已知限制、假设和未验证区域被记录。

6.9 Constraint 的分层策略

约束不是为了“让数据随机”,而是为了在合法空间中表达场景。建议分三层:

层次放置位置作用
协议合法约束Transaction永远合法的字段关系
默认分布约束Base Sequence常规流量概率
场景约束派生 Sequence / inline constraint定向边界或异常组合
class constrained_request_item extends request_item;
  `uvm_object_utils(constrained_request_item)

  function new(string name = "constrained_request_item");
    super.new(name);
  endfunction

  constraint c_alignment {
    address[1:0] == 2'b00;
  }

  constraint c_beats {
    beat_count inside {[1:16]};
  }

  constraint c_default_idle {
    soft idle_cycles inside {[0:10]};
  }

  constraint c_idle_distribution {
    idle_cycles dist {
      0       := 50,
      [1:3]   := 35,
      [4:10]  := 15
    };
  }
endclass

soft 约束允许场景层覆盖默认值。Inline constraint 用于单次或单场景收窄:

constrained_request_item req;

req = constrained_request_item::type_id::create("req");
start_item(req);
if (!req.randomize() with {
      op == WRITE_OP;
      beat_count == 16;
      idle_cycles == 0;
    })
  `uvm_fatal("RAND", "maximum burst request could not be randomized")
finish_item(req);

相关变量有求解依赖时可以使用 solve ... before ...,但它只影响分布,不改变合法解 集合。约束冲突必须 fatal,不能继续发送带默认值的 transaction。

随机测试需要记录 seed、test name、关键配置和 override。只有能用相同 seed 重现, 随机失败才具备工程价值。

6.10 Base Test 与派生 Test

Base Test 应集中处理所有测试都需要的装配工作:

  • 创建 Environment;
  • 构造输入/输出 Agent 配置;
  • 获取并分发 virtual interface;
  • 设置默认 timeout;
  • 打印 topology 和配置摘要;
  • 提供统一 drain/PASS 判断。

派生 Test 只表达差异:

class backpressure_test extends base_test;
  `uvm_component_utils(backpressure_test)

  virtual function void configure_environment();
    super.configure_environment();
    output_cfg.backpressure_enable = 1;
    output_cfg.max_stall_cycles = 20;
  endfunction

  task run_phase(uvm_phase phase);
    backpressure_sequence seq;

    phase.raise_objection(this);
    seq = backpressure_sequence::type_id::create("seq");
    seq.start(env.input_agent.sequencer);
    wait_for_verification_drain();
    phase.drop_objection(this);
  endtask
endclass

Base Test 提供 configure_environment() hook,并在创建 Env 前调用:

virtual function void configure_environment();
  // Derived tests override configuration only.
endfunction

第 2.9 节的 Base Test 在发布配置、创建 Env 前调用这个 hook,因此派生 Test 的策略一定 在 Agent 获取配置前生效。不要在 Environment 已完成 build 后 再修改决定组件是否创建的字段,例如 is_active

6.11 Coverage Collector 的结构

Coverage Collector 应是被动组件,通过 analysis imp 接收 Monitor transaction。

`uvm_analysis_imp_decl(_request_cov)
`uvm_analysis_imp_decl(_response_cov)

class coverage_collector extends uvm_component;
  `uvm_component_utils(coverage_collector)

  uvm_analysis_imp_request_cov #(
    request_item, coverage_collector
  ) request_imp;

  uvm_analysis_imp_response_cov #(
    response_item, coverage_collector
  ) response_imp;

  request_item request_sample;
  response_item response_sample;

  covergroup request_cg;
    option.per_instance = 1;
    cp_op: coverpoint request_sample.op;
    cp_beats: coverpoint request_sample.beat_count {
      bins minimum = {1};
      bins middle  = {[2:15]};
      bins maximum = {16};
    }
    op_x_beats: cross cp_op, cp_beats;
  endgroup

  function new(string name = "coverage_collector",
               uvm_component parent = null);
    super.new(name, parent);
    request_imp = new("request_imp", this);
    response_imp = new("response_imp", this);
    request_cg = new();
  endfunction

  function void write_request_cov(request_item item);
    request_sample = item;
    request_cg.sample();
  endfunction

  function void write_response_cov(response_item item);
    response_sample = item;
  endfunction
endclass

option.per_instance = 1 使多个 Agent 的覆盖率可以分别观察。是否最终合并,应由验证计划 决定:不同端口有独立风险时不能只看合并百分比。

6.12 Coverpoint、Bins 与 Cross

Coverage 设计应优先表达边界和风险:

coverpoint request_sample.idle_cycles {
  bins no_gap    = {0};
  bins short_gap = {[1:3]};
  bins long_gap  = {[4:10]};
  illegal_bins unsupported = {[11:$]};
}

常用 bins 类型:

类型用途
explicit bins规格中的离散模式
range bins长度、地址区域和延迟分段
transition bins状态或模式转换
wildcard bins位模式分类
ignore_bins合法但不属于当前验证目标
illegal_bins仿真中不应出现的采样值

illegal_bins 不能代替协议 Assertion,因为它只在 covergroup sample 时检查。局部周期 非法条件仍应使用 SVA。

Cross 只覆盖真正有交互风险的维度。例如 reset × outstanding × backpressure 有意义, 把所有 coverpoint 全量交叉通常会产生无法收敛的大量 bins。

6.13 覆盖率收敛分析

未命中 bin 需要分类,而不是盲目增加 seed:

原因处理
激励不可达修复约束或 Sequence
DUT 配置未开启增加配置场景
Monitor 未采样修复观测路径
规格不可能提供依据并 waiver
随机概率过低增加定向 Test 或调整 dist
功能已删除更新验证计划和 Coverage

覆盖率数据库合并前应确保 test 配置兼容,避免把不同参数下互斥的 bins 合并成虚假的 “全覆盖”。

6.14 Sanity 与负向测试

Sanity 检查用于证明平台自身有活动:

  • 至少观察一个输入 transaction;
  • 至少生成一个 expected;
  • 至少观察一个 actual;
  • 至少完成一次比较;
  • 关键 analysis port 有连接;
  • 关键配置和 virtual interface 非空。

负向测试用于证明检查器能失败:

  • Driver 违反稳定性时 SVA 失败;
  • DUT 输出改一 bit 时 Scoreboard mismatch;
  • Predictor 少发布一个 expected 时 remainder 或 timeout;
  • actual 使用错误 ID 时 unexpected response;
  • 复位中遗留 outstanding 时 check phase 报错。

Sanity 与 mutation 都通过后,回归 PASS 才更可信。

6.15 回归的组织与可重现性

回归不是简单地重复运行同一个 Test,而是执行明确的测试矩阵:

test × seed × DUT configuration × protocol mode

一个可维护的 test list 至少记录:

字段用途
test name选择 UVM Test
seed重现约束随机结果
configuration位宽、模式、功能开关
timeout防止单任务永久占用资源
expected result正常通过或预期报错
tagssmoke、reset、error、coverage 等分类

常见分层:

  • Smoke:少量定向 Test,快速检查编译、连接和基本数据流;
  • Feature:每个功能点的定向与随机 Test;
  • Daily:稳定测试集乘以多个 seed;
  • Full:完整配置矩阵、大量 seed 和覆盖率合并;
  • Negative:故意制造非法行为,预期特定 Assertion 或 checker 失败。

每个失败必须保留 test、seed、编译版本、配置、override、仿真命令和首个错误。修复后先用 原 seed 单独重现,再运行相关小回归,最后进入全量回归。

回归汇总不能只读取进程退出码。至少解析:

  • UVM_ERROR/UVM_FATAL 数;
  • Scoreboard compared/mismatched/outstanding;
  • Assertion failure;
  • timeout;
  • 是否命中预期负向错误;
  • Coverage 数据是否有效生成。

同一根因可能导致大量后续 mismatch。失败聚类应优先使用首个 error 的 report ID、 transaction ID 和时间,而不是简单统计全部日志行。


7. 工程扩展机制

7.1 Factory Override:替换实现而不改拓扑

Factory 的价值不是省略 new,而是在保持 Environment 结构不变时替换行为。

class error_injection_driver extends protocol_driver;
  `uvm_component_utils(error_injection_driver)

  // 只覆盖需要改变的驱动策略
endclass

function void error_test::build_phase(uvm_phase phase);
  protocol_driver::type_id::set_type_override(
    error_injection_driver::get_type());
  super.build_phase(phase);
endfunction

Override 必须在目标对象被 factory 创建前设置。若组件直接使用 new,type override 不会生效。工程中应打印 factory override 和最终 topology,确认替换对象确实存在。

7.2 Instance Override 的使用边界

同类型多个 Agent 只有一个实例需要替换时,可以使用 instance override。路径必须以最终 组件层次为准;路径变化会让 override 静默失效,因此应配套 topology 检查。

优先顺序建议:

  1. 配置字段可以表达的变化,用 config;
  2. 整个类型行为变化,用 type override;
  3. 只有单个实例特殊时,才用 instance override。

Type Override、Instance Override 与创建时机

Type override 影响该基础类型之后的所有 Factory 创建:

protocol_driver::type_id::set_type_override(
  error_injection_driver::get_type(),
  1  // replace existing override
);

Instance override 只匹配完整实例路径:

uvm_factory::get().set_inst_override_by_type(
  protocol_driver::get_type(),
  error_injection_driver::get_type(),
  "uvm_test_top.env.input_agent.driver"
);

Override 的解析发生在 type_id::create() 时,因此必须满足:

  • 基础类型和替换类型均已注册;
  • 替换类型与基础类型继承兼容;
  • override 在目标 create 之前设置;
  • 目标组件使用 Factory 创建,而不是直接 new
  • instance path 与最终 component tree 完全一致。

Transaction 也可以 override。例如把普通 request 替换为更强约束或错误注入 request, 但派生类不能破坏 Driver 依赖的字段契约。

Factory 调试

function void base_test::end_of_elaboration_phase(uvm_phase phase);
  super.end_of_elaboration_phase(phase);
  uvm_factory::get().print();
  uvm_top.print_topology();
endfunction

Factory 表回答“create 请求最终解析成什么类型”,Topology 回答“最终创建了哪个实例”。 两者应一起检查。

常见 override 失效原因:

现象原因
Factory 表中没有 override设置代码未执行或执行太晚
Factory 表正确,Topology 仍是旧类目标使用 new 创建
Type override 影响范围过大应改用 instance override 或 config
Instance override 不生效实例路径或实例名不匹配
派生类创建但行为不变被覆盖的方法不是 virtual,或派生类未 override 正确方法

7.3 Virtual Sequence 与多 Agent 协调

Virtual Sequence 只协调多个独立 sequencer,不应承担底层协议驱动。

class system_virtual_sequence extends uvm_sequence;
  `uvm_object_utils(system_virtual_sequence)

  protocol_sequencer control_sqr;
  protocol_sequencer data_sqr;

  task body();
    control_sequence cfg_seq;
    protocol_sequence data_seq;

    cfg_seq  = control_sequence::type_id::create("cfg_seq");
    data_seq = protocol_sequence::type_id::create("data_seq");

    cfg_seq.start(control_sqr);

    fork
      data_seq.start(data_sqr);
      run_background_traffic();
    join
  endtask
endclass

如果 sequencer 数量较少,可以由 Test 显式注入句柄;复杂 SoC 平台可使用 virtual sequencer 集中保存句柄。无论采用哪种方式,都要避免在 Sequence 内通过全局路径查找 组件。

Virtual Sequencer 的连接

Virtual Sequencer 不驱动 transaction,它只保存多个真实 Sequencer 的句柄:

class system_virtual_sequencer extends uvm_sequencer;
  `uvm_component_utils(system_virtual_sequencer)

  protocol_sequencer control_sqr;
  protocol_sequencer data_sqr;

  function new(string name = "system_virtual_sequencer",
               uvm_component parent = null);
    super.new(name, parent);
  endfunction
endclass

Environment 在 connect phase 注入真实句柄:

function void verification_env::connect_phase(uvm_phase phase);
  super.connect_phase(phase);

  virtual_sqr.control_sqr = control_agent.sequencer;
  virtual_sqr.data_sqr    = data_agent.sequencer;
endfunction

Virtual Sequence 可以声明预期的 Virtual Sequencer 类型:

class coordinated_sequence extends uvm_sequence;
  `uvm_object_utils(coordinated_sequence)
  `uvm_declare_p_sequencer(system_virtual_sequencer)

  task body();
    control_sequence control_seq;
    protocol_sequence data_seq;

    if (p_sequencer.control_sqr == null ||
        p_sequencer.data_sqr == null)
      `uvm_fatal("NO_SQR", "virtual sequencer handles are incomplete")

    control_seq = control_sequence::type_id::create("control_seq");
    data_seq    = protocol_sequence::type_id::create("data_seq");

    control_seq.start(p_sequencer.control_sqr);

    fork
      data_seq.start(p_sequencer.data_sqr);
      start_background_responses();
    join
  endtask
endclass

uvm_declare_p_sequencer 会在 Sequence 启动时进行类型转换。如果 Sequence 启动在错误 类型的 Sequencer 上会失败,因此 Test 应明确:

virtual_seq.start(env.virtual_sqr);

对于只有两个固定 Sequencer 的小平台,直接给 Virtual Sequence 设置句柄可能更清晰; Virtual Sequencer 适合层次稳定、需要大量跨接口场景的环境。

多 Agent 场景的同步原则

跨 Agent 协调应使用可观察事件,而不是固定延迟猜测:

  • 等配置写响应完成后再启动数据流;
  • 等 Monitor 或 Scoreboard 观察到状态,而不是只等 Sequence 返回;
  • 使用 event、FIFO、状态查询或 RAL mirror 作为同步条件;
  • 所有等待都要有 timeout;
  • 背景 Sequence 需要明确停止机制。

Sequence 返回只表示 Driver 已处理完 item,不一定表示 DUT 状态已经更新或输出已经可见。

7.4 RAL 的集成位置

RAL 解决寄存器抽象、地址映射、访问策略和镜像问题,它不是总线 Driver 的替代品。

register sequence
  ↓ uvm_reg read/write
adapter:uvm_reg_bus_op ↔ request_item

protocol sequencer/driver

DUT register interface

bus monitor → uvm_reg_predictor → mirrored value

RAL 集成必须明确:

  • register block 和 address map 的构建、锁定与 reset;
  • adapter 如何映射 byte enable、status 和 operation;
  • frontdoor 使用哪个 sequencer;
  • backdoor 路径是否稳定;
  • auto-predict 与显式 predictor 二选一,避免重复预测;
  • reset 后 mirrored/desired value 如何同步;
  • volatile、read-clear、write-one-clear 等访问属性。

显式 RAL Predictor 应接收 Bus Monitor 实际观察到的事务,这样镜像反映 DUT 真正接受的 访问,而不是 Sequence 的意图。

RAL 的基本层次

uvm_reg_block
├── uvm_reg
│   └── uvm_reg_field
├── uvm_reg_map
└── 可选的 sub-block / memory
  • uvm_reg_field 描述位宽、位置、访问属性和 reset value;
  • uvm_reg 组合字段;
  • uvm_reg_map 描述地址、总线宽度和端序;
  • uvm_reg_block 组合寄存器、map 和子 block。

定义寄存器和字段

class control_reg extends uvm_reg;
  `uvm_object_utils(control_reg)

  rand uvm_reg_field enable;
  rand uvm_reg_field mode;

  function new(string name = "control_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    enable = uvm_reg_field::type_id::create("enable");
    enable.configure(
      this,       // parent register
      1,          // size
      0,          // lsb position
      "RW",       // access
      0,          // volatile
      1'b0,       // reset value
      1,          // has reset
      1,          // is rand
      0           // individually accessible
    );

    mode = uvm_reg_field::type_id::create("mode");
    mode.configure(
      this, 2, 1, "RW", 0, 2'b00, 1, 1, 0
    );
  endfunction
endclass

字段位宽和位置不能重叠或超出 register width。Access string 必须与规格一致,否则 RAL 可能在 Sequence 层错误预测 mirror。

创建 Register Block 和 Address Map

class register_block extends uvm_reg_block;
  `uvm_object_utils(register_block)

  rand control_reg control;
  uvm_reg_map default_map;

  function new(string name = "register_block");
    super.new(name, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    default_map = create_map(
      "default_map",
      'h0,                 // base address
      4,                   // bytes per bus beat
      UVM_LITTLE_ENDIAN
    );

    control = control_reg::type_id::create("control");
    control.configure(this, null, "");
    control.build();

    default_map.add_reg(control, 'h00, "RW");

    lock_model();
  endfunction
endclass

lock_model() 在结构完成后固定地址模型。完成后再增加 register 或 map 通常是架构错误。 有多个地址视图时,可以为同一 register 建立多个 map。

Frontdoor 访问

Frontdoor 通过真实总线 VIP 访问 DUT:

uvm_status_e status;
uvm_reg_data_t value;

reg_model.control.write(
  status,
  32'h0000_0003,
  UVM_FRONTDOOR,
  reg_model.default_map,
  this
);

reg_model.control.read(
  status,
  value,
  UVM_FRONTDOOR,
  reg_model.default_map,
  this
);

if (status != UVM_IS_OK)
  `uvm_error("RAL_ACCESS", "control register access failed")

Frontdoor 会经过 adapter、Sequencer、Driver、Interface 和 DUT,因此能覆盖总线协议和地址 译码,但速度较慢。

Backdoor 访问

Backdoor 通过 HDL path 直接读写寄存器存储节点,不产生总线 transaction:

reg_model.control.write(
  status,
  32'h0000_0001,
  UVM_BACKDOOR,
  reg_model.default_map,
  this
);

Backdoor 适合快速初始化和数据一致性检查,但必须维护稳定的 HDL path。它不能替代 Frontdoor 协议验证,也可能绕过 DUT 的写副作用。

Adapter:RAL 操作与总线 Transaction 互转

class register_adapter extends uvm_reg_adapter;
  `uvm_object_utils(register_adapter)

  function new(string name = "register_adapter");
    super.new(name);
    supports_byte_enable = 1;
    provides_responses = 0;
  endfunction

  virtual function uvm_sequence_item reg2bus(
    const ref uvm_reg_bus_op rw
  );
    request_item req;

    req = request_item::type_id::create("req");
    req.op      = (rw.kind == UVM_WRITE) ? WRITE_OP : READ_OP;
    req.address = rw.addr;
    req.data    = rw.data;
    return req;
  endfunction

  virtual function void bus2reg(
    uvm_sequence_item bus_item,
    ref uvm_reg_bus_op rw
  );
    request_item req;

    if (!$cast(req, bus_item))
      `uvm_fatal("RAL_CAST", "adapter received an unexpected item type")

    rw.kind   = (req.op == WRITE_OP) ? UVM_WRITE : UVM_READ;
    rw.addr   = req.address;
    rw.data   = req.data;
    rw.status = UVM_IS_OK;
  endfunction
endclass

真实 Adapter 还要转换 byte enable、response status、burst 限制和读数据返回方式。 provides_responses 必须与 Driver 是否返回独立 response 一致。

连接 Sequencer 和显式 Predictor

class verification_env extends uvm_env;
  register_block reg_model;
  register_adapter reg_adapter;
  uvm_reg_predictor #(request_item) reg_predictor;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    reg_model = register_block::type_id::create("reg_model");
    reg_model.build();
    reg_model.reset();

    reg_adapter = register_adapter::type_id::create("reg_adapter");
    reg_predictor = uvm_reg_predictor#(request_item)::type_id::create(
      "reg_predictor", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);

    reg_model.default_map.set_sequencer(
      control_agent.sequencer,
      reg_adapter
    );

    reg_model.default_map.set_auto_predict(0);
    reg_predictor.map = reg_model.default_map;
    reg_predictor.adapter = reg_adapter;

    control_agent.request_ap.connect(reg_predictor.bus_in);
  endfunction
endclass

Auto predict 根据 RAL 发起的操作立即更新 mirror;显式 Predictor 根据 Bus Monitor 实际 观察到的操作更新 mirror。端到端平台通常优先显式 Predictor。两者同时开启会重复预测。

Desired、Mirrored 与 DUT 实际值

含义
Desired模型希望寄存器最终具有的值
Mirrored模型当前认为 DUT 中的值
ActualDUT 寄存器真实硬件值

set() 只修改 desired,不访问 DUT;update() 把需要变化的 desired 写入 DUT; mirror() 读取 DUT 并更新/检查 mirrored;predict() 根据已观察操作更新 mirrored。

reg_model.control.enable.set(1'b1);
reg_model.control.update(status, UVM_FRONTDOOR);

reg_model.control.mirror(
  status,
  UVM_CHECK,
  UVM_FRONTDOOR,
  reg_model.default_map,
  this
);

Reset 后必须调用 RAL model 的 reset() 更新镜像,但这不会自动驱动 DUT reset。DUT reset 与模型 reset 要由同一个平台事件协调。

常见访问属性

属性典型语义
RO只读
RW可读写
WO只写
W1C写 1 清零
W1S写 1 置位
RC读后清零
volatile值可能由硬件自行变化

带副作用或 volatile 字段不能用普通 RW 假设检查。RAL field 属性、功能 Predictor 和 Scoreboard 都要遵守同一规格语义。

RAL 集成检查清单

  • Register/field 位宽、lsb、reset 和 access 属性正确;
  • Address map 的 base、offset、bus width 和 endian 正确;
  • Model 已 build、lock 和 reset;
  • Adapter 正确转换读写、数据、地址、byte enable 和 status;
  • Frontdoor map 绑定正确 Sequencer;
  • Auto predict 与显式 Predictor 没有重复启用;
  • Bus Monitor 发布的是实际完成的访问;
  • Backdoor HDL path 在目标仿真层次有效;
  • Reset、volatile 和副作用寄存器有专门测试。

7.5 外部参考模型

外部 C/C++、Python 或 MATLAB 模型应通过清晰的数据契约集成:

  • 固定输入/输出类型与位宽;
  • 明确舍入、饱和、端序和 X 值策略;
  • 明确模型是无状态还是有状态;
  • reset 和配置更新必须可重复;
  • 记录模型版本和参数;
  • 批处理模型要维护输入与输出 ID 映射;
  • 外部调用失败必须 fatal,不能静默返回默认值。

为了调试,可把每次调用的输入、输出、seed、配置摘要和事务 ID 写入可重放日志。

7.6 性能与可扩展性

大型平台应避免:

  • 在高频 write() 中进行耗时字符串格式化;
  • 无限制增长的 queue 或 analysis FIFO;
  • 对大对象无条件深拷贝;
  • 所有组件使用最高 verbosity;
  • 用单个 Scoreboard 串行处理所有独立数据流。

可按功能拆分 Predictor/Scoreboard,给 FIFO 设置水位监控,并只在 mismatch 时展开详细 字段。性能优化不能改变 transaction 边界和检查完整性。


8. Bring-up、调试与交付检查

8.1 分阶段 Bring-up

不要第一次仿真就同时启用所有随机场景和检查器。推荐按以下顺序启用:

  1. 静态层次:编译 package,创建 Test/Env/Agent,打印 topology;
  2. 配置路径:确认每个 Agent 得到正确 config 和 virtual interface;
  3. 激励路径:只运行一个定向事务,在波形中确认握手;
  4. 观测路径:确认 Monitor 只在真实事件发布,字段和周期号正确;
  5. 预测路径:用手算输入核对 expected;
  6. 比对路径:接入 actual,确认正常匹配和故障注入失败;
  7. 结束条件:验证 drain、remainder 和 timeout;
  8. 覆盖路径:最后开启 Coverage 和大规模随机回归。

每一步都应有明确的通过证据,避免多个错误叠加后只能从最终 mismatch 反向猜测。

8.2 按路径定位故障

现象优先检查
Driver 没有活动Sequence 是否启动、sequencer 连接、objection
引脚有活动但 Monitor 无事务virtual interface、clocking block、握手条件
Input Monitor 有事务但无 expectedPredictor 连接、analysis imp/FIFO、模型异常
actual 有而 expected 无输入采样丢失、模型过滤、reset flush
expected 有而 actual 无DUT 未响应、Output Monitor、仿真过早结束
大量连续 mismatch首个时间戳/ID 错位,而不是逐条看数据
零错误但测试过快结束compared_count、FIFO remainder、objection
多 Agent 观察同一接口config_db 路径过宽或 vif 绑定错误

调试顺序应沿数据流逐跳确认“是否收到、何时收到、收到什么”,而不是先修改 Predictor 使结果看起来一致。

8.3 必做的启动检查

function void base_test::end_of_elaboration_phase(uvm_phase phase);
  super.end_of_elaboration_phase(phase);
  uvm_top.print_topology();
  uvm_factory::get().print();
endfunction

还应检查:

  • Active Agent 同时存在 Driver 和 Sequencer;
  • Passive Agent 不创建驱动组件;
  • 必需 virtual interface 非空且绑定到正确实例;
  • 所有 analysis port 都有预期订阅者;
  • config 的模式、超时和 enable 字段符合 Test;
  • Scoreboard 的 expected/actual 数据类型一致;
  • reset 初值和 epoch 一致。

8.4 日志和统计规范

统一 report ID 可以加快回归聚类:

Report ID用途
CFG配置摘要
DRVDriver 事务摘要
MON_REQ / MON_RSPMonitor 观察摘要
PREDexpected 生成
MATCH / MISMATCHScoreboard 结果
TIMEOUT等待超时
REMAINDER结束时未消费数据

默认日志保留计数与 ID;详细字段仅在高 verbosity 或失败时打印。最终 report 至少包括 generated、observed request、predicted、observed response、compared、matched、 mismatched、cancelled 和 outstanding 数量。

8.5 常见反模式

反模式后果工程替代
Predictor 读取 Sequence requestDriver/握手错误无法检出使用 Input Monitor
Predictor 读取 actual模型复制 DUT 错误独立 Oracle
Monitor 复用同一对象历史事务被覆盖每次创建或 clone
Test 直接连接底层组件层次变化导致用例失效Env/Agent 管理连接
config_db 全部使用 "*"多实例取错配置精确路径 + 配置对象
Sequence 结束即 drop objection最后响应丢失Scoreboard drain + timeout
只检查 mismatch 数丢包可能零错误remainder/outstanding/零比较检查
Coverage 采样随机化对象未实际发送也算覆盖采样 Monitor 事务
默认 FIFO 匹配乱序响应产生假错误ID/tag 匹配
直接照抄 RTL 建模同一算法错误被复制从规格独立推导

8.6 平台交付检查表

架构

  • 每条关键需求都有检查机制和覆盖证据;
  • Transaction 字段与可观察协议事件一致;
  • 激励、观测、预测、比对和控制路径边界清晰;
  • Active/Passive Agent 与复用目标一致。

连接

  • Driver–Sequencer 在 Agent 内连接;
  • 跨组件 analysis 连接集中在 Environment;
  • virtual interface 使用准确路径绑定;
  • Predictor 只接收 Input Monitor 数据;
  • expected 和 actual 分流进入 Scoreboard。

时间与状态

  • clocking block 定义采样/驱动时序;
  • reset、backpressure、延迟和 transaction 完成语义明确;
  • 周期号、ID 和 reset epoch 足以诊断对齐;
  • objection、drain 和 timeout 共同保证正确结束。

检查与收敛

  • Scoreboard 匹配策略符合 DUT 顺序保证;
  • 无 expected/actual remainder 和 outstanding;
  • SVA、Scoreboard、Coverage 职责不重叠也无空缺;
  • 手算向量和 mutation 能校准检查器;
  • 最终 PASS 条件排除“零检查通过”。

8.7 Report、Severity 与 Verbosity

平台日志应使用 UVM report,而不是散布 $display

`uvm_info("MON_REQ",
          $sformatf("observed %s", item.convert2string()),
          UVM_MEDIUM)

`uvm_warning("LATENCY_NEAR_LIMIT",
             "response is close to the maximum latency")

`uvm_error("MISMATCH", "actual data differs from expected")

`uvm_fatal("NO_VIF", "required virtual interface is missing")

Severity 表示结果严重程度:

Severity语义
INFO调试和进度信息
WARNING可继续运行但需要关注
ERROR检查失败,通常计入测试失败
FATAL无法继续,立即结束

Verbosity 只控制 INFO 是否显示,不应被用来隐藏 WARNING/ERROR。常见层次是 UVM_LOWUVM_MEDIUMUVM_HIGHUVM_DEBUG

运行时可以选择 Test 和日志级别:

+UVM_TESTNAME=backpressure_test
+UVM_VERBOSITY=UVM_HIGH

随机 seed 的命令行参数由模拟器决定,回归系统必须把实际 seed 记录在结果中。

如果需要针对某个 report ID 调整动作,应在 Test 中集中配置:

set_report_id_verbosity_hier("MON_REQ", UVM_HIGH);
set_report_severity_action_hier(UVM_ERROR, UVM_DISPLAY | UVM_COUNT);

不要把预期错误简单降级或屏蔽。负向测试应明确检查目标 report ID 是否出现,并确认没有 其他意外 ERROR/FATAL。

8.8 最终 PASS/FAIL 生成

最终结论应来自结构化统计,而不是日志最后一行:

function void base_test::report_phase(uvm_phase phase);
  uvm_report_server server;

  super.report_phase(phase);
  server = uvm_report_server::get_server();

  if (server.get_severity_count(UVM_FATAL) != 0 ||
      server.get_severity_count(UVM_ERROR) != 0 ||
      !env.scb.is_clean())
    `uvm_info("TEST_RESULT", "FAIL", UVM_NONE)
  else
    `uvm_info("TEST_RESULT", "PASS", UVM_NONE)
endfunction

scoreboard.is_clean() 应同时检查 compared 非零、mismatch 为零、FIFO 为空且 outstanding 为零。回归系统再结合进程状态、Assertion 和预期负向结果生成最终分类。


9. 推荐搭建顺序

实际新建平台时,可以按以下顺序推进:

  1. 从验证计划定义 transaction、检查点和匹配键;
  2. 定义 Interface、clocking block 和 SVA;
  3. 完成 Config、Sequencer、Driver、Monitor 和 Agent;
  4. 用单个定向事务打通激励与 Input Monitor;
  5. 实现独立 Predictor,并用手算向量校准;
  6. 实现 Scoreboard,先覆盖顺序场景,再扩展乱序/取消;
  7. 在 Environment 中连接检查与 Coverage 路径;
  8. 建立 Base Test、drain、timeout 和统计摘要;
  9. 增加边界、反压、reset、并发和异常场景;
  10. 最后接入 Factory Override、Virtual Sequence、RAL 或外部模型;
  11. 通过 Coverage、Assertion、回归和 Mutation Testing 完成收敛。

这个顺序的目的不是规定代码必须写成同一种形式,而是保证每增加一层复杂度时, 前一层已经有可观察、可重复的正确性证据。


最后更新: 2026-08-29