Advanced UVM

Chapter 3 of 6

A Second Interface: irq Monitor and Multi-Agent Environments

Give axil_regfile's irq output its own minimal interface, build a passive irq_agent to watch it (no driver at all -- there's nothing on this signal a testbench could ever drive), and bundle it with axi_agent inside a uvm_env: the direct payoff of uvm ch10's 'coordinating more than one agent' gap.

Chapter 10 of uvm ended by naming exactly what a single-agent environment can't do: "coordinating more than one agent ... a real DUT usually has more than one interface." axil_regfile has had that second interface since chapter 1 — irq — but so far it's just been a bare wire, read directly off the DUT in a plain initial block or $display. This chapter gives it the same treatment axi4lite_if already got: its own interface, its own monitor, and a place in a real multi-agent environment.

irq_if: a minimal passive interface

irq only ever needs to be read — nothing in this track ever drives it, since it's the DUT's own output. That collapses the interface down to almost nothing: one signal, one clocking block with only an input side, no output skew to configure at all:

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

Same input #1step sampling guarantee axi4lite_if uses (SV ch13) — a stable, settled value from just before the clock edge, so a monitor reading irq never races the DUT's own register update at that same edge. dut_mp declares irq as an output from the DUT's side (this is where axil_regfile drives it); mon_mp is the mirror image of axi4lite_if's tb_mp — a modport that exposes just the clocking block, this time to a component that only ever reads.

irq_txn and irq_monitor: reporting edges, not levels

A monitor watching a level signal has a choice: report the value every cycle, or report only when it changes. Reporting every cycle would flood an analysis port with a stream of identical "still asserted" events; reporting edges is what's actually useful — "irq just went high" is a meaningful event, "irq is high" a thousand cycles running is noise.

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 is a uvm_sequence_item even though nothing ever sequences one — it's the same base class axi_txn uses (uvm ch4's uvm_objectuvm_transactionuvm_sequence_item chain), reused here purely as a convenient, factory-registered, analysis-port-friendly data container. uvm_analysis_port/ap.write() is exactly axi_monitor's pattern from chapter 2, unmodified.

irq_agent: why there's no active branch

Chapter 2's axi_agent used get_is_active() == UVM_ACTIVE to decide whether to build a sequencer and driver at all — the same class works either way, depending on configuration. irq_agent doesn't have that choice to make:

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

There's no sqr/drv field, no get_is_active() check, and no active branch at all — not because this agent forgot to support one, but because there's nothing on the irq interface a testbench could ever drive. axi_agent genuinely could run passive (a test that only wants to watch AXI traffic without generating it) since it has both branches built; irq_agent structurally can't, since the DUT is always and only the thing driving irq. uvm ch8's active/passive distinction was about a choice two different tests might make about the same interface — this is a case where the interface itself removes the choice.

regfile_env: two agents under one parent

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 bundling more than one agent is exactly uvm ch9's pattern (there it bundled one agent with a scoreboard); irq_watcher's uvm_analysis_imp #(irq_txn, irq_watcher) + write() is exactly the receiving-side machinery ch9's scoreboard used to actually receive what a monitor broadcasts, not just the sending side. Nothing here is new UVM surface — it's the same two mechanisms from ch9, applied to a second agent instead of a scoreboard.

Seeing it work

Reusing chapter 2's axi_txn/axi_driver/axi_monitor/axi_sequencer/axi_agent/axi_basic_seq unchanged, the test now builds regfile_env instead of a bare 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

And the top-level module adds irq_if, wires it to the DUT's irq port instead of a bare logic, and publishes a second virtual interface through config_db alongside the first — the same call, a different type parameter, no collision, since uvm_config_db#(virtual axi4lite_if.tb_mp) and uvm_config_db#(virtual irq_if.mon_mp) are two different parameterizations of the same class, each with its own lookup table:

`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

Running axi_basic_seq's scenario now prints two uvm_info lines from irq_watcher that never existed before this chapter — IRQ ASSERTED right after the fourth DATA write, IRQ DEASSERTED right after the CTRL.IRQ_CLR write — the same environment, now genuinely observing a second interface instead of only driving the first one.

Summary

  • irq_if is axi4lite_if's pattern stripped to the minimum a purely-observed signal needs: one clocking block, input-only, no output skew to configure.
  • irq_monitor reports edges, not levels — vif.cb.irq !== last turns a thousand identical cycles into exactly two meaningful events per assert/deassert cycle.
  • irq_agent has no active branch at all, unlike axi_agent — not an oversight, a direct consequence of irq being a signal nothing but the DUT ever drives.
  • regfile_env bundles two agents under one uvm_env, uvm ch9's pattern; irq_watcher's uvm_analysis_imp/write() is the same receiving-side machinery ch9's scoreboard used, reused for a second agent instead.
  • This is the direct payoff of uvm ch10's named gap: an environment that coordinates more than one agent, for the first time in this site's history.

Why does irq_agent have no active branch at all, when axi_agent's build_phase explicitly checks get_is_active()?

Why does irq_monitor check `vif.cb.irq !== last` instead of just broadcasting an irq_txn on every clocking event?

What UVM base class does regfile_env extend, the same one uvm ch9 used to bundle an agent with a scoreboard?