一、UVM核心组件(上):从uvm_component到uvm_monitor

各位同学好,我是老李。今天咱们聊聊UVM里最基础也最绕不开的几个组件。说实话,我刚开始学UVM那会儿,也被uvm_component和uvm_object的区别搞得晕头转向。后来踩了几个坑,才慢慢摸清楚门道。

1.1 uvm_component vs uvm_object:到底差在哪?

这个问题,几乎每个UVM新手都会问。我个人的理解是这样的:

uvm_object是UVM中最基本的类。它没有时间概念,不参与仿真过程。说白了,它就是用来装数据的。比如你定义一个transaction,里面放地址、数据、控制信号,这就是典型的uvm_object。

uvm_component则不一样。它是有生命的,有仿真阶段的(phase)。它会创建、运行、销毁。你想想看,driver、monitor、agent这些,哪个不是在仿真过程中干活的?它们就是uvm_component。

核心区别一句话总结:

  • uvm_object:数据载体,不参与仿真流程
  • uvm_component:功能单元,参与仿真,有层次结构

我在项目中遇到过这样一个坑:有人把monitor写成了uvm_object,结果发现它根本收不到transaction。为什么?因为uvm_object没有phase机制,没法在run_phase里干活。嗯,这里要注意,凡是需要在仿真过程中持续运行的,必须是uvm_component。

来看个代码对比:

// uvm_object:纯数据
class my_transaction extends uvm_object;
  rand bit [31:0] addr;
  rand bit [31:0] data;
  `uvm_object_utils(my_transaction)
endclass

// uvm_component:有行为的组件
class my_driver extends uvm_component;
  `uvm_component_utils(my_driver)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  task run_phase(uvm_phase phase);
    // 这里才能干活
  endtask
endclass

你看,uvm_object用uvm_object_utils注册,uvm_component用uvm_component_utils注册。这个区别不是随便定的,它背后是UVM的工厂机制在起作用。

1.2 uvm_agent:把组件串起来

agent是什么?说白了就是一个容器。它把driver、monitor、sequencer这些组件打包在一起。我习惯把agent理解成一个"小环境",负责管理某个接口的所有组件。

举个例子,你的DUT有一个AXI接口,那你就需要一个AXI agent。这个agent里面包含:

  • 一个driver:负责驱动信号
  • 一个monitor:负责监视信号
  • 一个sequencer:负责管理sequence

agent有两种模式:active和passive。active模式下,agent会包含driver和sequencer,可以主动发送激励。passive模式下,只有monitor,只做观察。这个设计很巧妙,我在做验证IP复用时经常用到。

class my_agent extends uvm_agent;
  my_driver    drv;
  my_monitor   mon;
  my_sequencer seqr;
  
  `uvm_component_utils(my_agent)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    mon = my_monitor::type_id::create("mon", this);
    if(get_is_active() == UVM_ACTIVE) begin
      drv  = my_driver::type_id::create("drv", this);
      seqr = my_sequencer::type_id::create("seqr", this);
    end
  endfunction
endclass

注意看,我用了get_is_active()来判断模式。这个函数是uvm_agent自带的,你不需要自己实现。我曾经见过有人自己写了个变量来控制模式,结果跟UVM内置机制冲突了,调试了半天。

1.3 uvm_driver:激励的搬运工

driver的任务很简单:从sequencer拿到transaction,然后驱动到DUT的接口上。但简单不代表容易。我见过太多人把driver写得又臭又长。

好的driver应该遵循几个原则:

  • 只做驱动,不做判断
  • 只处理时序,不处理数据
  • 只负责发送,不负责检查

来看一个标准的driver模板:

class my_driver extends uvm_driver #(my_transaction);
  virtual my_if vif;
  
  `uvm_component_utils(my_driver)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(!uvm_config_db #(virtual my_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "virtual interface not found")
  endfunction
  
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      drive_transaction(req);
      seq_item_port.item_done();
    end
  endtask
  
  task drive_transaction(my_transaction tr);
    @(posedge vif.clk);
    vif.addr <= tr.addr;
    vif.data <= tr.data;
    vif.valid <= 1'b1;
    @(posedge vif.clk);
    vif.valid <= 1'b0;
  endtask
endclass

这里有个细节:seq_item_port.get_next_item和item_done必须成对出现。我曾经漏写过item_done,结果sequencer卡死了,整个仿真跑不下去。这种错误很难查,因为UVM不会报错,只是默默等待。

1.4 uvm_monitor:安静的观察者

monitor和driver正好相反。driver是往外发信号,monitor是往里收信号。它默默地观察接口上的信号变化,然后打包成transaction,发给scoreboard或者coverage collector。

monitor的设计要点:

  • 不能修改信号,只能观察
  • 要能处理各种时序异常
  • 最好支持协议检查

个人经验:我习惯在monitor里加一个简单的协议检查。比如检查地址对齐、数据有效信号是否在正确的时间拉高。这样可以在早期发现问题,不用等到scoreboard去比对。

class my_monitor extends uvm_monitor;
  virtual my_if vif;
  uvm_analysis_port #(my_transaction) ap;
  
  `uvm_component_utils(my_monitor)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
    ap = new("ap", this);
  endfunction
  
  task run_phase(uvm_phase phase);
    forever begin
      my_transaction tr = my_transaction::type_id::create("tr");
      @(posedge vif.clk);
      if(vif.valid) begin
        tr.addr = vif.addr;
        tr.data = vif.data;
        ap.write(tr);
      end
    end
  endtask
endclass

注意看,monitor通过uvm_analysis_port把数据发出去。这个port是广播式的,可以连到多个组件。我在项目中经常一个monitor同时连scoreboard和coverage collector,省事。

避坑指南:我曾经在monitor里直接用了$monitor系统函数,结果仿真速度慢得像蜗牛。后来才意识到,UVM有自己的观察机制,不要用Verilog的那一套。用uvm_analysis_port才是正道。

知识体系总览

下面这张图,是我自己总结的UVM核心组件关系。你看一眼,应该能明白它们是怎么配合的。

UVM核心组件关系图 uvm_object 数据载体,无仿真阶段 uvm_component 功能单元,有仿真阶段 继承 uvm_agent 容器,管理接口组件 uvm_driver 驱动激励到DUT uvm_monitor 观察信号,打包数据 包含 包含 uvm_sequencer 管理sequence流 TLM通信 DUT 待测设计 驱动信号 监视信号 图例: 数据类 组件类 容器 驱动 监视 序列器 DUT

这张图里,我特意把uvm_object和uvm_component放在最上面,因为它们是所有组件的基础。往下看,agent包含了driver和monitor,而driver又和sequencer通过TLM通信。最后,driver和monitor都跟DUT打交道——一个发信号,一个收信号。

好了,这一章的内容就到这里。记住一句话:uvm_object装数据,uvm_component干活,agent打包,driver发,monitor收。这个口诀我用了好多年,从来没出过错。

可可精品资料库