UVM-05: The UVM Agent — VLSI Trainers
VLSI Trainers UVM Series · UVM-05
UVM Series · UVM-05

The UVM Agent

The agent as a reusable verification kit for one protocol interface — active vs passive modes, the is_active flag, the agent configuration object pattern, how sequencer, driver, and monitor are built and connected inside the agent, and the analysis port.

📋 What an Agent Is

A UVM agent is a self-contained verification component that handles all testbench interactions with a single protocol interface. It bundles together the sequencer, driver, and monitor for that interface into one reusable package — a Verification IP (VIP) unit.

The agent is the lowest-level structural block in a UVM testbench. Once an agent is written for a given protocol (APB, AXI, SPI, I2C, USB), it can be dropped into any testbench that uses that protocol — at block level, at integration level, or at chip level — without modification. The environment does not need to know how the agent was implemented internally; it just instantiates it, configures it, and connects its analysis port.

UVM Agent — Position in the Testbench Hierarchy my_test (uvm_test) my_env (uvm_env) my_agent (uvm_agent) ← focus of this post Sequencer active only Driver active only Monitor always Scoreboard receives from analysis port analysis_port ↑↓ virtual interface to DUT vlsitrainers.com
Figure 1 — Agent position in the testbench hierarchy. The agent (green) sits below the env and above the individual driver/monitor/sequencer components. It exposes an analysis port outward to the scoreboard and coverage collector. The driver and sequencer exist only in active mode.

📋 Active vs Passive Mode

Every UVM agent inherits an is_active field from uvm_agent of type uvm_active_passive_enum. This single flag controls whether the agent drives stimulus or only observes.

UVM_ACTIVE (default)

  • Sequencer, driver, and monitor all instantiated
  • Agent drives the DUT via virtual interface
  • Sequences run on the sequencer
  • Monitor still observes and publishes transactions
  • Used when this testbench controls this interface

UVM_PASSIVE

  • Only the monitor is instantiated
  • No sequencer, no driver — agent drives nothing
  • Monitor still observes and publishes transactions
  • Used when observing an interface driven by someone else
  • Used at integration level to observe sub-system ports
The monitor is always instantiated — in both active and passive modes. This is a deliberate design rule. An environment that switches an agent from active to passive (e.g. for chip-level reuse) still needs observation. Only the sequencer and driver are conditional on is_active. Never put the monitor inside the active-only block.

The is_active field is set through the agent’s configuration object — not directly on the agent. The test creates the config object, sets is_active to UVM_ACTIVE or UVM_PASSIVE, and places it in config_db. The agent retrieves this config in its build_phase and uses it to decide what to instantiate.

📋 Agent Internals

Agent Internal Structure — Active vs Passive Active Agent (UVM_ACTIVE) Sequencer arbitrates seqs holds item queue seq_item_export Driver get_next_item() drives DUT signals seq_item_port Monitor samples signals builds transactions analysis_port item analysis_port Virtual Interface (DUT signals) Seqr + Drv only in UVM_ACTIVE. Monitor always present. Passive Agent (UVM_PASSIVE) Sequencer not built Driver not built Monitor samples signals builds transactions analysis_port analysis_port Virtual Interface (observed only) Only Monitor instantiated. Agent observes — does not drive. vlsitrainers.com
Figure 2 — Active agent (left) vs passive agent (right). In active mode all three sub-components are built and the sequencer-driver TLM connection is wired in connect_phase. In passive mode only the monitor is built. The analysis port is present and connected in both modes — observation is always available.

📋 The Agent Config Object

Every well-designed agent has a configuration object — a uvm_object that holds all the settings the agent needs to be built and operated. The config object decouples the agent’s construction from the test: the test creates and populates the config object, passes it to the agent through config_db, and the agent uses it in build_phase without needing to know who configured it.

The three mandatory members of any agent config object are:

