UVM Agent

27 Jun 2026
10 mins

In the last article, we learnt about UVM Monitor which observes DUT interface signals and broadcasts them via analysis ports. We now have a driver, a monitor, and a sequencer -- three separate components that need to be instantiated, configured, and connected in the testbench. As the number of interfaces in a design grows, managing these components individually becomes tedious and error-prone.

UVM provides UVM Agent (uvm_agent) which encapsulates the driver, monitor, and sequencer into a single reusable component. Let's dive into UVM Agent in this article.

What is UVM Agent

UVM Agent is a component that group together all the verification components needed to communicate with a specific DUT interface protocol. A typical agent contains:

  1. A driver -- to drive signal activity on the interface
  2. A monitor -- to observe signal activity on the interface
  3. A sequencer -- to manage transaction flow to the driver

The agent creates, configures, and connects these components internally. A higher-level testbench component like the environment simply instantiates the agent and does not worry about its internal wiring.

The agent is the primary unit of reuse in UVM. A well-designed agent for a protocol like AXI, APB, or I2C can be dropped into any testbench that uses the same interface, saving significant development and debug time.

Active vs Passive Mode

Not every verification scenario needs to drive signals. Consider a testbench that only monitors the DUT outputs without driving any input -- for instance, a protocol checker that verifies the DUT behavior is correct. In such cases, the agent should not instantiate the driver or sequencer.

UVM agents support two modes:

  • UVM_ACTIVE (default): The agent creates the driver, monitor, and sequencer. It can both drive and observe signals.
  • UVM_PASSIVE: The agent creates only the monitor. It only observes signals and does not drive anything.

The mode is controlled by the is_active member variable inherited from uvm_agent. Its type is uvm_active_passive_enum which has two values: UVM_ACTIVE and UVM_PASSIVE.

When building a reusable agent, always check is_active before creating the driver and sequencer. This ensures the agent can be used in both active and passive configurations without any code changes.

Defining a UVM Agent

Let's see how to define a custom agent for our ALU testbench.

1. Declaring the class

We define an agent by extending uvm_agent. The uvm_agent base class itself extends uvm_component.

class alu_agent extends uvm_agent;
    `uvm_component_utils(alu_agent)
 
    driver    drv;
    monitor   mon;
    sequencer seqr;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

2. Building components in build_phase

Inside build_phase, we conditionally create the driver and sequencer based on is_active. The monitor is always created since it is needed in both modes.

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
 
        `uvm_info(get_name(), $sformatf("Agent is %s",
            is_active == UVM_ACTIVE ? "ACTIVE" : "PASSIVE"), UVM_LOW)
 
        mon = monitor::type_id::create("mon", this);
 
        if (is_active == UVM_ACTIVE) begin
            drv = driver::type_id::create("drv", this);
            seqr = sequencer::type_id::create("seqr", this);
        end
    endfunction

3. Connecting components in connect_phase

Inside connect_phase, we connect the driver's seq_item_port to the sequencer's seq_item_export. This connection is only relevant in active mode.

    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
 
        if (is_active == UVM_ACTIVE) begin
            drv.seq_item_port.connect(seqr.seq_item_export);
        end
    endfunction

A common mistake is to skip the is_active check in connect_phase. When the agent is in passive mode, drv and seqr are null, and accessing their ports results in a null-pointer access and simulation crash.

4. Configuring child components

Beyond just creating and connecting components, the agent can also configure its children. For instance, the sequencer's arbitration method can be set from within the agent.

    function void start_of_simulation_phase(uvm_phase phase);
        super.start_of_simulation_phase(phase);
        if (is_active == UVM_ACTIVE) begin
            seqr.set_arbitration(UVM_SEQ_ARB_USER);
        end
    endfunction

By placing this configuration inside the agent, the environment stays clean and the agent remains self-contained.

Updated Testbench Hierarchy

With the agent in place, our environment no longer needs to instantiate the driver, monitor, and sequencer individually. Instead, it instantiates a single agent.

