Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
    • What Is UVM?
    • Testbench Architecture and Environment Composition
      • The Four Layers
      • The Agent as the Reuse Unit
      • The Env Owns the Infrastructure
      • Coordinating Agents
      • Active-Passive Mixes and Composition Debug
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Testbench Architecture and Environment Composition
  1. Topics
  2. UVM Verification
  3. UVM Foundations
  4. Testbench Architecture and Environment Composition
  5. The Agent as the Reuse Unit

Part 1 · Foundations · Intermediate

The Agent as the Reuse Unit

Active and passive agents, the is_active setting read in uvm_agent::build_phase, the agent's public analysis port, and block-to-chip reuse.

On this page

    Your DMA block bench is finished, and the chip team wants it. In the chip, the DMA's APB port is driven by the real CPU subsystem and its input stream comes from an upstream block. If your agents can only drive, the chip team cannot use them: two things would be driving the same wires. If you have to fork the agent code to make a "watch-only" version, you now maintain two agents. How does one agent class serve both benches without a single edit?

    The idea: the monitor always, the driver on demand

    An agent is the reusable box for one interface: a sequencer that queues stimulus, a driver that turns it into pin activity, and a monitor that watches the pins and reports what happened. The trick that makes it reusable is that the monitor never depends on the driver. Think of a referee and a player: the player (driver) acts, the referee (monitor) only observes, and the referee can officiate any match, including ones where your team is not playing.

    So an agent has two modes. Active builds all three parts and drives the interface. Passive builds only the monitor and observes an interface that something else drives. The mode is a setting, is_active, that comes from outside the agent, so the same class serves a block bench (active, it generates traffic) and a chip bench (passive, it checks real traffic).

    The picture: one class, two builds

    Two instances of the same agent class: the active instance has a sequencer, driver and monitor on the input pins; the passive instance has only a monitor on the output pins; both monitors feed the scoreboard.
    Both agents are axis_agent. The active one drives and watches the input; the passive one only watches the output the DUT drives. Both publish on the agent's ap.

    Build it: the bench's stream agent

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // snippet: from the course DMA bench
    class axis_agent extends uvm_agent;
      `uvm_component_utils(axis_agent)
      axis_sequencer sqr;
      axis_driver    drv;
      axis_monitor   mon;
      uvm_analysis_port #(axis_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);
        mon = axis_monitor::type_id::create("mon", this);
        if (get_is_active() == UVM_ACTIVE) begin
          sqr = axis_sequencer::type_id::create("sqr", this);
          drv = axis_driver::type_id::create("drv", this);
        end
      endfunction
    
      function void connect_phase(uvm_phase phase);
        ap = mon.ap;
        if (get_is_active() == UVM_ACTIVE)
          drv.seq_item_port.connect(sqr.seq_item_export);
      endfunction
    endclass
    • `super.build_phase(phase)` first. uvm_agent::build_phase is where is_active is read from config_db. Skip the call and the agent stays at its default, UVM_ACTIVE, whatever the env asked for.

    • The monitor is built unconditionally, the driver and sequencer only when get_is_active() == UVM_ACTIVE.

    • `ap = mon.ap` in connect_phase gives the agent a public analysis port. The env connects to in_agt.ap and never reaches inside to in_agt.mon.ap, so the monitor can be renamed or replaced without touching the env.

    • The driver-sequencer connection is made only in active mode, because in passive mode neither exists.

    Who sets the mode, and when

    The env decides that the output side is observe-only, and says so in config_db before it creates the agent. uvm_agent has no setter method for the mode; config_db (or a config object the agent copies into is_active) is the way in.

    design.sv
    1
    2
    3
    4
    5
    // snippet: from dma_env::build_phase
    uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
    apb     = apb_agent::type_id::create("apb", this);
    in_agt  = axis_agent::type_id::create("in_agt", this);
    out_agt = axis_agent::type_id::create("out_agt", this);

    The setting must exist before the agent's build_phase runs, because that is the only moment the agent reads it. Setting it in the env's build_phase works whether it comes just before or just after the create, since the child's build runs after the parent's finishes. Setting it in connect_phase changes a stored value and builds nothing.

    What an agent config object adds

    The bench keeps its agents minimal: the mode comes from config_db and each driver and monitor fetches its own virtual interface. Production VIP usually bundles everything an agent needs into one agent config object: the virtual interface, the active or passive mode, and switches such as coverage on or off, protocol checks on or off, or the maximum number of idle cycles. The env builds one per agent instance and hands it down; the agent copies the mode into is_active in its build_phase and passes the interface to its children. One object per instance means one place to look when an agent behaves unexpectedly, and the env decides the policy without reaching into the agent.

    Watch it run: the topology shows the mode

    From the bench's topology print in the course's run, trimmed to the two stream agents:

    Diagram
    in_agt                 axis_agent              -     @264
      drv                  axis_driver             -     @636
      mon                  axis_monitor            -     @496
      sqr                  uvm_sequencer           -     @513
    out_agt                axis_agent              -     @272
      mon                  axis_monitor            -     @674

    Same type, axis_agent, two shapes. out_agt has only mon: there is no driver that could touch the DUT's output, and no sequencer for a test to start anything on. When you inherit a bench, this print is the fastest way to confirm what is really active.

    What breaks: the passive agent that drove anyway

    Suppose a team copies axis_agent to make a variant and, while restructuring, writes its build_phase without the call to super.build_phase(phase). Everything compiles. On the output side, where the env asked for passive, the agent now builds a driver, because is_active is never read and stays UVM_ACTIVE. The driver's run_phase immediately writes tvalid <= 0 on the output interface, a signal the DUT also drives. Depending on the simulator that shows up as an elaboration error about a variable with both continuous and procedural drivers, or as a TVALID that the DUT can never raise, so the output monitor sees nothing and the scoreboard reports every expected word as leftover.

    The symptom points at the DUT ("it never produces output"), which is what makes this costly. The fix is one line, and the habit that prevents it is to read the topology print whenever an agent changes: a driver under an agent that should be passive is visible at time 0, long before any traffic.

    Note

    A passive agent at chip level still has to check the protocol. Put protocol assertions in the interface (SVA on the signals), where they work whatever drives the pins, rather than in the driver, which does not exist in passive mode.

    Interview

    "What is the difference between an active and a passive agent, and why does it matter?"

    A weak answer: "Active has a driver, passive doesn't." Correct and shallow.

    A strong answer: "The monitor never depends on the driver, so the same agent class can watch traffic it did not create. Active builds sequencer, driver and monitor for block-level stimulus; passive builds only the monitor for chip level, where real RTL drives the pins. The mode comes from is_active, read in uvm_agent::build_phase from config_db, so it must be set before the agent builds and super.build_phase must be called. The agent exposes its monitor's port as its own ap, so the env never depends on its internals."

    Expect the follow-ups: Where do protocol checks live for a passive agent? (interface assertions, plus monitor-side checks) and What happens if a test starts a sequence on a passive agent? (its sqr handle is null, so the start fails with a null-object error).

    Check yourself An env sets is_active to UVM_PASSIVE for out_agt in its connect_phase. What does the topology show?

    A driver and sequencer under out_agt. The agent read is_active during its own build_phase, which ran before any connect_phase, so it saw the default UVM_ACTIVE and built everything. The late set changes the stored setting and nothing else.

    Check yourself Why does the bench's agent expose ap = mon.ap instead of letting the env connect to mon.ap directly?

    So the env depends only on the agent's public interface. If the monitor is renamed, replaced through the factory, or split into two, only the agent's connect_phase changes; every env that uses the agent, at block and chip level, keeps working.

    Check yourself At chip level the APB agent is passive, but the chip team still wants to catch a protocol violation, such as PENABLE rising without PSEL. Where does that check live?

    In assertions bound to or written inside apb_if, which check the signals whatever drives them, and optionally in the monitor, which can flag an illegal sequence as it reconstructs transfers. Not in the driver: in passive mode there is no driver, and a check that only runs when the bench drives would never see the CPU's traffic.

    Next: the env creates these agents and connects them to the scoreboard. The env owns the infrastructure shows why creation and connection happen in different phases.

    Related topics

    • The env owns the infrastructure

    • Agent architecture

    • Passive agents at SoC level

    Previous

    15 / 280

    Next: The Env Owns the Infrastructure

    Up next

    Up nextThe Env Owns the Infrastructure

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Coordinating Agents
    • Active-Passive Mixes and Composition Debug