2. 搭建你的第一个UVM环境:从零开始,构建一个最简单的UVM testbench,理解uvm_component与uvm_object

说实话,很多初学者一上来就被UVM的庞大体系吓住了。什么factory、sequence、driver、monitor……一堆概念砸过来,很容易懵。

我个人习惯是:先别管那么多,动手搭一个能跑的环境出来。哪怕它再简陋,只要能跑通,你就已经迈过了最难的坎。

这一章,我们就从零开始,构建一个最简单的UVM testbench。目标只有一个:让一个transaction从driver发出去,在monitor里被看到。仅此而已。

2.1 先搞清楚两个核心概念:uvm_component 和 uvm_object

在动手之前,我们得先弄明白UVM世界里最基础的两个类。说白了,整个UVM环境就是由这两种东西拼起来的。

核心区别一句话总结:

  • uvm_component —— 有生命周期的,有层次结构的,需要被build、connect、run。比如driver、monitor、agent。
  • uvm_object —— 没有生命周期的,没有层次结构的,用来封装数据。比如transaction、sequence_item。

嗯,这里要注意:uvm_component 是 uvm_object 的子类。但UVM把它们的职责分得很清楚。你想想看,一个driver需要被创建、连接、运行,它得有“活着”的概念。而一个transaction只是一包数据,发完就完事了,不需要被“运行”。

我在项目中遇到过有人把transaction写成component,结果整个环境跑起来奇慢无比,还各种报错。原因就是component有phase机制,每创建一个component都会触发一堆回调,而object没有这些开销。

2.2 我们的第一个UVM环境长什么样?

先画个图,让你对整体有个感觉。这个环境非常简单,只有四个部分:

my_test (uvm_test) my_env (uvm_env) my_agent (uvm_agent) my_driver (uvm_driver) my_monitor (uvm_monitor) 图2-1 最简单的UVM环境结构

这个结构很清晰:test → env → agent → driver + monitor。每个组件都是上一级的“孩子”。

2.3 第一步:定义我们的transaction(uvm_object)

先写数据包。这个transaction很简单,就一个数据域和一个控制信号。

class my_transaction extends uvm_sequence_item;
  rand bit [7:0] data;
  rand bit       valid;
  
  `uvm_object_utils_begin(my_transaction)
    `uvm_field_int(data, UVM_ALL_ON)
    `uvm_field_int(valid, UVM_ALL_ON)
  `uvm_object_utils_end
  
  function new(string name = "my_transaction");
    super.new(name);
  endfunction
endclass

注意这里用的是 uvm_sequence_item,它是 uvm_transaction 的子类,而 uvm_transaction 又是 uvm_object 的子类。为什么不用直接 uvm_object?因为 uvm_sequence_item 提供了sequence机制的支持,以后扩展起来方便。

小技巧: 我习惯在transaction里加上 do_print 函数,方便调试时看数据内容。虽然UVM有默认的print机制,但自定义格式更清晰。

2.4 第二步:写driver(uvm_component)

driver负责从sequencer拿transaction,然后“驱动”到接口上。但我们现在没有接口,所以先简单打印出来。

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);
    phase.raise_objection(this);
    repeat(5) begin
      seq_item_port.get_next_item(req);
      $display("[Driver] Got transaction: data = %0d, valid = %0b", req.data, req.valid);
      seq_item_port.item_done();
    end
    phase.drop_objection(this);
  endtask
endclass

这里有个关键点:uvm_driver #(my_transaction) 这个参数化写法。它告诉UVM这个driver处理的是哪种transaction。我在项目中见过有人忘了写参数,结果driver和sequencer连不上,报了一堆类型不匹配的错误。

2.5 第三步:写monitor(uvm_component)

monitor负责“监听”接口上的活动。同样,我们现在没有接口,就模拟接收一下。

class my_monitor extends uvm_monitor;
  `uvm_component_utils(my_monitor)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  virtual task run_phase(uvm_phase phase);
    // 模拟接收5个transaction
    repeat(5) begin
      #10;
      $display("[Monitor] Observed activity at time %0t", $time);
    end
  endtask
endclass

注意: monitor里不要调用 raise_objection 和 drop_objection。objection的控制应该由test或sequence来管理。monitor只是被动观察,不控制仿真结束。

2.6 第四步:组装agent、env和test

agent把driver和monitor包在一起。env把agent包起来。test是顶层。

// agent
class my_agent extends uvm_agent;
  `uvm_component_utils(my_agent)
  
  my_driver    driver;
  my_monitor   monitor;
  uvm_sequencer #(my_transaction) sequencer;
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    driver    = my_driver::type_id::create("driver", this);
    monitor   = my_monitor::type_id::create("monitor", this);
    sequencer = uvm_sequencer #(my_transaction)::type_id::create("sequencer", this);
  endfunction
  
  virtual function void connect_phase(uvm_phase phase);
    driver.seq_item_port.connect(sequencer.seq_item_export);
  endfunction
endclass

// env
class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  
  my_agent agent;
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent = my_agent::type_id::create("agent", this);
  endfunction
endclass

// test
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  
  my_env env;
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);
  endfunction
  
  virtual function void end_of_elaboration_phase(uvm_phase phase);
    uvm_top.print_topology();
  endfunction
endclass

注意看 connect_phase 里的那行代码:driver.seq_item_port.connect(sequencer.seq_item_export)。这是driver和sequencer之间的“管道”。没有这行,driver就拿不到transaction。

2.7 第五步:跑起来!

最后,在顶层模块里启动UVM:

module top;
  initial begin
    run_test("my_test");
  end
endmodule

用VCS或QuestaSim跑一下,你会看到类似这样的输出:

UVM_INFO @ 0: reporter [RNTST] Running test my_test...
[Driver] Got transaction: data = 123, valid = 1
[Monitor] Observed activity at time 10
[Driver] Got transaction: data = 45, valid = 0
[Monitor] Observed activity at time 20
...

看到这个输出,恭喜你,你的第一个UVM环境跑通了!

2.8 避坑指南:我踩过的几个坑

  • 忘记调用 super.build_phase:这是最常见的错误。如果不调,父类的build不会执行,factory机制会出问题。
  • objection管理不当:我曾经在一个项目里忘了在test里raise objection,结果仿真一启动就结束了,什么都没看到。
  • 类型不匹配:driver和sequencer的参数类型必须一致。比如driver是 #(my_transaction),sequencer也得是 #(my_transaction)。

2.9 小结

这一章我们做了三件事:

  1. 理解了 uvm_component 和 uvm_object 的本质区别
  2. 搭建了一个完整的UVM环境(虽然很简单)
  3. 看到了transaction从driver发出的全过程

这个环境虽然简陋,但它包含了UVM的核心骨架。后面的所有内容,都是在这个骨架上添砖加瓦。

记住: 能跑通的环境就是好环境。别追求一开始就完美,先跑起来再说。

可可精品资料库