第二章:UVM基础架构——类库层次结构、核心基类与树形结构
各位同学,今天我们聊聊UVM的骨架。说白了,UVM就是一套用SystemVerilog写好的类库。你想想看,如果没有这套类库,我们做验证得从零开始搭环境,那得多痛苦?我个人习惯把UVM的类库想象成一个工具箱,里面分门别类放好了各种工具。今天我们就打开这个工具箱,看看里面到底有什么。
2.1 UVM类库的层次结构
UVM的类库,最顶层是uvm_void。它是个空类,啥也没有。为什么要搞个空类?嗯,这里要注意:uvm_void是所有类的根,它定义了一个统一的接口。从它往下,分出了两大分支:uvm_object和uvm_component。
我刚开始学UVM时,总搞不清这两个东西的区别。后来在项目中踩了坑才明白——uvm_object是“数据”,uvm_component是“硬件”。举个例子:一个transaction(事务)是数据,它用uvm_object;一个driver(驱动器)是硬件模块,它用uvm_component。
下面这张表,是我自己总结的,帮你快速区分:
| 特性 | uvm_object | uvm_component |
|---|---|---|
| 是否有生命周期 | 无(创建即用,用完即丢) | 有(从build到report) |
| 是否有树形结构 | 无 | 有(必须挂到树上) |
| 典型用途 | transaction、sequence_item | driver、monitor、agent |
| 是否可配置 | 通过field macros | 通过config_db |
核心要点:UVM类库的层次结构,本质上就是“数据”与“组件”的分离。你写验证环境时,先想清楚:这个东西是数据还是组件?想明白了,类就选对了。
2.2 核心基类:uvm_object
uvm_object是UVM中最基础的类。它提供了什么?我列几个最重要的:
- create():创建对象。UVM推荐用factory机制创建,而不是直接new。
- copy():深拷贝。我遇到过一个问题:两个transaction共享同一个对象,结果改了一个,另一个也变了。后来用copy()解决了。
- compare():比较两个对象是否相等。这个在scoreboard里特别常用。
- print():打印对象内容。调试时救命用的。
- record():记录事务到数据库。波形里能看到transaction,就是靠它。
我曾经在一个项目中,因为没重写compare()函数,导致scoreboard里永远比对不上。查了两天才发现——默认的compare()只比较指针,不比较内容。嗯,这个坑你们一定要记住。
下面是一个简单的transaction定义示例:
class my_transaction extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit wr_en;
// 注册到factory
`uvm_object_utils_begin(my_transaction)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_field_int(wr_en, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "my_transaction");
super.new(name);
endfunction
endclass
小技巧:用`uvm_field_*宏可以自动实现copy、compare、print等方法。但注意,这些宏会生成大量代码,影响编译速度。我个人习惯在小型项目里用宏,大型项目里手动实现关键方法。
2.3 核心基类:uvm_component
uvm_component是UVM验证环境的骨架。它和uvm_object最大的区别是:它有名字,有父节点,有生命周期。
每个uvm_component都必须有一个名字和一个父节点。为什么?因为UVM要构建一棵树。你想想看,如果没有树,你怎么知道哪个driver属于哪个agent?哪个agent属于哪个env?
生命周期方法,按执行顺序排列:
- build_phase:创建子组件。我习惯在这里做配置。
- connect_phase:连接组件之间的TLM端口。
- run_phase:主要运行逻辑。注意,这是唯一消耗仿真时间的phase。
- report_phase:打印结果。我经常在这里加一些统计信息。
下面是一个简单的driver示例:
class my_driver extends uvm_driver #(my_transaction);
`uvm_component_utils(my_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
// 驱动信号到DUT
drive_transaction(req);
seq_item_port.item_done();
end
endtask
endclass
注意:所有uvm_component的构造函数都必须有两个参数:name和parent。少一个都不行。我曾经因为漏了parent参数,编译通过了但仿真时组件挂不到树上,查了半天。
2.4 UVM树形结构
UVM的树形结构,说白了就是组件的父子关系。根节点是uvm_top,它是一个全局的uvm_root对象。所有组件都挂在它下面。
典型的树形结构长这样:
uvm_top
└── test (my_test)
└── env (my_env)
├── agent (my_agent)
│ ├── driver (my_driver)
│ ├── monitor (my_monitor)
│ └── sequencer (my_sequencer)
└── scoreboard (my_scoreboard)
为什么要搞这么复杂?我举个例子:假设你要在test里修改agent的配置。如果没有树,你得手动传递参数。有了树,你可以用uvm_config_db在任意层级设置和获取配置。
树形结构的几个关键点:
- 名字必须唯一:同一层级下,不能有两个同名的组件。
- 父节点必须存在:创建子组件时,父节点不能为null。
- 遍历方便:可以用
get_child()、get_parent()等方法遍历整棵树。
避坑指南:我曾经在项目中,因为两个agent的名字相同,导致config_db配置混乱。一个agent的配置被另一个agent覆盖了。从那以后,我养成了一个习惯:所有组件名字都加上层级前缀,比如"env.agent_a.driver",而不是简单的"driver"。
2.5 总结
好了,这一章的内容就这些。我们回顾一下:
- UVM类库从
uvm_void开始,分为uvm_object和uvm_component两大分支。 uvm_object处理数据,uvm_component处理组件。- UVM树形结构是验证环境的骨架,所有组件都挂在这棵树上。
下一章,我们会深入讲解UVM的phase机制。说白了,就是告诉UVM:什么时候该做什么事。敬请期待。