env.in_agt and env.out_agt in the DMA bench are the same class, axis_agent. One drives TVALID, TDATA and TLAST into the DMA. The other sits on the DMA's output, where those same signals are driven by the DUT. If the second one built a driver too, the bench would put a second driver on every output signal, and the scoreboard would be comparing X.
Nothing in the class says which copy is which. So how does one agent class decide, at build time, whether it drives?
A restaurant uses one recipe for a full plate and a half portion. The recipe does not change; a note from the front of house says which size to make. Mode-gated build is the same: the agent's build_phase always creates the monitor, and creates the sequencer and driver only if its mode is UVM_ACTIVE. The mode is a note from the parent, read at build time.
The mode is the field is_active, inherited from uvm_agent, of type uvm_active_passive_enum and defaulting to UVM_ACTIVE. Two pieces of library code matter. uvm_agent::build_phase looks up an is_active entry in config_db for this agent and copies it into the field; that is why an agent must call super.build_phase(phase). And the virtual function get_is_active() returns the field; a VIP whose mode lives in a config object overrides it. Code that asks the mode should call get_is_active(), never read the field directly.
Two setters in the bench decide modes. dma_env makes its output agent passive, and dma_subsys_env makes dma1.in_agt passive when its config says dma1's input is fed by dma0:
// snippet: from dma_env::build_phase and dma_subsys_env::build_phase
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
if (cfg.chain_internal)
uvm_config_db#(uvm_active_passive_enum)::set(this, "dma1.in_agt", "is_active", UVM_PASSIVE);Use the enum type. The reference library's uvm_agent also accepts an int entry for is_active, but the enum documents itself and matches what the agent stores. The set may even come after the create call in the same build_phase: create only constructs the child, and the child's build_phase runs after the parent's returns. A set made in the parent's connect_phase, on the other hand, is too late: the agent has already been built.
// snippet: from the course DMA bench
class axis_agent extends uvm_agent;
`uvm_component_utils(axis_agent)
axis_sequencer sqr;
axis_driver drv;
axis_monitor mon;
uvm_analysis_port #(axis_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = axis_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = axis_sequencer::type_id::create("sqr", this);
drv = axis_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
ap = mon.ap;
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclassThree lines carry the pattern. super.build_phase(phase) comes first, so the mode is known before anything is created. mon is created outside the if. sqr and drv are created inside it. Nothing else in the agent depends on the mode except the matching guard in connect_phase.
connect_phase asks get_is_active() again before connecting the driver to the sequencer. In passive mode drv and sqr are null, and an unguarded drv.seq_item_port.connect(...) is a null-object access at time 0. Asking through the same function in both phases means build and connect can never disagree.
If the agent has a config object (see Agent configuration), let the object be the single source of truth by overriding the virtual function:
// snippet: in an agent that holds apb_agent_cfg cfg, fetched in build_phase
virtual function uvm_active_passive_enum get_is_active();
return (cfg != null) ? cfg.is_active : is_active;
endfunctionNow build_phase, connect_phase, a virtual sequence and the env's debug print all get the same answer. The cfg != null fallback keeps the function safe if something asks before the object has been fetched.
A passive agent has no sequencer, so anything that starts stimulus must check. The bench's dma_vseq refuses to start unless its sequencer handles are set: its body begins with if (apb_sqr == null || axis_sqr == null) and a uvm_fatal that says which handles to set. (It checks the sequencer handles rather than the virtual interface handle because comparing a virtual interface with null miscompiles in Verilator 5.048; the comparison is legal SystemVerilog.)
Derived agents are common: an agent with an extra coverage collector, or the bench's own axis_sink_agent, which adds a slave-side sequencer and driver that answer the DUT with TREADY. The rule is the same in every case. The derived build_phase calls super.build_phase(phase) first, which builds the parent's monitor (and sequencer and driver if active), and then creates only what it adds. Its connect_phase calls super.connect_phase(phase) and then wires the additions. The reactive slave and why it builds its slave side even when passive are covered in the agent patterns topic.
In dma_smoke_test the topology printed at end of elaboration shows uvm_test_top.env.out_agt with a single child, mon, while env.apb and env.in_agt each list sqr, drv and mon. The scoreboard line proves the passive monitor still did its work:
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0Read the topology on every new bench, and on every bench after a refactor. It is the cheapest test of mode-gated build there is: one glance tells you which agents can drive.
An engineer needed AXI-Stream coverage on the output port and wrote axis_cov_agent extends axis_agent, overriding build_phase to create a coverage subscriber. The override created the subscriber and returned. It did not call super.build_phase(phase). The test died at time 0 with a null-object access in the env's connect_phase.
Trace it. Without the super call, no monitor was created and is_active was never read. The inherited connect_phase then ran ap = mon.ap with mon null, which is itself an error, and the env's out_agt.ap.connect(...) failed on the null port. If the derived class had created its own monitor but still skipped the super call, the failure would have been quieter and worse: the passive setting would never be read, the agent would default to active, and on a DUT-driven port the new driver would fight the DUT. The super.build_phase lesson tells that version of the story.
The fix is one line, super.build_phase(phase);, at the top of the override, followed by additions only. Code review should flag any agent build_phase without it.
is_active for out_agt in its connect_phase instead of build_phase. What happens?The setting is ignored for build purposes. The agent's build_phase ran during the build phase, read no is_active entry, and kept the default, UVM_ACTIVE. It built a driver for a DUT-driven port. The set must happen before the child's build_phase, which in practice means in the parent's build_phase.
if (get_is_active() == UVM_ACTIVE) block instead of in an else?Because the active agent needs it too. An active agent's monitor watches what its own driver produced on the pins, which is the only trustworthy view for checking and coverage. The monitor is unconditional; only stimulus is optional.
get_is_active() to return its config object's field, but its build_phase still tests is_active == UVM_ACTIVE directly. The env sets the config object to passive and does not set the is_active knob. What gets built?A driver and sequencer. The build_phase reads the inherited field, which still holds its default, UVM_ACTIVE, because nobody set the knob. Meanwhile get_is_active() reports passive, so the connect_phase skips the connection: the driver exists, is unconnected, and may still drive the pins from its run_phase. Always ask through get_is_active().
Next: in active mode the agent built a driver. In The driver you will follow one APB transfer edge by edge, and see the write that vanished when a slave got faster.