UVM 高阶用法

第 2 章 · 共 6 章

AXI4-Lite Driver、Monitor 与 Agent

把 axil_regfile 包进真正的 UVM:一个 axi_txn sequence item、一个终于要等 READY 而不是组合驱动的 driver、一个从两个独立通道重建事务的 monitor,两者一起打包成 axi_agent——直接复用 UVM 基础系列的 seq_item_port/analysis_port 机制,因为这里真正新的东西只有握手本身。

第 1 章是手动驱动 axil_regfile 的——两个纯任务(axi_write/axi_read),直接从 initial 块里调用。这一章把完全相同的驱动逻辑包进 UVM:一个 sequence item、一个 driver、一个 monitor,外加一个 uvm_agent——全部用 uvm 第 8 章已经教过的机制搭出来(seq_item_portanalysis_port、一个 virtual interface 句柄)。UVM 这套管道本身没有任何新东西。真正新的是管道另一头连着什么:一次真正的握手,第一次能让 driver 等待——mux2 是组合逻辑,uvm 第 8 章的 driver 从来没有哪次需要停下来等一个 ready 信号。

axi_txn:一个 item,两个方向

一种事务类型同时覆盖读和写,用 is_write 区分——沿用 uvm 第 8 章给 mux_transactiony 字段时定下的先例,让一个 transaction 既能携带驱动出去的内容,也能携带 DUT 产生的内容,axi_txn 同时携带 sequence 填的字段(addrwdata)和事务完成后 driver 填回来的字段(rdataresp):

class axi_txn extends uvm_sequence_item;
  rand bit        is_write;
  rand bit [7:0]  addr;
  rand bit [31:0] wdata;
       bit [31:0] rdata;
       bit [1:0]  resp;
 
  `uvm_object_utils(axi_txn)
 
  function new(string name = "axi_txn");
    super.new(name);
  endfunction
 
  function void do_copy(uvm_object rhs);
    axi_txn rhs_;
    if (!$cast(rhs_, rhs)) `uvm_fatal("AXI_TXN", "do_copy: cast failed")
    super.do_copy(rhs);
    is_write = rhs_.is_write;
    addr     = rhs_.addr;
    wdata    = rhs_.wdata;
    rdata    = rhs_.rdata;
    resp     = rhs_.resp;
  endfunction
 
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    axi_txn rhs_;
    if (!$cast(rhs_, rhs)) return 0;
    return (is_write == rhs_.is_write) && (addr == rhs_.addr) &&
           (wdata == rhs_.wdata) && (rdata == rhs_.rdata) && (resp == rhs_.resp);
  endfunction
 
  function string convert2string();
    return is_write ?
      $sformatf("WRITE addr=0x%0h wdata=0x%0h resp=%0d", addr, wdata, resp) :
      $sformatf("READ  addr=0x%0h rdata=0x%0h resp=%0d", addr, rdata, resp);
  endfunction
endclass

手写的 do_copy/do_compare/convert2stringuvm_object_utils——完全是 uvm 第 4 章的写法,这里也没有新东西。

Driver:第一次真正遇到反压(backpressure)

driver 的 drive() 任务直接照搬第 1 章的 axi_write/axi_read 任务——同样的握手、同样通过 clocking block 访问,只是现在活在一个类方法里,而不是一个裸的 initial 块:

class axi_driver extends uvm_driver #(axi_txn);
  `uvm_component_utils(axi_driver)
 
  virtual axi4lite_if.tb_mp vif;
 
  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 axi4lite_if.tb_mp)::get(this, "", "vif", vif))
      `uvm_fatal("AXI_DRV", "virtual interface not set")
  endfunction
 
  task run_phase(uvm_phase phase);
    // axi4lite_if.tb_mp 只暴露了 clocking block(第 1 章),没有直接暴露
    // aresetn——driver 没法自己感知复位,所以只是等过 top-level testbench
    // 用的那个固定复位窗口。这和第 1 章手动驱动的 testbench 用的是同一个
    // 简化,只是现在藏在类里面了,这里明说一下。
    repeat (4) @(vif.cb);
    vif.cb.awvalid <= 1'b0;
    vif.cb.wvalid  <= 1'b0;
    vif.cb.bready  <= 1'b1;
    vif.cb.arvalid <= 1'b0;
    vif.cb.rready  <= 1'b1;
 
    forever begin
      seq_item_port.get_next_item(req);
      drive(req);
      seq_item_port.item_done();
    end
  endtask
 
  task drive(axi_txn txn);
    if (txn.is_write) begin
      vif.cb.awaddr  <= txn.addr;
      vif.cb.awvalid <= 1'b1;
      vif.cb.wdata   <= txn.wdata;
      vif.cb.wstrb   <= 4'hF;
      vif.cb.wvalid  <= 1'b1;
      @(vif.cb);
      while (!(vif.cb.awready && vif.cb.wready)) @(vif.cb);
      vif.cb.awvalid <= 1'b0;
      vif.cb.wvalid  <= 1'b0;
      while (!vif.cb.bvalid) @(vif.cb);
      txn.resp = vif.cb.bresp;
    end else begin
      vif.cb.araddr  <= txn.addr;
      vif.cb.arvalid <= 1'b1;
      @(vif.cb);
      while (!vif.cb.arready) @(vif.cb);
      vif.cb.arvalid <= 1'b0;
      while (!vif.cb.rvalid) @(vif.cb);
      txn.rdata = vif.cb.rdata;
      txn.resp  = vif.cb.rresp;
    end
  endtask
