UVM Agent
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:
- A driver -- to drive signal activity on the interface
- A monitor -- to observe signal activity on the interface
- 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_activebefore 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.
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.
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.
A common mistake is to skip the
is_activecheck in connect_phase. When the agent is in passive mode,drvandseqrarenull, 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.
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.
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.
The
uvm_agentbase class automatically retrieves theis_activevalue from the configuration database during itsbuild_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
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.
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.seqrisnullbecause 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.
Try this code in EDA PlaygroundIn our code,
is_activeis set from the top module rather than the environment. When usingnullas 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 itsbuild_phaseas in our example.
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:
- In the testbench hierarchy, we see
agentunderenv, and inside itdrv,mon, andseqr. - The hierarchy is cleaner --
envnow has only the agent and the sequence objects. - When the agent is configured as
UVM_ACTIVE, all three child components are created and the driver-sequencer connection is established. - If the agent is switched to
UVM_PASSIVE, only the monitor appears in the hierarchy -- the driver and sequencer are not created. - The sequences start on
agent.seqrand 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.