UVM验证—UVM 简述
1 虚接口(回顾)
在 SystemVerilog 验证环境中,虚拟接口(virtual interface)是连接 “抽象验证组件(如 driver/monitor 等类)” 与 “物理硬件信号” 的核心桥梁。它解决了一个关键问题:SystemVerilog 的类(class)无法直接访问硬件信号(如 wire/reg 类型的端口),而虚拟接口通过 “句柄引用” 的方式,让类能够间接操作或读取硬件信号,实现验证环境与 DUT 的交互。
1.1 示例
假设我们有一个简单的 DUT(加法器),RTL 代码如下:
// DUT:一个简单的加法器
module adder(input clk, // 时钟信号(硬件信号)input rstn, // 复位信号(硬件信号)input en, // 使能信号(硬件信号)input [3:0] a, b, // 输入数据(硬件信号)output [4:0] sum // 输出结果(硬件信号)
);
// 内部逻辑:当en=1时,sum = a + b
always @(posedge clk or negedge rstn) beginif(!rstn) sum <= 0;else if(en) sum <= a + b;
end
endmodule
步骤 1:定义物理接口(连接验证环境和 DUT)
首先需要一个物理接口(interface),把 DUT 的硬件信号 “打包”,作为验证环境和 DUT 的连接点:
// 物理接口:定义DUT的所有硬件信号
interface adder_if;logic clk; // 时钟信号(硬件信号)logic rstn; // 复位信号(硬件信号)logic en; // 使能信号(硬件信号)logic [3:0] a, b; // 输入数据(硬件信号)logic [4:0] sum; // 输出结果(硬件信号)
endinterface
步骤 2:在 Testbench 中绑定 DUT 和接口
在 Testbench 中,将 DUT 的硬件信号与接口信号 “绑定”,让接口直接对应 DUT 的物理信号:
module tb;// 1. 实例化物理接口(创建一个具体的“信号集合”)adder_if phy_if(); // 物理接口实例,包含实际的硬件信号// 2. 实例化DUT,将其端口与物理接口的信号绑定adder u_adder(.clk (phy_if.clk), // DUT的clk → 物理接口的clk.rstn (phy_if.rstn), // DUT的rstn → 物理接口的rstn.en (phy_if.en), // DUT的en → 物理接口的en.a (phy_if.a), // DUT的a → 物理接口的a.b (phy_if.b), // DUT的b → 物理接口的b.sum (phy_if.sum) // DUT的sum → 物理接口的sum);
endmodule
步骤 3:用虚拟接口让 driver 访问硬件信号 — 作为物理接口的 “句柄”
- 虚拟接口是物理接口的 “引用(句柄)”,本质是一个变量,用于指向物理接口实例。它的声明方式是在接口类型前加
virtual
关键字。 driver
是一个类(class),本身无法直接访问硬件信号(SystemVerilog 的类不能直接引用硬件信号),因此需要通过虚拟接口(virtual interface) 间接访问:
// Driver类:负责给DUT发送激励(需要访问硬件信号)
class driver;// 声明虚拟接口(指向物理接口的句柄)virtual adder_if vif; // 关键:虚拟接口作为“桥梁”// 构造函数:通过参数接收物理接口的引用,赋值给虚拟接口function new(virtual adder_if vif);this.vif = vif; // 虚拟接口指向物理接口实例(phy_if)endfunction
endclass
步骤 4:在 Testbench 中传递虚拟接口
在 Testbench 中,将物理接口的实例(phy_if
)通过构造函数new(vif)
传递给验证组件(如 driver),此时组件的虚拟接口就指向了实际的物理接口,从而能够访问硬件信号。
步骤 5:验证组件通过虚拟接口与 DUT 交互
验证组件(driver/monitor)通过虚拟接口,直接读写物理接口的信号,从而实现与 DUT 的交互:
- driver 通过虚拟接口给 DUT 发送激励(如设置
en=1
、a=3
、b=5
); - monitor 通过虚拟接口采样 DUT 的输出(如读取
sum=8
)。
// driver中驱动激励的任务(通过虚拟接口)
task driver::drive();forever begin@(posedge vif.clk); // 同步到时钟上升沿(通过虚拟接口访问clk)vif.en = 1; // 使能有效(操作硬件信号en)vif.a = 3; // 输入数据a=3(操作硬件信号a)vif.b = 5; // 输入数据b=5(操作硬件信号b)// DUT会自动计算sum = a + b = 8(硬件信号sum被更新)end
endtask// monitor中采样输出的任务(通过虚拟接口)
task monitor::sample();forever begin@(posedge vif.clk); // 同步到时钟上升沿if(vif.en) begin // 当使能有效时(读取硬件信号en)$display("DUT输出sum = %d", vif.sum); // 读取硬件信号sumendend
endtask
1.2 虚拟接口的核心作用与优势
连接抽象与物理:
验证组件(类)是抽象的软件逻辑,无法直接访问硬件信号;虚拟接口作为 “句柄”,让类能够间接操作物理信号,实现 “软件验证环境” 与 “硬件 DUT” 的通信。
提高可重用性:
若更换 DUT,只需修改物理接口的绑定(如将adder_if
换成multiplier_if
),验证组件(driver/monitor)的代码无需修改(只需传递新的虚拟接口),大幅提升验证环境的复用性。
支持多实例验证:
一个虚拟接口可以指向不同的物理接口实例,支持同时验证多个 DUT(如多个加法器),每个实例用独立的物理接口,虚拟接口动态指向对应实例即可。
1.3 一句话总结
虚拟接口就像验证组件(如 driver)的 “遥控器”:物理接口是 “插座”(连接 DUT 的硬件信号),虚拟接口是 “遥控器的无线连接”,让组件无需直接接触硬件,就能通过这个 “无线连接” 控制或读取 DUT 的信号,是 SystemVerilog 验证中 “软硬件交互” 的核心机制。
2 Hello Word(UVM)
2.1 代码
import uvm_pkg::*;
`include "uvm_macros.svh"// 定义环境类,继承自uvm_env
class hello_world_env extends uvm_env;// 构造函数function new(string name, uvm_component parent = null);super.new(name, parent); // 调用父类构造函数endfunction : new
endclass : hello_world_env// 定义测试类,继承自uvm_test
class hello_world_test extends uvm_test;`uvm_component_utils(hello_world_test) // 注册组件到UVM工厂hello_world_env env; // 声明环境实例// 构造函数function new(string name, uvm_component parent = null);super.new(name, parent);env = new("env", this); // 实例化环境,指定父组件为当前测试类endfunction : new// 主测试阶段任务task main_phase(uvm_phase phase);super.main_phase(phase);// 提起异议:告诉UVM不要提前结束仿真phase.raise_objection(this);#1000ns; // 延迟1000ns// 打印信息:UVM的标准打印宏`uvm_info(this.get_name(), "***Hello World From UVM***", UVM_LOW)// 放下异议:允许UVM结束仿真phase.drop_objection(this);endtask : main_phase
endclass : hello_world_test// 测试平台顶层模块
module tb_hello_world();initial begin// 启动UVM测试,指定运行hello_world_testrun_test("hello_world_test");end
endmodule : tb_hello_world
2.2 代码结构与核心概念解析
- UVM 库与宏引入
import uvm_pkg::*;
:导入 UVM 库,包含所有 UVM 核心类和方法include "uvm_macros.svh"
:引入 UVM 宏定义(如uvm_component_utils
、uvm_info
等)
-
环境类(hello_world_env)
- 继承自
uvm_env
(UVM 环境基类),用于组织验证组件 - 目前是一个空环境,实际项目中会包含 driver、monitor、checker 等组件
- 继承自
- 测试类(hello_world_test)
- 继承自
uvm_test
(UVM 测试基类),是 UVM 验证的入口点 `uvm_component_utils(hello_world_test)
:将类注册到 UVM 工厂,使其可以通过字符串动态创建- 实例化了
hello_world_env
环境,形成 "测试 - 环境" 的层次结构
- 继承自
- 主测试阶段(main_phase)
- UVM 的标准 phase 机制,
main_phase
是主要的测试执行阶段 phase.raise_objection(this)
:阻止 UVM 仿真提前结束(UVM 默认在没有异议时会立即结束)#1000ns
:仿真延迟,模拟实际测试中的时间流逝`uvm_info(...)
:UVM 标准打印宏,输出 "Hello World" 信息,包含三个参数:- 第一个参数:组件名称(通过
this.get_name()
获取) - 第二个参数:要打印的消息
- 第三个参数:消息冗余度(UVM_LOW 表示基本信息)
- 第一个参数:组件名称(通过
phase.drop_objection(this)
:释放异议,允许 UVM 结束仿真
- UVM 的标准 phase 机制,
- 顶层测试平台(tb_hello_world)
- 简单的模块,通过
run_test("hello_world_test")
启动 UVM 测试 run_test
函数会根据传入的字符串("hello_world_test")从 UVM 工厂中创建对应的测试实例并执行
- 简单的模块,通过
2.3 运行流程
- 仿真开始,执行
tb_hello_world
模块的initial
块 - 调用
run_test
启动 UVM 框架,创建hello_world_test
实例 hello_world_test
的构造函数创建hello_world_env
环境- UVM 自动执行各 phase,当进入
main_phase
时:- 提起异议,防止仿真结束
- 延迟 1000ns
- 打印 "Hello World From UVM"
- 放下异议
- 所有异议被放下后,UVM 结束仿真
3 UVM组件概述(树形结构)
3.1 类继承树的核心层级
从顶到底,基础类层层派生,构建 UVM 的 “基因库”:
-
uvm_void
:
UVM 最顶层的空类,是所有类的 “祖先”(类似 Java 的Object
),没有实际功能,仅作为继承起点。 -
uvm_object
:
UVM 中所有 “可复用对象” 的基类,提供基本方法(如copy
/compare
/print
),是 UVM 面向对象设计的基础。
3.2 两大分支(uvm_transaction
vs uvm_component
)
uvm_object
派生为两大核心分支,对应 UVM 的两类关键对象:
(1)uvm_transaction
分支(“数据对象” 侧 )
- 作用:处理 “事务级数据”(如测试用例的激励、响应 ),不参与组件层次管理,专注数据操作。
- 派生关系:
uvm_transaction
→uvm_sequence_item
→uvm_sequence_base
→uvm_sequence
- 典型类:
uvm_sequence_item
:最小的事务单元(如包含addr
/data
/en
的数据包 )。uvm_sequence
:事务序列,可把多个sequence_item
按顺序发给sequencer
,构建复杂测试场景。
(2)uvm_component
分支(“组件层次” 侧 )
- 作用:处理 “验证组件”(如
driver
/monitor
/env
),负责构建 UVM 的层次化结构(父组件→子组件 ),并通过phase
机制控制执行顺序。 - 派生关系:
uvm_object
→uvm_report_object
→uvm_component
- 典型类(右侧分支 ):
uvm_driver
:驱动 DUT 输入,把sequence
发的item
转化为硬件信号。uvm_monitor
:采样 DUT 输出,把硬件信号转化为sequence_item
。uvm_agent
:封装driver
+monitor
+sequencer
,作为一个 “功能单元” 复用。uvm_scoreboard
:比对monitor
采的实际数据和driver
发的预期数据,判断 DUT 功能。uvm_env
:封装多个agent
/scoreboard
,构建更上层的验证环境。uvm_test
:验证的顶层入口,实例化uvm_env
,配置并启动测试。uvm_root
:全局唯一的顶层组件(隐藏的 “根节点” ),所有uvm_test
都是它的子组件。
(3)uvm_sequencer
相关(序列器 )
uvm_sequencer_base
→uvm_sequencer_param
→uvm_sequencer
- 作用:作为
sequence
和driver
的 “中间人”,管理sequence
发的item
,按顺序转发给driver
,是 “序列驱动” 机制的核心。
3.3 继承关系的意义
- 代码复用:基础类(如
uvm_object
/uvm_component
)实现通用功能(如new
构造、phase
执行 ),派生类(如uvm_driver
)只需专注自己的逻辑(如驱动硬件信号 )。 - 层次化验证:
uvm_component
分支的类通过继承,天然支持 父→子组件层次(如uvm_env
包含uvm_agent
),配合phase
机制(如build_phase
/run_phase
),让验证平台的执行顺序可控(先建父组件,再建子组件;先跑reset_phase
,再跑main_phase
)。
3.4 和 phase
机制的关联
- 图里提到 “内部 phase 机制可以使得平台的顺序执行”,因为
uvm_component
及其派生类 都继承了phase
执行逻辑:- UVM 会按固定顺序(如
build_phase
→connect_phase
→run_phase
)遍历组件层次树,依次执行每个组件的phase
方法,保证验证平台 “先构建、再连接、最后运行” 的有序流程。
- UVM 会按固定顺序(如
这张图是 UVM 的 “家族基因树”:从 uvm_void
开始,派生 uvm_object
,再分 “事务数据”(uvm_transaction
分支 )和 “组件层次”(uvm_component
分支 ),右侧 driver
/monitor
/env
等类都是 uvm_component
的 “子孙”,共同构建层次化验证平台,并用 phase
机制控制执行顺序,让验证流程有序运行 。
3.5 组件实例化与层次结构
3.5.1 uvm_component
构造函数声明
function new (string name, uvm_component parent=null);
string name
:当前组件的名称(如"my_driver"
),用于标识层次结构(方便uvm_info
打印、调试 )。uvm_component parent
:父组件的句柄,null
表示无父组件(顶层组件用null
,子组件需指定父组件 )。
3.5.2 子组件实例化示例
class my_env extends uvm_env;my_driver my_driver; // 声明子组件function new(string name, uvm_component parent=null);super.new(name, parent); my_driver = new("my_driver", this); // 实例化子组件,指定父组件为 `this`(当前 `my_env` )endfunction
endclass
my_driver = new("my_driver", this);
:- 第一个参数
"my_driver"
是子组件的名称; - 第二个参数
this
表示 当前类(my_env
)作为父组件,让子组件归属到my_env
的层次下。
- 第一个参数
3.5.3 this
关键字的含义
this
在类的方法(如new
函数)中,代表 当前类的实例(当前my_env
对象 )。- 在实例化子组件时,
this
传给parent
参数,作用是 “把当前类作为父组件”,让子组件(my_driver
)成为当前类(my_env
)的子节点,构建 UVM 的 层次化组件树(父→子关系 )。
3.5.4 UVM 层次化结构的意义
- 层次化管理:通过
parent
串联组件(如my_env
→my_driver
),形成验证平台的树形结构(类似文件夹的层级 ),方便管理复杂验证环境。 - 资源共享:父组件可以传递配置(
uvm_config_db
)、时钟 / 复位等资源给子组件。 - 调试与日志:每个组件的
name
+ 层次路径(如my_env.my_driver
),让uvm_info
打印的信息能清晰定位来源,方便调试。
3.5.5 实际验证平台中的作用
- 顶层组件(如
uvm_test
):实例化时parent
传null
(无父组件 ),是层次树的根。 - 中间组件(如
uvm_env
):实例化子组件(driver/monitor
)时,用this
作为父组件,把子组件挂在自己的层次下。 - 底层组件(如
uvm_driver
):被上层组件实例化,通过parent
融入整个层次树。
4 UVM 通信组件 TLM
这是 UVM 中 TLM(Transaction Level Modeling,事务级建模)端口的分类与连接规则 讲解,核心是教你区分 port
/export
/imp
三类 TLM 端口,以及它们如何协作实现组件间的事务通信,拆解如下:
4.1 三类 TLM 端口的角色与功能
(1)port
/ analysis_port
(发起端接口 )
- 角色:作为 “发起端”(如
uvm_sequence
/uvm_driver
)的出口,主动发起事务通信(如发送sequence_item
)。 - 核心区别:
port
:一对一通信(一个port
连一个imp
)。analysis_port
:一对多通信(一个port
可以连多个imp
,比如monitor
同时把数据发给scoreboard
和coverage
)。
(2)export
/ analysis_export
(中间层接口 )
- 角色:作为 “中间过渡层”(如
uvm_agent
/uvm_env
内部 ),不发起通信,只 转发事务,让端口连接能跨越多层组件。 - 核心区别:
export
:一对一转发(类似port
的中间版 )。analysis_export
:一对多转发(类似analysis_port
的中间版 )。
(3)imp
(目标端末端 )
- 角色:作为 “目标端”(如
uvm_scoreboard
/uvm_sequencer
)的入口,是事务通信的 终点,必须实现实际的事务处理逻辑(如put
/get
方法 )。
4.2 端口连接规则(优先级与层级)
- 优先级:
port
>export
>imp
(port
优先级最高,imp
最低 )。 - 连接规则:
- 高优先级端口可以连接低优先级端口(
port
能连export
或imp
;export
能连export
或imp
)。 - 低优先级端口不能连高优先级端口(
export
不能连port
)。 - 最终连接必须终止于
imp
(事务的终点必须是imp
,否则通信无法完成 )。
- 高优先级端口可以连接低优先级端口(
4.3 典型连接示例(结合上图)
- 发起端(Initiator):用
port
发起事务 → - 中间层(中间的
export
):用export
转发事务 → - 目标端(Target):用
imp
接收并处理事务。 - 作用:当发起端和目标端之间有多层组件(如
driver
在agent
里,agent
在env
里 ),export
可以让事务 “穿透” 中间层,最终到达imp
处理,保证层次化组件间的通信。
4.4 imp
的实现细节
- 声明方式:
systemverilog
`uvm_blocking_put_imp#(T, IMP)
T
:要传输的事务类型(如uvm_sequence_item
)。IMP
:实现该接口的组件(如scoreboard
类名 )。
- 功能:
imp
所在的组件必须实现put
/get
等方法(如put
函数实际处理事务数据 ),TLM 只是通道,真正的逻辑由imp
所在组件实现。
4.5 实际应用价值
- 层次化通信:让复杂验证环境(多层组件嵌套)中的事务能顺利传递(如
sequence
在env
外层,通过port
+export
穿透到内层driver
的imp
)。 - 解耦与复用:
port
/export
/imp
解耦了发起端和目标端的实现,中间层组件无需关心事务细节,只需转发,提升验证平台的可复用性。
4.7 端口的方法
4.7.1 方法
(1)put
方法
- 功能:发起端(如
driver
)主动发送数据 给目标端(如sequencer
/scoreboard
)。 - 数据流向:发起端 → 目标端(单向,只发不收 )。
- 典型场景:
driver
把sequence_item
发给sequencer
,或monitor
把采集的item
发给scoreboard
。 - 声明示例:
systemverilog
task put(T t); // T 是事务类型(如 uvm_sequence_item)// 实现数据发送逻辑(目标端的 imp 要实现这个方法) endtask
(2)get
/peek
方法
- 功能:发起端(如
sequence
)主动从目标端索取数据。 - 数据流向:目标端 → 发起端(单向,只收不发 )。
- 区别:
get
:目标端把数据 移除 并返回给发起端(目标端数据会被 “取走” )。peek
:目标端把数据 复制 并返回给发起端(目标端数据保留,相当于 “偷看” )。
- 典型场景:
sequence
从sequencer
索取sequence_item
,或scoreboard
从monitor
复制数据做比对。
声明示例:
systemverilog
task get(output T t); // T 是事务类型// 实现数据索取逻辑(目标端的 imp 要实现)
endtasktask peek(output T t);// 类似 get,但数据不移除,只复制
endtask
(3)transport
方法
- 功能:发起端 先发送数据,再等待响应(相当于
put
+get
的组合 )。 - 数据流向:发起端 → 目标端(发数据),然后 目标端 → 发起端(回响应),是 双向通信。
- 典型场景:需要 “请求 - 响应” 交互的场景(如
sequence
发请求给driver
,driver
回响应给sequence
)。 - 声明示例:
systemverilog
task transport(REQ request, output RSP response); // REQ:请求类型,RSP:响应类型// 先发送 request,再接收 response endtask
(4)write
方法
- 功能:针对
analysis_port
(一对多广播 ),发起端 发送数据给多个目标端(如monitor
同时发给scoreboard
和coverage
)。 - 数据流向:发起端 → 多个目标端(一对多,单向广播 )。
- 特殊要求:
- 目标端的
imp
必须实现write
函数,当analysis_port
广播时,会 自动调用 所有连接的imp
的write
方法。
- 目标端的
- 典型场景:
monitor
用analysis_port
把item
同时发给scoreboard
(做比对 )和coverage
(做覆盖 )。 - 声明示例:
systemverilog
// 发起端用 analysis_port 广播 uvm_analysis_port #(T) ap; // 目标端的 imp 必须实现 write 函数 function void write(T t); // 处理广播过来的数据 endfunction
4.7.2 方法的典型应用场景
put
:简单的 “发数据” 场景(如driver
发item
给sequencer
)。get
/peek
:“索取数据” 场景(如sequence
从sequencer
拿item
)。transport
:“请求 - 响应” 场景(如协议层的握手、命令 - 回包 )。write
:“一对多广播” 场景(如monitor
同时通知scoreboard
和coverage
)。
4.7.3 核心区别总结
方法 | 数据流向 | 典型场景 | 特殊要求 |
---|---|---|---|
put | 发起端 → 目标端 | 单向发送(如 driver 发数) | 目标端 imp 实现 put 方法 |
get | 目标端 → 发起端 | 单向索取(如 sequence 取数) | 目标端 imp 实现 get 方法 |
peek | 目标端 → 发起端 | 复制索取(偷看数据) | 目标端 imp 实现 peek 方法 |
transport | 发起→目标→发起 | 请求 - 响应(如命令 - 回包) | 需同时处理请求和响应 |
write | 发起端 → 多目标 | 一对多广播(如 monitor 广播) | 目标端 imp 实现 write 方法 |
put
是 “发数据”,get
/peek
是 “拿数据”,transport
是 “发 + 拿(请求 - 响应)”,write
是 “一对多广播”。- 不同方法对应不同的数据流向和场景,目标端的
imp
必须实现对应方法的逻辑,才能完成事务通信,是 UVM 组件间协作的核心手段 。
4.8 常用通信方式
analysis 端口和 TLM FIFO
import uvm_pkg::*; // 导入UVM库,包含所有UVM核心类和方法
`include "uvm_macros.svh" // 导入UVM宏定义(如`uvm_component_utils、`uvm_info等)// 1. 事务类:定义TLM通信中传输的数据结构
// 作用:封装需要传递的信息(地址、数据等),是TLM通信的基本单元
class tlm_item extends uvm_sequence_item;rand bit [31:0] addr; // 32位地址字段(rand表示可随机化)rand bit [31:0] data; // 32位数据字段// 注册到UVM工厂:使该类可通过工厂模式动态创建实例`uvm_object_utils(tlm_item)// 构造函数:初始化事务对象function new(string name = "tlm_item");super.new(name); // 调用父类构造函数endfunction// 打印事务内容:UVM标准打印方法,用于调试时输出事务信息function void do_print(uvm_printer printer);super.do_print(printer);// 以十六进制格式打印addr和data字段printer.print_field("addr", addr, 32, UVM_HEX);printer.print_field("data", data, 32, UVM_HEX);endfunction
endclass// 2. 声明分析端口实现的后缀:
// 用于生成特定名称的分析端口实现类(uvm_analysis_imp_consumer)
// _consumer为自定义后缀,需与后续实现方法名(write_consumer)匹配
`uvm_analysis_imp_decl(_consumer)// 3. 生产者类:负责生成事务并通过TLM端口发送
class tlm_producer extends uvm_component;// 声明两个分析端口(uvm_analysis_port):// 分析端口支持一对多通信,可连接多个接收端uvm_analysis_port#(tlm_item) ap_a; // 端口a:用于直接发送事务uvm_analysis_port#(tlm_item) ap_b; // 端口b:用于通过FIFO发送事务// 事务对象:存储要发送的数据tlm_item item_a;tlm_item item_b;// 注册到UVM工厂:使组件可被UVM层次结构管理`uvm_component_utils(tlm_producer)// 构造函数:初始化组件和端口function new(string name = "tlm_producer", uvm_component parent = null);super.new(name, parent); // 调用父类构造函数,指定组件名称和父组件ap_a = new("ap_a", this); // 初始化端口ap_a,父组件为当前producerap_b = new("ap_b", this); // 初始化端口ap_b,父组件为当前produceritem_a = new(); // 创建事务对象item_aitem_b = new(); // 创建事务对象item_bendfunction : new// 主阶段任务:UVM标准phase,验证的主要执行阶段task main_phase(uvm_phase phase);super.main_phase(phase); // 调用父类main_phase// 提起异议:告诉UVM仿真器不要提前结束(默认无异议时会立即退出)phase.raise_objection(this);// 给事务对象赋值(实际验证中通常使用randomize()随机生成)item_a.addr = 32'h1234; // 地址赋值为0x1234item_a.data = 32'h4321; // 数据赋值为0x4321item_b.addr = 32'h5678; // 地址赋值为0x5678item_b.data = 32'h8765; // 数据赋值为0x8765// 通过分析端口发送事务:调用write()方法`uvm_info(get_name(), "Sending item_a via ap_a", UVM_LOW)ap_a.write(item_a); // 通过ap_a发送item_a`uvm_info(get_name(), "Sending item_b via ap_b", UVM_LOW)ap_b.write(item_b); // 通过ap_b发送item_b#100; // 延迟100ns:等待消费者处理事务// 放下异议:允许UVM仿真器结束仿真phase.drop_objection(this);endtask : main_phase
endclass : tlm_producer// 4. 消费者类:负责接收并处理生产者发送的事务
class tlm_consumer extends uvm_component;tlm_item item; // 用于存储接收的事务数据// 声明端口:// 1. 分析端口实现(uvm_analysis_imp_consumer):接收ap_a发送的事务// 模板参数:<事务类型,实现类>uvm_analysis_imp_consumer#(tlm_item, tlm_consumer) port_a;// 2. 阻塞型get端口(uvm_blocking_get_port):从FIFO获取事务// 调用get()方法时会阻塞,直到有数据可用uvm_blocking_get_port#(tlm_item) port_b;// 注册到UVM工厂`uvm_component_utils(tlm_consumer)// 构造函数:初始化组件和端口function new(string name = "tlm_consumer", uvm_component parent = null);super.new(name, parent); // 调用父类构造函数port_a = new("port_a", this); // 初始化port_a,父组件为当前consumerport_b = new("port_b", this); // 初始化port_b,父组件为当前consumerendfunction : new// 主阶段任务:通过port_b获取并处理事务task main_phase(uvm_phase phase);super.main_phase(phase); // 调用父类main_phase// 从port_b获取事务:阻塞等待,直到FIFO中有数据port_b.get(item);// 打印接收的事务信息:使用UVM_INFO宏,包含组件名和数据`uvm_info(get_name(), $sformatf("Received from port_b: addr=0x%0h, data=0x%0h",item.addr, item.data), UVM_LOW);endtask : main_phase// 分析端口回调函数:当ap_a发送数据时,UVM会自动调用此方法// 方法名必须为"write_<后缀>",与`uvm_analysis_imp_decl(_consumer)的后缀匹配function void write_consumer(tlm_item item);// 打印接收的事务信息`uvm_info(get_name(), $sformatf("Received from port_a: addr=0x%0h, data=0x%0h",item.addr, item.data), UVM_LOW);endfunction : write_consumer
endclass : tlm_consumer// 5. 环境类:UVM验证环境的核心容器,负责实例化组件并连接端口
class tlm_env extends uvm_env;// 声明组件:生产者、消费者和FIFOtlm_producer tlm_pdr; // 生产者实例tlm_consumer tlm_csr; // 消费者实例// TLM分析FIFO:用于缓冲事务,解决生产者和消费者速度不匹配问题// 模板参数:<事务类型>uvm_tlm_analysis_fifo #(tlm_item) producer_to_consumer_fifo;// 注册到UVM工厂`uvm_component_utils(tlm_env)// 构造函数:初始化环境function new(string name, uvm_component parent = null);super.new(name, parent); // 调用父类构造函数endfunction : new// 构建阶段(build_phase):UVM标准phase,用于实例化组件// 所有组件应在build_phase中创建,而非构造函数function void build_phase(uvm_phase phase);super.build_phase(phase); // 调用父类build_phase// 通过UVM工厂创建组件实例:type_id::create(实例名, 父组件)tlm_pdr = tlm_producer::type_id::create("tlm_pdr", this);tlm_csr = tlm_consumer::type_id::create("tlm_csr", this);// 创建FIFO实例,父组件为当前envproducer_to_consumer_fifo = new("producer_to_consumer_fifo", this);endfunction : build_phase// 连接阶段(connect_phase):UVM标准phase,用于连接组件间的端口// 端口连接应在connect_phase中完成,确保所有组件已实例化function void connect_phase(uvm_phase phase);super.connect_phase(phase); // 调用父类connect_phase// 连接1:生产者ap_a → 消费者port_a(直接通信,无缓冲)tlm_pdr.ap_a.connect(tlm_csr.port_a);// 连接2:生产者ap_b → FIFO → 消费者port_b(带缓冲的通信)// 步骤1:ap_b连接到FIFO的analysis_export(FIFO接收端)tlm_pdr.ap_b.connect(producer_to_consumer_fifo.analysis_export);// 步骤2:消费者port_b连接到FIFO的blocking_get_export(FIFO发送端)tlm_csr.port_b.connect(producer_to_consumer_fifo.blocking_get_export);endfunction : connect_phase// 主阶段任务:环境级的控制逻辑(当前为空,可扩展)task main_phase(uvm_phase phase);super.main_phase(phase); // 调用父类main_phaseendtask : main_phase
endclass : tlm_env// 6. 测试类:UVM验证的入口点,用于启动验证环境
class tlm_test extends uvm_test;tlm_env env; // 环境实例// 注册到UVM工厂`uvm_component_utils(tlm_test)// 构造函数function new(string name = "tlm_test", uvm_component parent = null);super.new(name, parent); // 调用父类构造函数endfunction : new// 构建阶段:创建环境实例function void build_phase(uvm_phase phase);super.build_phase(phase); // 调用父类build_phaseenv = tlm_env::type_id::create("env", this); // 创建环境实例endfunction : build_phase
endclass : tlm_test// 7. 顶层测试平台模块:仿真的起点
module tb;initial begin// 启动UVM测试:通过字符串指定要运行的测试类(tlm_test)// UVM会自动创建测试实例并按phase顺序执行run_test("tlm_test");end
endmodule : tb
(1)完整的层次结构:
- 顶层模块
tb
→ 测试类tlm_test
→ 环境类tlm_env
→ 组件(tlm_producer
和tlm_consumer
) - 遵循 UVM 标准的层次化结构,便于管理和扩展
(2)两条独立的通信链路:
-
链路 1(无缓冲直接通信):
tlm_producer.ap_a
→tlm_consumer.port_a
生产者通过分析端口直接发送数据到消费者,消费者通过write_consumer
方法接收 -
链路 2(FIFO 缓冲通信):
tlm_producer.ap_b
→producer_to_consumer_fifo
→tlm_consumer.port_b
数据先存入 FIFO,消费者通过阻塞型get
方法从 FIFO 中取数据,解决速度匹配问题
(3)UVM 标准实践:
- 所有组件都添加了
uvm_component_utils
宏注册 - 组件实例化放在
build_phase
,端口连接放在connect_phase
,符合 UVM 相位机制 - 使用
raise_objection
和drop_objection
控制仿真周期
(4)事务流程:
- 生产者在
main_phase
中生成两个事务(item_a
和item_b
) - 分别通过两条链路发送
- 消费者分别通过
port_a
和port_b
接收并打印数据
(5)预期输出
运行仿真时,会产生类似以下的输出,展示两条通信链路的数据传输:
UVM_INFO tlm_test.sv(56) @ 0: uvm_test_top.env.tlm_pdr [tlm_pdr] Sending item_a via ap_a
UVM_INFO tlm_test.sv(98) @ 0: uvm_test_top.env.tlm_csr [tlm_csr] Received from port_a: addr=0x1234, data=0x4321
UVM_INFO tlm_test.sv(58) @ 0: uvm_test_top.env.tlm_pdr [tlm_pdr] Sending item_b via ap_b
UVM_INFO tlm_test.sv(91) @ 0: uvm_test_top.env.tlm_csr [tlm_csr] Received from port_b: addr=0x5678, data=0x8765
详细流程(点击)