The Agent & Environment
Every component built so far in this section — driver, monitor, sequencer, scoreboard — exists as a standalone class, with nothing yet actually assembling them into a tree. That assembly happens in two layers: the agent, which bundles everything specific to one interface, and the environment, which bundles one or more agents together with whatever checks their combined traffic.
The agent: one interface's worth of components
class fifo_agent extends uvm_agent;
`uvm_component_utils(fifo_agent)
fifo_driver driver;
fifo_monitor monitor;
uvm_sequencer #(fifo_txn) sequencer;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
monitor = fifo_monitor::type_id::create("monitor", this);
if (get_is_active() == UVM_ACTIVE) begin
driver = fifo_driver::type_id::create("driver", this);
sequencer = uvm_sequencer#(fifo_txn)::type_id::create("sequencer", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active() == UVM_ACTIVE)
driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction
endclass
The get_is_active() guard is the detail worth dwelling on. uvm_agent — the base class — already carries a built-in is_active property, configurable from outside via uvm_config_db (covered in depth on the Resource DB & Config DB page) without this class needing to declare anything for it itself:
UVM_ACTIVE(the default): builds driver and sequencer as well as the monitor — this agent can both generate stimulus and observe it.UVM_PASSIVE: builds only the monitor. No driver, no sequencer.
The monitor is created unconditionally — observation is always useful, whether or not this particular agent is the one driving stimulus in a given test. A passive agent shows up in practice when a testbench needs to watch a bus it doesn't own in a particular scenario (a secondary interface driven by something other than this testbench, for instance) — same monitor, same scoreboard wiring, just no driver/sequencer built at all for that configuration.
build_phase's guard and connect_phase's guard both check get_is_active() independently — and they have to agree. If build_phase skips creating a driver (passive) but connect_phase unconditionally tries to .connect() it anyway, that's a null-handle error the moment the environment tries to elaborate. This is exactly the kind of mismatch that's easy to introduce by editing one phase method without noticing the other one needs the same edit.
The environment: agents plus whatever checks them
class fifo_env extends uvm_env;
`uvm_component_utils(fifo_env)
fifo_agent agent;
fifo_scoreboard scoreboard;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = fifo_agent::type_id::create("agent", this);
scoreboard = fifo_scoreboard::type_id::create("scoreboard", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
agent.monitor.ap.connect(scoreboard.ap_imp);
endfunction
endclass
This one connect_phase line — agent.monitor.ap.connect(scoreboard.ap_imp) — is where the monitor's broadcast and the scoreboard's write() method actually meet. Everything upstream of this line (the monitor reconstructing transactions, the scoreboard knowing how to check them) was written with zero awareness of the other; this is the only place in the whole testbench that has to know both exist.
A fifo_coverage subscriber, if one were added, would connect here too — agent.monitor.ap.connect(coverage.analysis_export) — one more line, with neither the monitor nor the scoreboard needing to change at all. That's the concrete payoff of the TLM decoupling introduced back on the monitor page: the environment is the only place that has to grow as more consumers of the monitor's output get added.
With only one agent, this environment might look like unnecessary ceremony — the payoff shows up at scale. A block with two interfaces reuses the same fifo_agent class twice inside a larger environment; an SoC-level environment might compose several block-level environments as children of its own. None of that requires touching fifo_agent, fifo_driver, or fifo_monitor — only the environment doing the composing changes.
What's next
The environment is a complete, reusable unit — but nothing yet tells it when to run, or with which stimulus. That's the test's job, together with the plain-Verilog module that finally connects all of this to the DUT — covered next.