Your DMA block bench is finished, and the chip team wants it. In the chip, the DMA's APB port is driven by the real CPU subsystem and its input stream comes from an upstream block. If your agents can only drive, the chip team cannot use them: two things would be driving the same wires. If you have to fork the agent code to make a "watch-only" version, you now maintain two agents. How does one agent class serve both benches without a single edit?
An agent is the reusable box for one interface: a sequencer that queues stimulus, a driver that turns it into pin activity, and a monitor that watches the pins and reports what happened. The trick that makes it reusable is that the monitor never depends on the driver. Think of a referee and a player: the player (driver) acts, the referee (monitor) only observes, and the referee can officiate any match, including ones where your team is not playing.
So an agent has two modes. Active builds all three parts and drives the interface. Passive builds only the monitor and observes an interface that something else drives. The mode is a setting, is_active, that comes from outside the agent, so the same class serves a block bench (active, it generates traffic) and a chip bench (passive, it checks real traffic).
// 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
endclass`super.build_phase(phase)` first. uvm_agent::build_phase is where is_active is read from config_db. Skip the call and the agent stays at its default, UVM_ACTIVE, whatever the env asked for.
The monitor is built unconditionally, the driver and sequencer only when get_is_active() == UVM_ACTIVE.
`ap = mon.ap` in connect_phase gives the agent a public analysis port. The env connects to in_agt.ap and never reaches inside to in_agt.mon.ap, so the monitor can be renamed or replaced without touching the env.
The driver-sequencer connection is made only in active mode, because in passive mode neither exists.
The env decides that the output side is observe-only, and says so in config_db before it creates the agent. uvm_agent has no setter method for the mode; config_db (or a config object the agent copies into is_active) is the way in.
// snippet: from dma_env::build_phase
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
apb = apb_agent::type_id::create("apb", this);
in_agt = axis_agent::type_id::create("in_agt", this);
out_agt = axis_agent::type_id::create("out_agt", this);The setting must exist before the agent's build_phase runs, because that is the only moment the agent reads it. Setting it in the env's build_phase works whether it comes just before or just after the create, since the child's build runs after the parent's finishes. Setting it in connect_phase changes a stored value and builds nothing.
The bench keeps its agents minimal: the mode comes from config_db and each driver and monitor fetches its own virtual interface. Production VIP usually bundles everything an agent needs into one agent config object: the virtual interface, the active or passive mode, and switches such as coverage on or off, protocol checks on or off, or the maximum number of idle cycles. The env builds one per agent instance and hands it down; the agent copies the mode into is_active in its build_phase and passes the interface to its children. One object per instance means one place to look when an agent behaves unexpectedly, and the env decides the policy without reaching into the agent.
From the bench's topology print in the course's run, trimmed to the two stream agents:
in_agt axis_agent - @264
drv axis_driver - @636
mon axis_monitor - @496
sqr uvm_sequencer - @513
out_agt axis_agent - @272
mon axis_monitor - @674Same type, axis_agent, two shapes. out_agt has only mon: there is no driver that could touch the DUT's output, and no sequencer for a test to start anything on. When you inherit a bench, this print is the fastest way to confirm what is really active.
Suppose a team copies axis_agent to make a variant and, while restructuring, writes its build_phase without the call to super.build_phase(phase). Everything compiles. On the output side, where the env asked for passive, the agent now builds a driver, because is_active is never read and stays UVM_ACTIVE. The driver's run_phase immediately writes tvalid <= 0 on the output interface, a signal the DUT also drives. Depending on the simulator that shows up as an elaboration error about a variable with both continuous and procedural drivers, or as a TVALID that the DUT can never raise, so the output monitor sees nothing and the scoreboard reports every expected word as leftover.
The symptom points at the DUT ("it never produces output"), which is what makes this costly. The fix is one line, and the habit that prevents it is to read the topology print whenever an agent changes: a driver under an agent that should be passive is visible at time 0, long before any traffic.
is_active to UVM_PASSIVE for out_agt in its connect_phase. What does the topology show?A driver and sequencer under out_agt. The agent read is_active during its own build_phase, which ran before any connect_phase, so it saw the default UVM_ACTIVE and built everything. The late set changes the stored setting and nothing else.
ap = mon.ap instead of letting the env connect to mon.ap directly?So the env depends only on the agent's public interface. If the monitor is renamed, replaced through the factory, or split into two, only the agent's connect_phase changes; every env that uses the agent, at block and chip level, keeps working.
In assertions bound to or written inside apb_if, which check the signals whatever drives them, and optionally in the monitor, which can flag an illegal sequence as it reconstructs transfers. Not in the driver: in passive mode there is no driver, and a check that only runs when the bench drives would never see the CPU's traffic.
Next: the env creates these agents and connects them to the scoreboard. The env owns the infrastructure shows why creation and connection happen in different phases.