UVM 高阶用法

第 3 章 · 共 6 章

第二个接口:irq Monitor 与多 Agent 环境

给 axil_regfile 的 irq 输出配一个最小化的独立接口,搭一个纯被动的 irq_agent 去观察它(完全没有 driver——这个信号上根本没有 testbench 能驱动的东西),再把它和 axi_agent 一起装进一个 uvm_env:这正是 uvm 第 10 章留下的“协调不止一个 agent”这个缺口的直接兑现。

uvm 第 10 章结尾点名了单 agent 环境做不到的事:"协调不止一个 agent……真实的 DUT 通常不止一个接口。"axil_regfile 从第 1 章起就有这个第二接口——irq——但到目前为止它一直只是一根裸线,在一个普通的 initial 块里直接读、或者用 $display 打印。这一章给它配上和 axi4lite_if 一样的待遇:自己的 interface、自己的 monitor,以及在一个真正的多 agent 环境里的一席之地。

irq_if:一个最小化的被动接口

irq 从头到尾只需要被——这个系列里从来没有谁会去驱动它,因为它是 DUT 自己的输出。这让接口本身简化到几乎没什么可写的:一个信号,一个只有输入侧的 clocking block,完全不需要配置输出偏移:

interface irq_if (input logic aclk);
  logic irq;
 
  clocking cb @(posedge aclk);
    default input #1step;
    input irq;
  endclocking
 
  modport dut_mp (input aclk, output irq);
  modport mon_mp (clocking cb);
endinterface

axi4lite_if(SV 第 13 章)用的是同一套 input #1step 采样保证——拿到的是时钟边沿前一刻已经稳定的值,这样 monitor 读 irq 的时候,永远不会跟 DUT 自己在同一个边沿的寄存器更新赛跑。dut_mp 从 DUT 一侧把 irq 声明成输出axil_regfile 就是从这里驱动它的);mon_mpaxi4lite_iftb_mp 的镜像——一个只暴露 clocking block 的 modport,只不过这次是给一个只读不写的组件用的。

irq_txnirq_monitor:报告变化沿,而不是电平

观察一个电平信号的 monitor 有两种选择:每个周期都报告一次当前值,或者只在值变化时才报告。每周期都报告会让 analysis port 被一连串"还在拉高"的重复事件淹没;只报告变化沿才是真正有用的——"irq 刚刚拉高了"是一个有意义的事件,"irq 已经拉高了一千个周期"只是噪声。

class irq_txn extends uvm_sequence_item;
  rand bit level;
 
  `uvm_object_utils(irq_txn)
 
  function new(string name = "irq_txn");
    super.new(name);
  endfunction
 
  function void do_copy(uvm_object rhs);
    irq_txn rhs_;
    if (!$cast(rhs_, rhs)) `uvm_fatal("IRQ_TXN", "do_copy: cast failed")
    super.do_copy(rhs);
    level = rhs_.level;
  endfunction
 
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    irq_txn rhs_;
    if (!$cast(rhs_, rhs)) return 0;
    return (level == rhs_.level);
  endfunction
 
  function string convert2string();
    return $sformatf("IRQ %s", level ? "ASSERTED" : "DEASSERTED");
  endfunction
endclass
 
class irq_monitor extends uvm_monitor;
  `uvm_component_utils(irq_monitor)
 
  virtual irq_if.mon_mp vif;
  uvm_analysis_port #(irq_txn) ap;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
    ap = new("ap", this);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(virtual irq_if.mon_mp)::get(this, "", "vif", vif))
      `uvm_fatal("IRQ_MON", "virtual interface not set")
  endfunction
 
  task run_phase(uvm_phase phase);
    bit last = 1'b0;
    forever begin
      @(vif.cb);
      if (vif.cb.irq !== last) begin
        irq_txn txn = irq_txn::type_id::create("txn");
        txn.level = vif.cb.irq;
        ap.write(txn);
        last = vif.cb.irq;
      end
    end
  endtask
endclass

irq_txn 是一个 uvm_sequence_item,尽管从来没有谁会真的去 sequence 它——它用的是和 axi_txn 同一条基类链(uvm 第 4 章的 uvm_objectuvm_transactionuvm_sequence_item),这里纯粹是拿它当一个方便、经过工厂注册、能配合 analysis port 使用的数据容器。uvm_analysis_port/ap.write() 完全是第 2 章 axi_monitor 的写法,原封不动。

irq_agent:为什么没有 active 分支

第 2 章的 axi_agentget_is_active() == UVM_ACTIVE 来决定要不要建 sequencer 和 driver——同一个类,配置不同就能两种都跑。irq_agent 根本不需要做这个选择:

class irq_agent extends uvm_agent;
  `uvm_component_utils(irq_agent)
 
  irq_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 = irq_monitor::type_id::create("mon", this);
  endfunction
endclass

这里没有 sqr/drv 字段,没有 get_is_active() 判断,压根就没有 active 分支——不是这个 agent 忘了支持,而是因为 irq 这个接口上根本没有 testbench 能去驱动的东西。axi_agent 确实可能被配成被动模式跑(比如某个 test 只想观察 AXI 流量,不想生成它),因为它两个分支都建好了;irq_agent 在结构上就做不到,因为驱动 irq 的永远只有 DUT 自己。uvm 第 8 章的 active/passive 之分,说的是同一个接口,不同的 test 可能想做出不同的选择——而这里是接口本身就取消了这个选择。

regfile_env:两个 agent 归到同一个父节点下

class irq_watcher extends uvm_component;
  `uvm_component_utils(irq_watcher)
 
  uvm_analysis_imp #(irq_txn, irq_watcher) imp;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
    imp = new("imp", this);
  endfunction
 
  function void write(irq_txn txn);
    `uvm_info("IRQ_WATCH", txn.convert2string(), UVM_LOW)
  endfunction
endclass
 
class regfile_env extends uvm_env;
  `uvm_component_utils(regfile_env)
 
  axi_agent   axi_agt;
  irq_agent   irq_agt;
  irq_watcher irqw;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    axi_agt = axi_agent::type_id::create("axi_agt", this);
    irq_agt = irq_agent::type_id::create("irq_agt", this);
    irqw    = irq_watcher::type_id::create("irqw", this);
  endfunction
 
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    irq_agt.mon.ap.connect(irqw.imp);
  endfunction
