A VIP team ships a new release of their APB agent. The only change in the notes: the monitor was split into a bus monitor and a protocol checker, and renamed. The next morning forty block environments fail to compile. Every one of them had wired its scoreboard straight to env.apb.mon.ap, a handle that no longer exists. The VIP did nothing wrong by its own rules. The environments had reached through the box instead of using its sockets.
So what is the box, and which parts of it is the rest of the testbench allowed to touch?
An agent is a uvm_agent that packages everything one interface needs: a sequencer that decides which transaction goes next, a driver that turns transactions into pin activity, and a monitor that turns pin activity back into transactions. Think of it as a kitchen appliance. You plug things into its sockets and turn its dials; you never open the case and wire into the heating element, because the manufacturer may move it in the next model.
The agent has exactly three sockets. Stimulus goes in through the sequencer: a test or virtual sequence calls seq.start(env.apb.sqr). Observations come out of one analysis port on the agent, ap, which any number of subscribers can connect to. Configuration arrives before the agent is built, through uvm_config_db: whether it is active or passive, which virtual interface to use, and any policy knobs. Everything else (the driver, the monitor, their names and their internal threads) is private.
Follow the two loops. The stimulus loop runs left to right: sequence, sequencer, driver, pins, with item_done closing the handshake (the read data travels back inside the same request object). The observation path runs from the pins through the monitor to the analysis port. The two never touch inside the agent: the monitor does not ask the driver what it meant to send. That separation is what lets the same agent go passive at chip level and keep checking.
Here is the whole agent from the DMA bench. It is short because the work lives in its children; the agent's job is to build them, wire them and expose the sockets.
// snippet: from the course DMA bench
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_sequencer sqr;
apb_driver drv;
apb_monitor mon;
uvm_analysis_port #(apb_item) ap; // the agent's public output
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase); // reads is_active from config_db
mon = apb_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = apb_sequencer::type_id::create("sqr", this);
drv = apb_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
endclasssuper.build_phase(phase) is not ceremony: uvm_agent's build_phase is where the is_active setting is read from config_db. The monitor is created unconditionally, because every use of the agent needs to observe. The sequencer and driver are created only when get_is_active() returns UVM_ACTIVE. The full story of that decision, and how to extend it, is in Mode-gated build.
The agent's public port is not a new port at all. ap = mon.ap copies the monitor's port handle into the agent's member, so env.apb.ap and env.apb.mon.ap are the same object. The env connects to apb.ap and never names the monitor. If a later release replaces the monitor, only this one line inside the agent changes.
This works because of phase order. connect_phase runs bottom-up: the agent's connect_phase finishes before the env's starts, so by the time dma_env calls apb.ap.connect(sb.apb_export), the handle is set. The alternative style creates a real port on the agent in build_phase (ap = new("ap", this);) and connects mon.ap.connect(ap) in connect_phase: a child-port-to-parent-port connection. Both are correct. The port-to-port version shows up as its own port in the topology and gives the agent a port that exists from build onward; the handle copy is shorter and adds no extra hop. Pick one per project.
The env side is the consumer of the three sockets, and it uses only those:
// snippet: from the course DMA bench
class dma_env extends uvm_env;
`uvm_component_utils(dma_env)
apb_agent apb;
axis_agent in_agt, out_agt;
dma_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// The output side is observed only: the DUT drives it.
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);
sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
apb.ap.connect(sb.apb_export);
in_agt.ap.connect(sb.in_export);
out_agt.ap.connect(sb.out_export);
endfunction
endclassNotice what the env does not do. It never touches apb.mon, apb.drv or any interface. It sets one knob (is_active for out_agt), creates three agents and a scoreboard, and connects three agent ports. The same axis_agent class appears twice, once active and once passive, decided entirely by that knob.
Part | Owns | Must never |
|---|---|---|
Sequencer | Arbitration between sequences, handing items to the driver, routing responses back | Touch pins or know protocol timing |
Driver | Pin timing for stimulus: when each signal changes, waiting for handshakes | Check data, publish to the scoreboard, decide what to send next |
Monitor | Recognising completed transfers on the pins and publishing them | Drive any signal, raise objections, change an item after publishing it |
Agent (wrapper) | Building and wiring the three, exposing | Contain test-specific logic or checking |
Run dma_smoke_test. The base test prints the topology at end of elaboration, and you can read the agent shapes straight from it: env.apb and env.in_agt each hold sqr, drv and mon; env.out_agt holds only mon. The proof that every analysis port reached its subscriber is the scoreboard's closing line:
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 08 words matched needs all three agent ports: KEY from apb.ap, input words from in_agt.ap, output words from out_agt.ap. Break any one connection and the count drops or check_phase complains.
The output agent observed 8 words without a driver. A passive agent is a full participant in checking; it simply has nothing to drive.
Back to the forty broken environments. The symptom was a compile error, which is the lucky case. The unlucky case is the same reach-in done one level softer: an env that connects in build_phase, right after creating the agent, to apb.ap. It compiles. At time 0 the simulator stops with a null-object access, because apb.ap is assigned in the agent's connect_phase, which has not run yet. The root cause in both cases is the same: the env depended on the agent's insides (a child's name, or the moment a handle is set) instead of its contract.
The fix is the discipline in this lesson. Connect only to agent.ap, and only in the env's connect_phase. Start stimulus only on agent.sqr. Configure only through config_db before the agent is built. An agent that follows the rule can be refactored freely; an env that follows it never notices.
ap = mon.ap; from the agent's connect_phase into its build_phase, right after mon is created. Does it still work?Yes, as long as it comes after the monitor is created: the monitor creates its port in its constructor, so mon.ap exists as soon as mon does. It is also earlier than any consumer needs it. What would break is putting it before mon = apb_monitor::type_id::create(...), which copies a null handle that never gets updated.
out_agt.ap to both the scoreboard and a coverage collector. Which subscriber's write() runs first, and should either care?The standard does not specify the order; the reference library happens to call subscribers in alphabetical order of their full names. Neither subscriber may depend on it. Each must treat its write() as independent and must not modify the item it receives, because the other subscriber holds the same object.
Because the driver knows only what it meant to drive. If the DUT ignored a transfer, or another master drove the bus, or the agent is passive at chip level with no driver at all, the driver's view is wrong or absent. The monitor reports what really happened on the pins, which is the only thing a scoreboard should trust.
Next: the agent got its mode and its virtual interface from config_db. In Agent configuration you will see who puts them there, when a config object beats loose knobs, and the shared handle that turned two agents passive at once.