uvm_test_top (env)
  |
  +-- agent (alu_agent)
        +-- drv (driver)
        +-- mon (monitor)
        +-- seqr (sequencer)

In the new topology driver, monitor and sequencer are child of agent. As usual sequences will not be seen in uvm_top.print_topology() as they are objects, not components.

Configuring Agent Mode

The agent's is_active mode is set from a higher level component using uvm_config_db. The set must happen before the agent is created so that the value is available during its build_phase.

// In environment's build_phase
uvm_config_db#(uvm_active_passive_enum)::set(this, "agent", "is_active", UVM_PASSIVE);
agent = alu_agent::type_id::create("agent", this);

The uvm_agent base class automatically retrieves the is_active value from the configuration database during its build_phase. Setting it through config_db before the agent is created ensures the value is picked up correctly. Setting it after creation has no effect.

Example

Let's implement the complete agent and update our environment.

Agent Class

import uvm_pkg::*;
 
class alu_agent extends uvm_agent;
    `uvm_component_utils(alu_agent)
 
    driver    drv;
    monitor   mon;
    sequencer seqr;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
 
        `uvm_info(get_name(), $sformatf("Agent is %s", is_active == UVM_ACTIVE ? "ACTIVE" : "PASSIVE"), UVM_LOW)
 
        mon = monitor::type_id::create("mon", this);
 
        if (is_active == UVM_ACTIVE) begin
            drv = driver::type_id::create("drv", this);
            seqr = sequencer::type_id::create("seqr", this);
        end
    endfunction
 
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
 
        if (is_active == UVM_ACTIVE) begin
            drv.seq_item_port.connect(seqr.seq_item_export);
        end
    endfunction
 
    function void start_of_simulation_phase(uvm_phase phase);
        super.start_of_simulation_phase(phase);
        if (is_active == UVM_ACTIVE) begin
            seqr.set_arbitration(UVM_SEQ_ARB_USER);
        end
    endfunction
endclass

Updated Environment

Our environment now creates the agent instead of creating each component separately. The diff below shows what changed from the last article's environment.