endclass

get_next_item/item_done 正是 uvm 第 7 章预告、第 8 章完整展开的那套 sequencer/driver 双向协议——requvm_driver #(axi_txn) 自带的句柄,由 get_next_item 填好。真正新的地方是 drive() 里那两个 while 循环:mux2 是组合逻辑,uvm 第 8 章的 driver 从来没有遇到过"已经拉高了某个信号,还得原地等 DUT 跟上"这种情况。这里,axil_regfile 可以(而且按 spec 是允许的)把 awready/wready/arready 拉低任意多个周期——driver 必须写得能容忍这一点,而不是想当然地假设一个周期总够用。

Monitor:从两个独立通道重建事务

monitor 什么都不驱动——它观察和 driver 相同的 vif.cb(复用 driver 的 tb_mp 视图,而不是单独搞一个只读视图,这和 uvm 第 8 章给 mux2_if 的 monitor 标注过的简化是同一个道理),把完成的事务通过 analysis_port 报告出去:

class axi_monitor extends uvm_monitor;
  `uvm_component_utils(axi_monitor)
 
  virtual axi4lite_if.tb_mp vif;
  uvm_analysis_port #(axi_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 axi4lite_if.tb_mp)::get(this, "", "vif", vif))
      `uvm_fatal("AXI_MON", "virtual interface not set")
  endfunction
 
  task run_phase(uvm_phase phase);
    fork
      watch_write();
      watch_read();
    join
  endtask
 
  task watch_write();
    bit [7:0]  addr_q;
    bit [31:0] data_q;
    forever begin
      @(vif.cb);
      if (vif.cb.awvalid && vif.cb.awready) addr_q = vif.cb.awaddr;
      if (vif.cb.wvalid  && vif.cb.wready)  data_q = vif.cb.wdata;
      if (vif.cb.bvalid  && vif.cb.bready) begin
        axi_txn txn = axi_txn::type_id::create("txn");
        txn.is_write = 1'b1;
        txn.addr     = addr_q;
        txn.wdata    = data_q;
        txn.resp     = vif.cb.bresp;
        ap.write(txn);
      end
    end
  endtask
 
  task watch_read();
    bit [7:0] addr_q;
    forever begin
      @(vif.cb);
      if (vif.cb.arvalid && vif.cb.arready) addr_q = vif.cb.araddr;
      if (vif.cb.rvalid  && vif.cb.rready) begin
        axi_txn txn = axi_txn::type_id::create("txn");
        txn.is_write = 1'b0;
        txn.addr     = addr_q;
        txn.rdata    = vif.cb.rdata;
        txn.resp     = vif.cb.rresp;
        ap.write(txn);
      end
    end
  endtask
endclass

fork ... joinwatch_write()watch_read() 作为两个独立并发的进程运行——这正是 systemverilog-basics 第 14 章教过的构造,这次真正派上了用场:写通道和读通道确实需要各自独立的观察者,因为它们可能同时都处在事务进行中。注意 watch_write()axil_regfile 自己(第 1 章)的写接受逻辑要简单——DUT 得决定自己是不是准备好接受新的 AW/W(这正是 aw_have/w_have 和那些 ready 信号存在的原因),但 monitor 只需要记住它看到的最新地址和数据,然后跟下一次 BVALID 配对——由于一次只有一笔事务在途(第 1 章的 DUT 设计),到 BVALID 出现的时候,最近一次捕获的 addr_q/data_q 必然就是对的。

打包成 axi_agent