endclass

uvm_env 打包不止一个 agent 完全是 uvm 第 9 章的写法(那里打包的是一个 agent 加一个 scoreboard);irq_watcheruvm_analysis_imp #(irq_txn, irq_watcher) + write(),正是第 9 章 scoreboard 用来真正接收 monitor 广播内容的那套接收端机制,不只是发送端。这里没有任何新的 UVM 表面——就是第 9 章的同两个机制,套用在第二个 agent 上,而不是 scoreboard 上。

看它跑起来

沿用第 2 章的 axi_txn/axi_driver/axi_monitor/axi_sequencer/axi_agent/axi_basic_seq(原样不动),test 现在建的是 regfile_env,而不是一个裸的 axi_agent

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

top-level module 加上 irq_if,把它接到 DUT 的 irq 端口上,而不是一根裸的 logic,再通过 config_db 多发布一个 virtual interface——同样的调用,只是换了个类型参数,不会冲突,因为 uvm_config_db#(virtual axi4lite_if.tb_mp)uvm_config_db#(virtual irq_if.mon_mp) 是同一个类的两种不同参数化实例,各自有各自的查找表:

`include "uvm_macros.svh"
import uvm_pkg::*;
 
module tb_top;
  logic aclk;
 
  axi4lite_if axi_if (.aclk(aclk));
  irq_if      irq_intf (.aclk(aclk));
 
  axil_regfile #(.IRQ_THRESHOLD(4)) dut (
    .s_axi_aclk    (axi_if.aclk),
    .s_axi_aresetn (axi_if.aresetn),
    .s_axi_awaddr  (axi_if.awaddr),
    .s_axi_awprot  (axi_if.awprot),
    .s_axi_awvalid (axi_if.awvalid),
    .s_axi_awready (axi_if.awready),
    .s_axi_wdata   (axi_if.wdata),
    .s_axi_wstrb   (axi_if.wstrb),
    .s_axi_wvalid  (axi_if.wvalid),
    .s_axi_wready  (axi_if.wready),
    .s_axi_bresp   (axi_if.bresp),
    .s_axi_bvalid  (axi_if.bvalid),
    .s_axi_bready  (axi_if.bready),
    .s_axi_araddr  (axi_if.araddr),
    .s_axi_arprot  (axi_if.arprot),
    .s_axi_arvalid (axi_if.arvalid),
    .s_axi_arready (axi_if.arready),
    .s_axi_rdata   (axi_if.rdata),
    .s_axi_rresp   (axi_if.rresp),
    .s_axi_rvalid  (axi_if.rvalid),
    .s_axi_rready  (axi_if.rready),
    .irq           (irq_intf.irq)
  );
 
  initial aclk = 1'b0;
  always #5 aclk = ~aclk;
 
  initial begin
    axi_if.aresetn = 1'b0;
    repeat (3) @(posedge aclk);
    axi_if.aresetn = 1'b1;
  end
 
  initial begin
    uvm_config_db#(virtual axi4lite_if.tb_mp)::set(null, "*", "vif", axi_if);
    uvm_config_db#(virtual irq_if.mon_mp)::set(null, "*", "vif", irq_intf);
    run_test("axi_smoke_test");
  end
endmodule

现在跑 axi_basic_seq 那套场景,会打印出两行这一章之前从没出现过的 uvm_info,都来自 irq_watcher——第四次 DATA 写之后紧跟着 IRQ ASSERTEDCTRL.IRQ_CLR 那次写之后紧跟着 IRQ DEASSERTED——还是同一套环境,现在真正在观察第二个接口了,而不只是驱动第一个。

小结

  • irq_if 就是把 axi4lite_if 的写法精简到一个纯被观察信号所需要的最小限度:一个 clocking block,只有输入,没有输出偏移需要配置。
  • irq_monitor 报告的是变化沿,不是电平——vif.cb.irq !== last 把一千个相同的周期,变成每次拉高/拉低各自恰好一个有意义的事件。
  • irq_agent 完全没有 active 分支,跟 axi_agent 不一样——不是遗漏,而是 irq 这个信号除了 DUT 之外没人会驱动的直接结果。
  • regfile_env 把两个 agent 打包进同一个 uvm_env,是 uvm 第 9 章的写法;irq_watcheruvm_analysis_imp/write() 就是第 9 章 scoreboard 用过的同一套接收端机制,这次用在第二个 agent 上。
  • 这正是 uvm 第 10 章那个明确点名的缺口的直接兑现:一个真正协调不止一个 agent 的环境,这是这个网站历史上第一次。

为什么 irq_agent 完全没有 active 分支,而 axi_agent 的 build_phase 明确检查了 get_is_active()?

为什么 irq_monitor 要检查 `vif.cb.irq !== last`,而不是每次 clocking event 都广播一个 irq_txn?

regfile_env 继承自哪个 UVM 基类?uvm 第 9 章用同一个基类把一个 agent 和一个 scoreboard打包在一起。