class env extends uvm_component;
    `uvm_component_utils(env)
 
-    sequencer custom_seqr;
-    uvm_sequencer#(transaction) base_seqr;
     random_seq rnd_seq1, rnd_seq2, rnd_seq3;
-    int use_arbitratoin_method;
-    driver drv;
-    monitor mon;
+    alu_agent agent;
    ...
 
    // function: build
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
-        custom_seqr = sequencer::type_id::create("custom_seqr", this);
-        base_seqr = uvm_sequencer#(transaction)::type_id::create("base_seqr", this);
-        drv = driver::type_id::create("drv", this);
-        mon = monitor::type_id::create("mon", this);
+        agent = alu_agent::type_id::create("agent", this);
         rnd_seq1 = random_seq::type_id::create("rnd_seq1");
         rnd_seq2 = random_seq::type_id::create("rnd_seq2");
         rnd_seq3 = random_seq::type_id::create("rnd_seq3");
     endfunction: build_phase
 
-    // function: connect_phase
-    function void connect_phase(uvm_phase phase);
-        super.connect_phase(phase);
-        drv.seq_item_port.connect(custom_seqr.seq_item_export);
-    endfunction: connect_phase
 
    ...
     // task: run_phase
     task run_phase(uvm_phase phase);
         super.run_phase(phase);
         phase.raise_objection(this);
         fork
-            rnd_seq1.start(custom_seqr, null, 20);
-            rnd_seq2.start(custom_seqr, null, 10);
-            rnd_seq3.start(custom_seqr, null, 20);
+            rnd_seq1.start(agent.seqr, null, 20);
+            rnd_seq2.start(agent.seqr, null, 10);
+            rnd_seq3.start(agent.seqr, null, 20);
         join
         phase.drop_objection(this);
     endtask: run_phase
 
-    // function: start_of_simulation_phase
-    function void start_of_simulation_phase(uvm_phase phase);
-        super.start_of_simulation_phase(phase);
-
-        // set the below variable (1-6) to try different arbitration methods
-        ...
-    endfunction: start_of_simulation_phase
 endclass: env

The driver-sequencer connection and arbitration logic are no longer in the environment. They are handled internally by the agent, keeping the environment clean and modular.

When the agent is in passive mode, agent.seqr is null because the sequencer is never created. Starting sequences on a null handle causes a simulation crash. In a real testbench, the environment should check the agent's mode or the test should be written to handle this gracefully.

TB Top

The testbench top now configures the agent's mode along with the virtual interface. The use_passive_agent flag controls whether the agent operates in active or passive mode.

module temp_top();
+    // USE PASSIVE AGENT - set below variable to instantiate passive agent
+    bit use_passive_agent = 1;
 
...
    initial begin
        uvm_config_db#(virtual alu_if)::set(null, "*", "vif", alu_if_inst);
+        if (use_passive_agent) begin
+            uvm_config_db#(uvm_active_passive_enum)::set(null, "*", "is_active", UVM_PASSIVE);
+        end
    end
endmodule

In our code, is_active is set from the top module rather than the environment. When using null as the context, the "*" wildcard matches any agent in the hierarchy. Also, the set must happen before the agent is created so that the value is available during its build_phase as in our example.

Try this code in EDA Playground
Output
# KERNEL: UVM_INFO @ 0: reporter [RNTST] Running test env...  
# KERNEL: UVM_INFO /home/runner/agent.sv(18) @ 0: uvm_test_top.agent [agent] Agent is ACTIVE  
# KERNEL: UVM_INFO /home/runner/sequencer.sv(14) @ 0: uvm_test_top.agent.seqr [seqr] building custom sequencer...  
# KERNEL: UVM_INFO ./uvm-1.2/src/base/uvm_root.svh(583) @ 0: reporter [UVMTOP] UVM testbench topology:  
# KERNEL: --------------------------------------------------------------  
# KERNEL: Name Type Size Value  
# KERNEL: --------------------------------------------------------------  
# KERNEL: uvm_test_top env - @336  
# KERNEL: agent alu_agent - @353  
# KERNEL: drv driver - @398  
# KERNEL: rsp_port uvm_analysis_port - @417  
# KERNEL: seq_item_port uvm_seq_item_pull_port - @407  
# KERNEL: mon monitor - @379  
# KERNEL: item_collected_port uvm_analysis_port - @388  
# KERNEL: seqr sequencer - @427  
# KERNEL: rsp_export uvm_analysis_export - @436  
# KERNEL: seq_item_export uvm_seq_item_pull_imp - @554  
# KERNEL: arbitration_queue array 0 -  
# KERNEL: lock_queue array 0 -  
# KERNEL: num_last_reqs integral 32 'd1  
# KERNEL: num_last_rsps integral 32 'd1  
# KERNEL: --------------------------------------------------------------  
# KERNEL:

Let's decode the output:

  1. In the testbench hierarchy, we see agent under env, and inside it drv, mon, and seqr.
  2. The hierarchy is cleaner -- env now has only the agent and the sequence objects.
  3. When the agent is configured as UVM_ACTIVE, all three child components are created and the driver-sequencer connection is established.
  4. If the agent is switched to UVM_PASSIVE, only the monitor appears in the hierarchy -- the driver and sequencer are not created.
  5. The sequences start on agent.seqr and interact with the driver through the same handshake mechanism we saw in earlier articles.

Conclusion

In this article we went through how to declare UVM Agent using the uvm_agent base class. We saw how an agent encapsulates the driver, monitor, and sequencer into a reusable component, and how the is_active flag controls whether the agent operates in active or passive mode. By grouping related verification components together, the agent makes the testbench modular and reusable across different projects. In the next article we will see how to build the environment that orchestrates multiple agents, scoreboards, and coverage collectors.