第二章:UVM基础架构——类库层次结构、核心基类与树形结构

各位同学,今天我们聊聊UVM的骨架。说白了,UVM就是一套用SystemVerilog写好的类库。你想想看,如果没有这套类库,我们做验证得从零开始搭环境,那得多痛苦?我个人习惯把UVM的类库想象成一个工具箱,里面分门别类放好了各种工具。今天我们就打开这个工具箱,看看里面到底有什么。

2.1 UVM类库的层次结构

UVM的类库,最顶层是uvm_void。它是个空类,啥也没有。为什么要搞个空类?嗯,这里要注意:uvm_void是所有类的根,它定义了一个统一的接口。从它往下,分出了两大分支:uvm_objectuvm_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?

生命周期方法,按执行顺序排列:

  1. build_phase:创建子组件。我习惯在这里做配置。
  2. connect_phase:连接组件之间的TLM端口。
  3. run_phase:主要运行逻辑。注意,这是唯一消耗仿真时间的phase。
  4. 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_objectuvm_component两大分支。
  • uvm_object处理数据,uvm_component处理组件。
  • UVM树形结构是验证环境的骨架,所有组件都挂在这棵树上。

下一章,我们会深入讲解UVM的phase机制。说白了,就是告诉UVM:什么时候该做什么事。敬请期待。