UVM 基础

第 8 章 · 共 10 章

Driver 与 Monitor:TLM 端口实战

monitor 那种 push 风格的 analysis_port,相对 sequencer 那种 pull 风格 seq_item_port 的区别;为什么它取代了 SV 第 14 章手写的 mailbox;给 mux_transaction 加上一个观测到的输出字段;以及把 sequencer/driver/monitor 打包成一个可配置的 uvm_agent。

第 7 章已经给了 driver 它的 seq_item_port 和最终的 run_phase 形态。第 1 章第四条承诺——可复用的验证 IP,而不是一套定制的 mailbox 握手——还缺的另一半是:一个 monitor,以及它广播观测结果的标准化方式。

两种 TLM 端口形态:pull 与 push

第 7 章的 seq_item_port 是一个 pull:driver 主动调用 get_next_item(),并阻塞直到有东西可用。monitor 的工作不一样——它不消费一个待处理任务队列,只是盯着 DUT,把看到的东西通知给不管有多少个监听者(第 9 章会接上一个 scoreboard;理论上可以同时接不止一个,虽然这个系列只会接一个)。这就是一个 pushuvm_analysis_port#(T),通过 write(T t) 调用把 t 立刻交给每一个连接上的接收方。

这正是 analysis_port 能取代 capstone 里手写的 mailbox(SV 第 14 章)的真正原因,而不只是给同一个东西换了个名字:mailbox 是为恰好一个进程排队等待稍后 get(),一次一个。write() 完全不排队——它立刻同步地调用每一个连接上的接收方自己的 write() 方法,不管当前有零个、一个还是好几个监听者,行为都一样。monitor 不需要知道也不需要关心下游是谁,它只管调用 write(),然后继续。

给 mux_transaction 加上观测到的输出

第 4 章的 mux_transaction 只需要 sel/a/b——driver 要驱动的全部东西。monitor 还需要捕获 DUT 实际产生的结果,所以它多了一个字段:

class mux_transaction extends uvm_sequence_item;
  `uvm_object_utils(mux_transaction)
 
  rand bit sel;
  rand bit a;
  rand bit b;
  bit      y;  // 观测到的 DUT 输出 -- 不随机化,由 monitor 填入
 
  // ... 构造函数不变 ...
 
  function void do_copy(uvm_object rhs);
    mux_transaction rhs_;
    if (!$cast(rhs_, rhs))
      `uvm_fatal("DO_COPY", "rhs is not a mux_transaction")
    super.do_copy(rhs);
    sel = rhs_.sel;
    a   = rhs_.a;
    b   = rhs_.b;
    y   = rhs_.y;
  endfunction
 
  function string convert2string();
    return $sformatf("sel=%0b a=%0b b=%0b y=%0b", sel, a, b, y);
  endfunction
 
  // do_compare 也只需要加一行一样的比较:&& (y == rhs_.y)
endclass

y 不是 rand——任何用来记录 DUT 实际行为的字段都不应该被随机化。

写一个 monitor

class my_monitor extends uvm_monitor;
  `uvm_component_utils(my_monitor)
 
  virtual mux2_if.tb_mp vif;
  uvm_analysis_port #(mux_transaction) ap;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap = new("ap", this);
    if (!uvm_config_db#(virtual mux2_if.tb_mp)::get(this, "", "vif", vif))
      `uvm_fatal("MON", "no virtual interface set for vif -- check the config_db::set() call in the top module")
  endfunction
 
  task run_phase(uvm_phase phase);
    mux_transaction tr;
    forever begin
      @(vif.sel, vif.a, vif.b);
      #1;  // 等组合逻辑稳定下来,和 driver 用的是同一个等待
      tr = mux_transaction::type_id::create("tr");
      tr.sel = vif.sel;
      tr.a   = vif.a;
      tr.b   = vif.b;
      tr.y   = vif.y;
      ap.write(tr);
    end
  endtask
endclass

