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

ActivePassive
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_port connects to m_sequencer's seq_item_export, exactly as shown in UVM Sequencer - guarded by the same active check as build_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 through uvm_config_db, with no extra code required
  • Every connect_phase guard has to mirror the corresponding build_phase guard, since you can't connect a handle that was never built

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5