// ── Agent configuration object ───────────────────────────
class apb_agent_config extends uvm_object;
  `uvm_object_utils(apb_agent_config)

  // ① Virtual interface — connects agent to DUT signals
  virtual apb_if  vif;

  // ② Active/passive mode
  uvm_active_passive_enum is_active = UVM_ACTIVE;   // default: active

  // ③ Optional sub-component enables
  bit has_functional_coverage = 0;
  bit has_scoreboard          = 0;

  // Protocol-specific settings
  int num_wait_states  = 0;
  bit error_injection  = 0;

  function new(string name = "apb_agent_config");
    super.new(name);
  endfunction
endclass
Set all config fields to safe defaults. The config object should work correctly with defaults even if no test customises it. Use UVM_ACTIVE as the default for is_active (most agents are active by default), and 0 for optional sub-component flags. Tests that need different behaviour override only the fields they care about.

📋 Build Phase — Conditional Construction

The agent’s build_phase has three responsibilities: retrieve the config object from config_db, always build the monitor, and conditionally build the sequencer and driver based on is_active.

class apb_agent extends uvm_agent;
  `uvm_component_utils(apb_agent)

  apb_driver    m_drv;
  apb_sequencer m_seqr;
  apb_monitor   m_mon;
  apb_agent_config m_cfg;

  uvm_analysis_port #(apb_seq_item) ap;  // forwarded from monitor

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    // ① Retrieve config — fatal if not found
    if (!uvm_config_db #(apb_agent_config)::get(
          this, "", "cfg", m_cfg))
      `uvm_fatal("CFG", "No agent config found!")

    // ② Create analysis port (always)
    ap = new("ap", this);

    // ③ Create monitor (always — active AND passive)
    m_mon = apb_monitor::type_id::create("m_mon", this);

    // ④ Create sequencer + driver only in active mode
    if (m_cfg.is_active == UVM_ACTIVE) begin
      m_seqr = apb_sequencer::type_id::create("m_seqr", this);
      m_drv  = apb_driver::type_id::create("m_drv", this);
    end

    // ⑤ Pass virtual interface to monitor (and driver if active)
    uvm_config_db #(virtual apb_if)::set(
      this, "m_mon", "vif", m_cfg.vif);
    if (m_cfg.is_active == UVM_ACTIVE)
      uvm_config_db #(virtual apb_if)::set(
        this, "m_drv", "vif", m_cfg.vif);
  endfunction
endclass

📋 Connect Phase — TLM Wiring

The connect_phase has two jobs: wire the driver’s seq_item_port to the sequencer’s seq_item_export (the TLM handshake path for sequence items), and forward the monitor’s analysis port to the agent’s own analysis port so external components can connect to the agent directly without knowing the monitor’s path.

  function void connect_phase(uvm_phase phase);
    // ① Connect driver to sequencer (active only)
    if (m_cfg.is_active == UVM_ACTIVE)
      m_drv.seq_item_port.connect(m_seqr.seq_item_export);

    // ② Forward monitor's analysis port to agent's analysis port
    // External consumers connect to agent.ap — not agent.m_mon.ap
    // This hides the internal structure from the environment
    m_mon.ap.connect(this.ap);
  endfunction
Why forward the analysis port? If the env connects directly to agent.m_mon.ap, it couples itself to the agent’s internal structure — it must know that there is a monitor with a member called ap. If the agent is refactored (e.g. the monitor is renamed), the env breaks. Forwarding through agent.ap gives the env a stable public API. The env connects to agent.ap and the agent’s internal wiring is an implementation detail.

📋 The Agent Analysis Port

The agent exposes a single uvm_analysis_port as its public output interface. This port broadcasts every transaction the monitor captures to all connected subscribers — scoreboard, coverage collector, or any other analysis component in the environment.

Analysis Port — Broadcast to Multiple Subscribers Monitor ap.write(txn) Agent boundary agent.ap Scoreboard.analysis_export Coverage.analysis_export Other subscriber… Connected in env’s connect_phase: agent.ap.connect(sb.analysis_export) agent.ap.connect(cov.analysis_export) vlsitrainers.com
Figure 3 — Agent analysis port fan-out. The monitor calls ap.write(txn) once. The UVM analysis port infrastructure automatically delivers the transaction to every connected subscriber. The connection from agent.ap to each subscriber is made in the env’s connect_phase — not inside the agent.

The environment wires subscribers to the agent’s analysis port in its own connect_phase:

// In my_env's connect_phase:
function void connect_phase(uvm_phase phase);
  // Connect agent's analysis port to scoreboard and coverage
  m_agent.ap.connect(m_sb.analysis_export);
  m_agent.ap.connect(m_cov.analysis_export);
endfunction

📋 Complete Agent Implementation

The complete APB agent bringing all sections together:

typedef uvm_sequencer #(apb_seq_item) apb_sequencer;  // simple typedef

class apb_agent extends uvm_agent;
  `uvm_component_utils(apb_agent)

  // Sub-component handles
  apb_driver        m_drv;
  apb_sequencer     m_seqr;
  apb_monitor       m_mon;
  apb_agent_config  m_cfg;

  // Public analysis port — env connects subscribers here
  uvm_analysis_port #(apb_seq_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);
    if (!uvm_config_db #(apb_agent_config)::get(
          this, "", "cfg", m_cfg))
      `uvm_fatal("CFG", "apb_agent: no config found")

    ap    = new("ap", this);
    m_mon = apb_monitor::type_id::create("m_mon", this);

    if (m_cfg.is_active == UVM_ACTIVE) begin
      m_seqr = apb_sequencer::type_id::create("m_seqr", this);
      m_drv  = apb_driver::type_id::create("m_drv", this);
    end

    uvm_config_db #(virtual apb_if)::set(
      this, "m_mon", "vif", m_cfg.vif);
    if (m_cfg.is_active == UVM_ACTIVE)
      uvm_config_db #(virtual apb_if)::set(
        this, "m_drv", "vif", m_cfg.vif);
  endfunction

  function void connect_phase(uvm_phase phase);
    if (m_cfg.is_active == UVM_ACTIVE)
      m_drv.seq_item_port.connect(m_seqr.seq_item_export);
    m_mon.ap.connect(this.ap);   // forward monitor port to agent port
  endfunction

endclass

📋 Agent Reuse Across Levels

A correctly designed agent can be plugged into any environment at any level of the integration hierarchy without modification. The config object controls everything; the agent adapts to whatever context it finds itself in.

Levelis_activeWhat changesWhat stays the same
Block-level (driving interface)UVM_ACTIVENothing — default behaviourAgent code unchanged
Integration-level (observing only)UVM_PASSIVEOnly m_cfg.is_activeAgent code unchanged; monitor still active
Chip-level (multiple instances)MixedEach agent’s config objectAgent class is identical for all instances
Error injection testUVM_ACTIVEFactory override: driver → error_driverAgent build/connect code unchanged
Coverage-only runUVM_PASSIVEis_active + coverage enable flagAgent code unchanged
The agent is your primary unit of reuse. A well-tested APB agent can be shared as a verification IP package across teams and projects. Any environment that imports the package can instantiate the agent, configure it via the config object, and immediately have full APB drive and observation capability. This is the core payoff of the UVM agent pattern.

📋 Quick Reference

ItemKey fact
Agent base classuvm_agent — extends uvm_component
Agent purposeSelf-contained VIP for one protocol interface — bundles seqr + drv + mon
is_active typeuvm_active_passive_enum — values UVM_ACTIVE or UVM_PASSIVE
UVM_ACTIVE buildsSequencer + Driver + Monitor
UVM_PASSIVE buildsMonitor only — no sequencer, no driver
Monitor built whenAlways — both active and passive modes. Never put monitor inside the active block.
Config object baseuvm_object (not component — no phases, no hierarchy)
Config mandatory fieldsVirtual interface handle, is_active flag
Config passed viauvm_config_db::set() in test build_phase, ::get() in agent build_phase
Sequencer typedeftypedef uvm_sequencer #(my_seq_item) my_sequencer; — often a simple typedef
Seq-driver TLM connectionm_drv.seq_item_port.connect(m_seqr.seq_item_export) in connect_phase
Analysis port forwardingm_mon.ap.connect(this.ap) — hides internal monitor path from env
Env connects toagent.ap — not agent.m_mon.ap. Never reach inside an agent.
Fan-outOne analysis port → multiple subscribers (scoreboard, coverage, any analysis component)
Virtual interface distributionAgent’s build_phase calls config_db::set() to push vif to m_drv and m_mon
Scroll to Top