This chapter has built reg_driver, reg_monitor, reg_sequencer, and reg_coverage as four separate, unconnected pieces. This article builds the component that owns all four, wires them together, and gives the whole group a single on/off switch for whether it drives the DUT at all.
10 min read | Intermediate Level
What You'll Learn
- What an agent is, and why it groups a sequencer, driver, and monitor together
- The difference between an active and a passive agent
- How to build reg_agent and connect every component this chapter has written so far
- How to configure an agent as active or passive from outside it
What Is an Agent?
An agent encapsulates a sequencer, driver, and monitor into a single entity, instantiating and connecting them together. Since UVM is built around configurability, an agent also typically exposes a couple of configuration knobs of its own - whether it's active or passive, being the main one.
Active vs. Passive Agents
| Active | Passive |
|---|---|
| Instantiates all three components: sequencer, driver, monitor. Drives data to the DUT. | Instantiates only the monitor. Used for checking and coverage only, with no data driven to the DUT. |
A passive agent shows up whenever a testbench needs to observe a protocol without driving it - watching an interface that another part of the testbench, or a separate instance of the same DUT, already drives. reg_monitor and reg_coverage are exactly the pieces a passive reg_agent would still build; reg_driver and reg_sequencer are the pieces it would skip.
Every agent derived from uvm_agent has a built-in method, get_is_active(), that reports which of the two it currently is:
// Inside reg_agent's build_phase
if (get_is_active()) begin
// Build driver and sequencer
end
// Build monitor regardless
Building reg_agent
1. Declare the Class
class reg_agent extends uvm_agent;
`uvm_component_utils(reg_agent)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// Code for rest of the steps come here
endclass
2. Declare Handles for Every Component
reg_driver m_driver;
reg_monitor m_monitor;
reg_sequencer m_sequencer;
reg_coverage m_coverage;
All four classes already exist, each built in its own article in this chapter. reg_agent's job from here is purely structural - instantiate each one, and connect the ones that need connecting.
3. Instantiate Components in build_phase
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (get_is_active()) begin
m_sequencer = reg_sequencer::type_id::create("m_sequencer", this);
m_driver = reg_driver::type_id::create("m_driver", this);
end
// A monitor and coverage are useful whether the agent is active or passive
m_monitor = reg_monitor::type_id::create("m_monitor", this);
m_coverage = reg_coverage::type_id::create("m_coverage", this);
endfunction
m_sequencer and m_driver only get built when reg_agent is active, since a passive agent has nothing to drive. m_monitor and m_coverage are built unconditionally - observing the bus and sampling coverage are useful whether or not this particular agent is the one driving it.
4. Connect Components in connect_phase
This is the step that closes every connection this chapter has been building toward, one at a time, across the articles for each component:
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active())
m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
m_monitor.mon_analysis_port.connect(m_coverage.analysis_export);
endfunction
Two separate connections happen here, and neither one is new - both were described from each side in the component articles, and this is simply where they're finally made real:
- m_driver's
seq_item_portconnects to m_sequencer'sseq_item_export, exactly as shown in UVM Sequencer - guarded by the same active check asbuild_phase, since there's no sequencer to connect to when the agent is passive. - m_monitor's mon_analysis_port connects to m_coverage's
analysis_export, exactly as shown in Subscriber - unconditionally, since both exist regardless of active/passive.
Quick Check
Q: Why does the m_monitor-to-m_coverage connection have no get_is_active() check around it, while the driver-to-sequencer connection does?
Show Answer
m_monitor and m_coverage are both built unconditionally in build_phase, regardless of whether reg_agent is active or passive - so the connection between them is always valid. m_sequencer only exists when the agent is active; connecting to a handle that was never built would fail, so that connection has to be guarded the same way its construction was.
Putting all four components together this way means reg_agent is the single point where every piece this chapter built - reg_sequencer, reg_driver, reg_monitor, and reg_coverage - finally exists as one connected unit, ready to be dropped into an environment.
Configuring Active or Passive From Outside
An agent derived from uvm_agent has a built-in field, is_active, of type uvm_active_passive_enum - defaulting to UVM_ACTIVE. A test or environment can override it using the same uvm_config_db mechanism used throughout this chapter, without reg_agent needing any code of its own to support it:
// Mark a specific agent instance as passive
uvm_config_db #(uvm_active_passive_enum)::set(this, "path_to_agent", "is_active", UVM_PASSIVE);
// Mark a specific agent instance as active (the default, shown here for symmetry)
uvm_config_db #(uvm_active_passive_enum)::set(this, "path_to_agent", "is_active", UVM_ACTIVE);
This works without reg_agent declaring an is_active field itself, or calling uvm_config_db::get() for it - both already exist inside uvm_agent, and the base class's own build_phase retrieves this setting automatically before reg_agent's own build_phase code runs. get_is_active(), used throughout this article, is simply how that already-retrieved value gets read back.
Why Not Just Give reg_driver and reg_monitor a Common Parent Directly?
Nothing stops a hierarchy from skipping the agent and connecting reg_driver and reg_sequencer directly inside an environment instead. The reason an agent is worth the extra layer is reuse: a reg_agent that bundles its own active/passive switch, its own driver-sequencer wiring, and its own coverage collector can be instantiated more than once - one active agent driving the DUT, one passive agent watching a second copy of the same interface elsewhere in a larger testbench - without duplicating any of that wiring logic each time.
Common Mistakes
Mistake #1: Building the sequencer and driver unconditionally
Wrong:
m_sequencer = reg_sequencer::type_id::create("m_sequencer", this);
m_driver = reg_driver::type_id::create("m_driver", this);
m_monitor = reg_monitor::type_id::create("m_monitor", this);
Right:
if (get_is_active()) begin
m_sequencer = reg_sequencer::type_id::create("m_sequencer", this);
m_driver = reg_driver::type_id::create("m_driver", this);
end
m_monitor = reg_monitor::type_id::create("m_monitor", this);
Why? Building a sequencer and driver unconditionally defeats the entire purpose of a passive agent - there'd be no way to instantiate reg_agent without it trying to drive the DUT, even in a context where nothing should be driving it.
Mistake #2: Connecting a handle that was never built
Forgetting the get_is_active() guard around the connect_phase wiring causes a null-handle error whenever reg_agent is configured as passive - m_sequencer and m_driver were never created, so there's nothing for m_driver's seq_item_port to connect to. The guard in connect_phase always has to match the one used in build_phase.
Practice Exercise
Challenge: Add an Agent-Level Coverage Enable
Add a bit enable_coverage property to reg_agent that, when 0, skips building m_coverage entirely - regardless of whether the agent is active or passive.
Hint
This needs a plain class property on reg_agent, plus an if around the existing line that creates m_coverage. Remember that connect_phase's wiring for m_coverage will also need the same guard, for the same reason the sequencer/driver connection does.
Solution
bit enable_coverage = 1;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (get_is_active()) begin
m_sequencer = reg_sequencer::type_id::create("m_sequencer", this);
m_driver = reg_driver::type_id::create("m_driver", this);
end
m_monitor = reg_monitor::type_id::create("m_monitor", this);
if (enable_coverage)
m_coverage = reg_coverage::type_id::create("m_coverage", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active())
m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
if (enable_coverage)
m_monitor.mon_analysis_port.connect(m_coverage.analysis_export);
endfunction
Both guards have to agree with each other for the same reason the active/passive guards do - connect_phase can only wire up what build_phase actually built.
Summary
In this article, you learned:
- An agent groups a sequencer, driver, and monitor into one reusable unit, and connects them together
- An active agent builds and connects all three; a passive agent only builds the monitor
get_is_active()reads a setting that's already configurable from outside the agent throughuvm_config_db, with no extra code required- Every
connect_phaseguard has to mirror the correspondingbuild_phaseguard, since you can't connect a handle that was never built
Going Deeper
- Setting is_active and other agent knobs: UVM Config Database
- How port-to-export connections resolve: UVM TLM Port to Export to Imp