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 flush | reset × 活动状态 cross |
| 边界计数 | 最小/最大长度 | beat 数与 last | Scoreboard | 长度边界 bins |
| 并发或乱序 | 多个未完成请求 | 响应 ID/顺序 | ID 匹配 Scoreboard | outstanding 数 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_cycles 和 sample_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_NOCOPY | copy 时忽略 |
UVM_NOCOMPARE | compare 时忽略 |
UVM_NOPRINT | print/sprint 时忽略 |
UVM_NOPACK | pack/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_utils、uvm_object_utils、report macro 等预处理宏。二者作用不同,
不能互相替代。
Component 和 Object 的注册、构造与创建形式也不同:
| 类型 | 注册宏 | 构造函数 | Factory 创建 |
|---|---|---|---|
| Component | uvm_component_utils(T) | new(name, parent) | T::type_id::create(name, parent) |
| Object | uvm_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
这里必须同时匹配:
set与get的参数化类型;- 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_config、virtual 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 数据。
配置失败时依次检查:
T是否完全一致,特别是 virtual interface 的 modport;- Top 的
set是否发生在run_test()之前; - Test、Env、Agent 的实例名是否与路径一致;
- field name 大小写是否一致;
- 是否有更宽泛的配置覆盖了精确配置;
- 目标组件是否在预期 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 ↔ Sequencer | Agent |
| Monitor → Predictor/Coverage | Environment |
| Predictor → Scoreboard | Environment |
| Output Monitor → Scoreboard | Environment |
| 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 Monitor | Predictor | request_item | 生成 expected |
| Input Monitor | Coverage | request_item | 统计实际输入场景 |
| Predictor | Scoreboard expected FIFO | response_item | 保存预期结果 |
| Output Monitor | Scoreboard actual FIFO | response_item | 保存实际结果 |
| Output Monitor | Coverage | response_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 策略必须明确。常见选择有:
- reset 到来时取消当前事务,驱动 idle,并正常
item_done(); - reset 解除后重新发送当前事务;
- 返回带 cancelled 状态的 response;
- 把 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/export | Driver 从 Sequencer 拉取 | 是 | 激励仲裁 |
uvm_analysis_port | 一对多广播 | 否,零时间 | Monitor/ Predictor 发布 |
uvm_analysis_export | 转发 analysis 调用 | 否 | FIFO 或层次透传 |
uvm_analysis_imp | 终止连接并实现 write() | 否 | Predictor、Coverage |
uvm_tlm_analysis_fifo | analysis 写入、阻塞读取 | 读可阻塞 | 解耦到达时间 |
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_expected | write_expected() |
uvm_analysis_imp_decl(_actual) | uvm_analysis_imp_actual | write_actual() |
uvm_analysis_imp_decl(_request) | uvm_analysis_imp_request | write_request() |
后缀通常以 _ 开头,因为宏会直接拼接名称。例如 _expected 最终形成
uvm_analysis_imp_expected 和 write_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_imp或actual_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 与周期定义
周期精确平台必须统一三个问题:
- Monitor 在哪个边沿采样;
- Driver 的输出在该边沿前还是后生效;
- 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_simulation | function,不消耗时间 |
| 运行 | run,或 reset/configure/main/shutdown 等 runtime phase | task,可以消耗时间 |
| 收尾 | extract、check、report、final | function,不再等待新数据 |
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...join、join_any 和 join_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 调用,在当前时间和调用栈中依次通知订阅者。
它不创建新仿真线程,也不允许订阅者等待时钟。
这带来三个工程结论:
write()中只做快速复制、入队或纯函数预测;- 耗时处理通过 FIFO 交给 run phase;
- 不能依赖订阅者调用顺序实现功能。
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 功能输出 |
| Scoreboard | expected + actual | PASS/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 索引;
- 是否应发送
len或len + 1个 beat; - 只有握手成功的 beat 才计数;
- last 必须与最终有效握手同时成立;
- reset 或错误响应如何终止剩余 beat。
6.4 SVA、Sanity、Scoreboard 与 Coverage 的职责
| 机制 | 主要回答 | 适合检查 |
|---|---|---|
| SVA | 每个周期是否遵守局部规则? | 握手稳定、时序窗口、one-hot、禁止条件 |
| Sanity | 平台是否真的在工作? | 接口非空、计数非零、无 X、连接存在 |
| Scoreboard | 端到端功能结果是否正确? | 数据、顺序、延迟、丢失、重复 |
| Functional Coverage | 目标场景是否到达? | 模式、边界、组合、状态迁移 |
| Code Coverage | RTL 结构是否被执行? | 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 一致”证明。至少采用以下方法:
- 用手算定向向量校准 Predictor;
- 对边界值建立独立期望表;
- 对关键算法使用不同实现方式交叉检查;
- 故意注入错误,确认检查器必然失败;
- 检查错误是否在合理位置被报告。
可注入的 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 | 正常通过或预期报错 |
| tags | smoke、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 检查。
优先顺序建议:
- 配置字段可以表达的变化,用 config;
- 整个类型行为变化,用 type override;
- 只有单个实例特殊时,才用 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 中的值 |
| Actual | DUT 寄存器真实硬件值 |
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
不要第一次仿真就同时启用所有随机场景和检查器。推荐按以下顺序启用:
- 静态层次:编译 package,创建 Test/Env/Agent,打印 topology;
- 配置路径:确认每个 Agent 得到正确 config 和 virtual interface;
- 激励路径:只运行一个定向事务,在波形中确认握手;
- 观测路径:确认 Monitor 只在真实事件发布,字段和周期号正确;
- 预测路径:用手算输入核对 expected;
- 比对路径:接入 actual,确认正常匹配和故障注入失败;
- 结束条件:验证 drain、remainder 和 timeout;
- 覆盖路径:最后开启 Coverage 和大规模随机回归。
每一步都应有明确的通过证据,避免多个错误叠加后只能从最终 mismatch 反向猜测。
8.2 按路径定位故障
| 现象 | 优先检查 |
|---|---|
| Driver 没有活动 | Sequence 是否启动、sequencer 连接、objection |
| 引脚有活动但 Monitor 无事务 | virtual interface、clocking block、握手条件 |
| Input Monitor 有事务但无 expected | Predictor 连接、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 | 配置摘要 |
DRV | Driver 事务摘要 |
MON_REQ / MON_RSP | Monitor 观察摘要 |
PRED | expected 生成 |
MATCH / MISMATCH | Scoreboard 结果 |
TIMEOUT | 等待超时 |
REMAINDER | 结束时未消费数据 |
默认日志保留计数与 ID;详细字段仅在高 verbosity 或失败时打印。最终 report 至少包括 generated、observed request、predicted、observed response、compared、matched、 mismatched、cancelled 和 outstanding 数量。
8.5 常见反模式
| 反模式 | 后果 | 工程替代 |
|---|---|---|
| Predictor 读取 Sequence request | Driver/握手错误无法检出 | 使用 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_LOW、UVM_MEDIUM、UVM_HIGH 和 UVM_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. 推荐搭建顺序
实际新建平台时,可以按以下顺序推进:
- 从验证计划定义 transaction、检查点和匹配键;
- 定义 Interface、clocking block 和 SVA;
- 完成 Config、Sequencer、Driver、Monitor 和 Agent;
- 用单个定向事务打通激励与 Input Monitor;
- 实现独立 Predictor,并用手算向量校准;
- 实现 Scoreboard,先覆盖顺序场景,再扩展乱序/取消;
- 在 Environment 中连接检查与 Coverage 路径;
- 建立 Base Test、drain、timeout 和统计摘要;
- 增加边界、反压、reset、并发和异常场景;
- 最后接入 Factory Override、Virtual Sequence、RAL 或外部模型;
- 通过 Coverage、Assertion、回归和 Mutation Testing 完成收敛。
这个顺序的目的不是规定代码必须写成同一种形式,而是保证每增加一层复杂度时, 前一层已经有可观察、可重复的正确性证据。
最后更新: 2026-08-29