Intermediate UVM
Agents, drivers and monitors, TLM ports, scoreboards, and functional coverage.
Driver + monitor + sequencer, bundled
An agent is a UVM component that wraps everything needed to interact with one interface of the DUT: a driver, a monitor, and (if active) a sequencer.
class axi_agent extends uvm_agent; `uvm_component_utils(axi_agent) axi_driver drv; axi_monitor mon; axi_sequencer seqr; 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 drv = axi_driver::type_id::create("drv", this); seqr = axi_sequencer::type_id::create("seqr", this); end endfunction function void connect_phase(uvm_phase phase); if (get_is_active() == UVM_ACTIVE) drv.seq_item_port.connect(seqr.seq_item_export); endfunction endclass
Active vs passive
An active agent drives the interface. A passive agent only monitors. When you instantiate the same DUT's interface in two places (e.g., master and slave), one agent is active, the other passive — but both contribute to coverage.
Where pins meet transactions
The driver consumes sequence items from the sequencer and drives the interface. The monitor samples the interface and publishes observed transactions via an analysis port.
class axi_driver extends uvm_driver#(axi_txn); `uvm_component_utils(axi_driver) virtual axi_lite_if vif; task run_phase(uvm_phase phase); forever begin seq_item_port.get_next_item(req); drive_txn(req); seq_item_port.item_done(); end endtask task drive_txn(axi_txn t); @(vif.cb_m); vif.cb_m.awaddr <= t.addr; vif.cb_m.awvalid <= 1; wait (vif.cb_m.awready); @(vif.cb_m); vif.cb_m.awvalid <= 0; // … W and B channels endtask endclass
Analysis ports and the broadcast pattern
The monitor talks to scoreboards, coverage collectors, and predictors through an analysis port. An analysis port supports many subscribers — just call write() once and every connected subscriber gets a copy.
class axi_monitor extends uvm_monitor; `uvm_component_utils(axi_monitor) uvm_analysis_port#(axi_txn) ap; virtual axi_lite_if vif; function void build_phase(uvm_phase phase); ap = new("ap", this); endfunction task run_phase(uvm_phase phase); forever begin axi_txn t; collect_txn(t); ap.write(t); // fan out to every subscriber end endtask endclass
Receiving analysis writes
Subscribers receive via uvm_analysis_imp. When a component needs to receive from multiple analysis ports, use `uvm_analysis_imp_decl(_suffix) to create distinct write_suffix methods.
Self-checking at the transaction level
A scoreboard is the component that decides if the DUT is correct. It receives transactions from the monitor, runs them through a reference model, and compares against DUT responses.
`uvm_analysis_imp_decl(_input) `uvm_analysis_imp_decl(_output) class axi_scoreboard extends uvm_scoreboard; `uvm_component_utils(axi_scoreboard) uvm_analysis_imp_input#(axi_txn, axi_scoreboard) in_imp; uvm_analysis_imp_output#(axi_txn, axi_scoreboard) out_imp; axi_txn expected[$]; function void write_input(axi_txn t); expected.push_back(predict(t)); endfunction function void write_output(axi_txn t); axi_txn exp = expected.pop_front(); if (!exp.compare(t)) `uvm_error("SB", $sformatf("mismatch exp=%s got=%s", exp.sprint(), t.sprint())) endfunction endclass
Subscriber pattern for coverage
A coverage collector is a uvm_subscriber that listens to an analysis port and samples covergroups. Keep it separate from the scoreboard — one is about what passed, the other is about what you exercised.
class axi_coverage extends uvm_subscriber#(axi_txn); `uvm_component_utils(axi_coverage) axi_txn t; covergroup cg; option.per_instance = 1; addr_cp : coverpoint t.addr { bins low = {[0:'hFFF]}; bins high = {['h1000:$]}; } resp_cp : coverpoint t.resp; cross_ar : cross addr_cp, resp_cp; endgroup function void write(axi_txn tr); t = tr; cg.sample(); endfunction endclass