几点值得注意:

  • uvm_monitor 相比 uvm_component没有加任何东西——它纯粹是一个命名习惯,和 uvm_test(第 3 章)存在的理由一样。用它而不是纯 uvm_component,只是标记这个组件的角色。
  • ap = new("ap", this),不是 type_id::create(...) TLM 端口不是要经过 factory(第 6 章)的 uvm_component/uvm_object 子类——它们是普通构造出来的对象,所以用普通的 new(...) 调用才是正确、地道的构造方式。
  • monitor 复用了和 driver 完全一样的 virtual mux2_if.tb_mp 视图和完全一样的 config_db::get() 模式——第 5 章顶层 module 里那次带 "*" 通配符的 set(),本来就已经覆盖了任何查询 "vif" 的组件;monitor 只是又多了一个来查询的组件。
  • @(vif.sel, vif.a, vif.b) 等待这三个信号中任意一个变化,然后 #1 给组合逻辑留出和 driver 自己的 drive() task 一样的稳定时间——之后 vif.y 就反映了刚才驱动的结果。
  • 通过 tb_mp modport 读取 vif.sel/vif.a/vif.b 是合法的,尽管 tb_mp 把它们声明成了 output——modport 的 output 方向限制的是谁可以一个信号,不是谁可以一个你本来就能写的信号。(一个更精致的 testbench 会给 monitor 一个专属的只读 modport 视图;这个系列的 mux2_if 没有这么做,因为复用 tb_mp 更简单,而且同样合法。)
  • ap.write(tr) 现在还没有连接任何东西。 这一章结束时还没有任何监听者——那是第 9 章的工作。在一个没有连接任何东西的 analysis_port 上调用 write() 不是错误;它只是一次目前没有听众的广播。

打包起来:uvm_agent

第 1 章已经介绍过 uvm_agent——把 sequencer、driver、monitor 打包在一起,可以配置成 activepassive。现在三者都已经存在,打包它们几乎是机械性的工作:

class my_agent extends uvm_agent;
  `uvm_component_utils(my_agent)
 
  my_driver                        drv;
  uvm_sequencer #(mux_transaction) sqr;
  my_monitor                       mon;
 
  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);
      sqr = uvm_sequencer#(mux_transaction)::type_id::create("sqr", this);
    end
  endfunction
 
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (get_is_active() == UVM_ACTIVE)
      drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

uvm_agent 相比 uvm_component 只加了一样东西:一个内置的 is_active 字段(一个枚举类型,默认是 UVM_ACTIVE),这里通过 get_is_active() 读取。monitor 是无条件构造的——观测并不需要驱动。driver 和 sequencer 只有在 agent 处于 active 状态时才会被构造、被连接;一个 passive agent(配置方式和第 5 章讲的 config_db 一样,在 build_phase 运行之前设置 agent 的 is_active 字段)会同时丢掉这两者,只保留 monitor——适合那种应该被观测但绝不应该被驱动的总线一侧。

Test 也变简单了

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
 
  my_agent agent;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent = my_agent::type_id::create("agent", this);
  endfunction
 
  task run_phase(uvm_phase phase);
    my_sequence seq;
    phase.raise_objection(this);
    seq = my_sequence::type_id::create("seq");
    seq.start(agent.sqr);
    phase.drop_objection(this);
  endtask
endclass

my_test 不再单独管理 drv/sqr——my_agent 内部自己负责这部分连接,test 只需要通过它(agent.sqr)去启动一个 sequence。

小结

  • seq_item_port(第 7 章)是一个 pull:driver 主动请求,阻塞直到有东西可用。uvm_analysis_port#(T) 是一个 push:write(t) 立刻把 t 交给每一个连接上的接收方,不排队——这正是它能取代 SV 第 14 章 mailbox 的真正原因,而不只是换了个名字。
  • monitor 只观测不驱动:用和 driver 一样的 virtual interfaceconfig_db::get() 模式,但只读信号,从不给它们赋值。
  • uvm_monitor 相比 uvm_component 没有加任何东西——和 uvm_test 一样,是一个标记组件角色的命名习惯。
  • TLM 端口用普通的 new(...) 构造,不是 type_id::create(...)——它们不是注册进 factory 的类型。
  • uvm_agent 打包 sequencer、driver、monitor;get_is_active()(背后是一个内置的 is_active 字段)决定 driver/sequencer 是否会被构造、被连接,还是这个 agent 只保留 monitor(passive)。

除了名字不同,seq_item_port 的 pull 和 uvm_analysis_port 的 push 之间真正的功能差异是什么?

为什么 monitor 的 ap = new('ap', this); 用 new() 来写,而 my_monitor 本身却是用 my_monitor::type_id::create('mon', this) 构造的?

在 my_agent 的 build_phase 里,为什么 mon 是无条件构造的,而 drv 和 sqr 只有在 get_is_active() == UVM_ACTIVE 时才构造?

在这一章结束时,monitor 每次观测到活动都会调用 mon.ap.write(tr),但 mon.ap 还没连接任何东西。会发生什么?