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);
endinterfaceSame 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
endclassirq_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_object → uvm_transaction → uvm_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
endclassThere'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
endclassuvm_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
endclassAnd 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
endmoduleRunning 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_ifisaxi4lite_if's pattern stripped to the minimum a purely-observed signal needs: one clocking block, input-only, no output skew to configure.irq_monitorreports edges, not levels —vif.cb.irq !== lastturns a thousand identical cycles into exactly two meaningful events per assert/deassert cycle.irq_agenthas no active branch at all, unlikeaxi_agent— not an oversight, a direct consequence ofirqbeing a signal nothing but the DUT ever drives.regfile_envbundles two agents under oneuvm_env,uvmch9's pattern;irq_watcher'suvm_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
uvmch10'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?