class axi_sequencer extends uvm_sequencer #(axi_txn);
  `uvm_component_utils(axi_sequencer)
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass
 
class axi_agent extends uvm_agent;
  `uvm_component_utils(axi_agent)
 
  axi_sequencer sqr;
  axi_driver    drv;
  axi_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 = axi_monitor::type_id::create("mon", this);
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = axi_sequencer::type_id::create("sqr", this);
      drv = axi_driver::type_id::create("drv", 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 第 8 章介绍过的 uvm_agent 写法:get_is_active() == UVM_ACTIVE 决定 sequencer/driver 到底建不建——只要换个配置,同一个 agent 就能复用成一个纯被动、只负责观察的角色,第 3 章会把这个特性真正用在 irq_agent 上。

一个 sequence 和一个 test:同样的场景,现在由 UVM 驱动

class axi_basic_seq extends uvm_sequence #(axi_txn);
  `uvm_object_utils(axi_basic_seq)
 
  function new(string name = "axi_basic_seq");
    super.new(name);
  endfunction
 
  task write(bit [7:0] addr, bit [31:0] data);
    axi_txn req = axi_txn::type_id::create("req");
    start_item(req);
    req.is_write = 1'b1;
    req.addr     = addr;
    req.wdata    = data;
    finish_item(req);
  endtask
 
  task read(bit [7:0] addr);
    axi_txn req = axi_txn::type_id::create("req");
    start_item(req);
    req.is_write = 1'b0;
    req.addr     = addr;
    finish_item(req);
  endtask
 
  task body();
    write(8'h00, 32'h1);   // CTRL: ENABLE=1
    write(8'h08, 32'hAA);  // DATA write #1
    write(8'h08, 32'hBB);  // DATA write #2
    write(8'h08, 32'hCC);  // DATA write #3
    write(8'h08, 32'hDD);  // DATA write #4 -> COUNT=4 == IRQ_THRESHOLD
    read(8'h0C);            // COUNT
    read(8'h04);            // STATUS -- expect bit0=1
    write(8'h00, 32'h3);   // CTRL: ENABLE=1, IRQ_CLR=1
    read(8'h04);            // STATUS -- expect bit0=0 again
    read(8'h10);            // unmapped -> DECERR
  endtask
endclass
 
class axi_smoke_test extends uvm_test;
  `uvm_component_utils(axi_smoke_test)
 
  axi_agent agt;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agt = axi_agent::type_id::create("agt", 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(agt.sqr);
    phase.drop_objection(this);
  endtask
endclass

start_item/finish_itemuvm 第 7 章)、工厂的 type_id::create()uvm 第 6 章)、把 objection 包在 test 的阻塞调用 seq.start(sqr) 外面(uvm 第 7 章自己定下的"一旦 run_phase 不再是手写循环,objection 就从 driver 挪到 test"这个决定)——这个 sequence 和 test 完全是用读者已经掌握的材料拼出来的。沿用第 1 章的 axil_regfileaxi4lite_if(原样不动),top-level module 走的是 uvm 第 3 章那套 config_db 交接的写法:

`include "uvm_macros.svh"
import uvm_pkg::*;
 
module tb_top;
  logic aclk;
  logic irq;
 
  axi4lite_if axi_if (.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)
  );
 
  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);
    run_test("axi_smoke_test");
  end
endmodule

这一章的例子需要一个既支持 UVM、又完整实现 clocking block 的仿真器——Icarus Verilog 两条都不满足。在 EDA Playground 上选 Aldec Riviera-PRO(免费,不需要你自己的授权——和第 1 章、SV 第 13 章指向的是同一个替代方案);它一次满足这两个要求。

小结

  • axi_txn 同时携带 sequence 提出的请求(addr/wdata)和事务完成后 driver 观察到的结果(rdata/resp)——和 uvm 第 8 章给 mux_transactiony 字段是同一种"一个 transaction 同时携带两个方向"的写法。
  • driver 的 drive() 任务本质上就是第 1 章手写的那两个任务,没有实质改变,只是现在由 get_next_item/item_done 调用,而不是裸的 initial 块。
  • 真正新的东西是反压:axil_regfile 可以把 READY 拉低任意多个周期,driver 必须写得能等它——mux2 从来不需要这个。
  • monitor 重建已完成事务所需的记账比 DUT 接受它们所需的要少——只需要记住最新的地址/数据,跟下一次响应配对,因为一次只有一笔事务在途。
  • axi_agent 就是 uvm 第 8 章那套 uvm_agent 写法——get_is_active() 决定 sequencer/driver 到底存不存在,为第 3 章被动的 irq_agent 做好了铺垫。
  • 这一章的例子既需要支持 UVM 的仿真器,也需要完整支持 clocking block 的仿真器——Icarus Verilog 两个都没有;用 EDA Playground 上的 Aldec Riviera-PRO。

为什么 axi_driver 的 drive() 任务需要一个等待 awready/wready 的 while 循环,而 uvm 第 8 章 mux2 的 driver 从来不需要类似的东西?

axi_monitor 的 watch_write() 为什么比第 1 章 axil_regfile 自己的写接受逻辑简单,尽管两者重建的是同一个 AW/W 独立性?

axi_txn 的哪个字段用来区分一次读事务和一次写事务?