Build an active or passive agent

Package an agent so a test can use it without knowing its insides, build its driver and sequencer only when it is active, and catch an agent that silently built the wrong way.

Lesson
U11.1
Time
25 min
Runs on
Verilator with UVM
Check
6 questions

The incrementer agent moves into the chip bench, where the real upstream block drives x on port B, so the test sets that agent to passive. The scoreboard still misses transfers, and a topology print shows port_b.drv and port_b.sqr. The agent's build_phase never called super.build_phase, so it never read the setting. It built the chip bench's idle-driving driver from U4.3, which writes valid = 0 on every idle cycle, on the pins the upstream block drives.

Component under port_bThe test asked forThe agent built
Monitor monYesYes
Sequencer sqrNoYes
Driver drvNoYes, and this driver writes valid on every idle cycle

A passive request that the agent never read. Its driver competes with the upstream block for the same signals.

One agent, two jobs

An agent bundles everything a bench needs for one interface: a sequencer, a driver and a monitor. At block level the agent is active. It creates stimulus, its driver puts that stimulus on the pins, and its monitor reports what happened.

In a bigger bench, another design block often drives the same interface. The bench must then stay off those pins, but it still wants to see the traffic. A passive agent does that: it builds only the monitor. The scoreboard and coverage connected to the monitor keep working unchanged.

This is why a monitor never asks the driver what it sent. It reads the pins, so it sees the same transfers whoever drives them. A monitor that copied items from its own driver would go blind the moment the agent became passive.

What a reusable agent ships

What the incrementer agent shipsEverything inside incr_pkg except the interface. Arrows are calls or builds; the monitor's arrow is an analysis write.
What the incrementer agent shipsEverything inside `incr_pkg` except the interface. Arrows are calls or builds; the monitor's arrow is an analysis write. sequence API sequences send(x, y, sqr): onecall per transfer object incr_agent_cfg vif, is_active, knobs(U11.2) agent incr_agent Builds the monitor;sqr and drv only whenactive driver incr_driver Items to pins; copies yback into the item monitor incr_monitor Pins to items; oneanalysis port subscriber incr_coverage Subscriber: valuesseen (U15.1) object incr_item x sent, y returned interface incr_if Interface: outsidethe package starts on agent.sqrread in buildbuilds if activebuilds alwayswrite(t)drivessampled
  1. What a test touches

    A test fills the config object and calls API sequences. It never names the driver or the monitor.

    More context

    The agent reads its config object in build_phase, before it decides which children to build.

    Follow the calls, values and waits
    1. data cfg: UVM_ACTIVE, vif: incr_agent_cfg → incr_agent
  2. Active build

    With UVM_ACTIVE the agent builds a sequencer, a driver and the monitor. The driver is the only class that writes the pins.

    More context

    The example prints port_a UVM_ACTIVE children: mon sqr drv.

    Follow the calls, values and waits
    1. call create mon: incr_agent → incr_monitor
    2. call create sqr, drv: incr_agent → incr_driver
  3. One API call, one transfer

    The test calls send(10, y, sqr). The sequence hands one item to the driver, which drives x=10 and waits for the answer.

    More context

    The test sees a task call. The item, the grant and item_done stay inside the agent.

    Follow the calls, values and waits
    1. call send(10, y, sqr): API sequences → incr_agent
    2. data x=10: incr_driver → incr_if
    3. Waiting: API sequences — in send(10) until 20 ns
  4. The monitor sees it15 ns (illustrative)

    The monitor samples x=10 and y=11 from the pins and turns them back into an item.

    More context

    Log line 15 ns port_a saw x=10 y=11. The monitor works the same whether or not a driver is present.

    Follow the calls, values and waits
    1. data x=10, y=11: incr_if → incr_monitor
  5. send returns the answer20 ns (illustrative)

    The driver copies y into the item before item_done, so send returns 11 to the test.

    More context

    Log line 20 ns test send(10) returned 11. The second call, send(11), returns 12 at 40 ns.

    Follow the calls, values and waits
    1. return returns y=11: incr_agent → API sequences
    2. Released: API sequences
  6. Passive build

    With UVM_PASSIVE only the monitor is built. It still samples the pins and feeds coverage and any checker connected to its port.

    More context

    The example prints port_b UVM_PASSIVE children: mon. No sequencer and no driver exist, so nothing in the bench can write port B.

    Follow the calls, values and waits
    1. call create mon: incr_agent → incr_monitor
  7. Passive traffic35 ns (illustrative)

    Another block drives port B. The passive monitor samples x=200 and y=201 and publishes the item.

    More context

    Log line 35 ns port_b saw x=200 y=201. The example has no coverage class; in the full package the subscriber receives this same write.

    Follow the calls, values and waits
    1. data x=200, y=201: incr_if → incr_monitor
    2. data write(t): x=200: incr_monitor → incr_coverage
Read the diagram as text
  • API sequences: send(x, y, sqr): one call per transfer
  • incr_agent_cfg: vif, is_active, knobs (U11.2)
  • incr_agent: Builds the monitor; sqr and drv only when active
  • incr_driver: Items to pins; copies y back into the item
  • incr_monitor: Pins to items; one analysis port
  • incr_coverage: Subscriber: values seen (U15.1)
  • incr_item: x sent, y returned
  • incr_if: Interface: outside the package
  • API sequences → incr_agent: starts on agent.sqr
  • incr_agent_cfg → incr_agent: read in build
  • incr_agent → incr_driver: builds if active
  • incr_agent → incr_monitor: builds always
  • incr_monitor → incr_coverage: write(t)
  • incr_driver → incr_if: drives
  • incr_if → incr_monitor: sampled

Seven classes and one interface. The item is shared by the sequences, the driver and the monitor.

An agent is reused as a package: one import incr_pkg::*; gives a test every class it needs. The interface is the one piece outside, because SystemVerilog does not allow an interface inside a package. The top module instantiates it and hands it over as a virtual interface.

One file per class, included in dependency order. The example keeps the same package in one file.
incr_if.sv        interface incr_if            outside the package
incr_pkg.sv       package incr_pkg;
                    import uvm_pkg::*;
                    `include "incr_item.svh"
                    `include "incr_agent_cfg.svh"
                    `include "incr_driver.svh"
                    `include "incr_monitor.svh"
                    `include "incr_coverage.svh"
                    `include "incr_agent.svh"
                    `include "incr_seq_lib.svh"   API sequences
                  endpackage

Each piece has one job. The item says what one transfer carries. The config object holds what the agent may be told (U11.2). The driver turns items into pin activity and the monitor does the reverse. A coverage subscriber records which values were seen (U15.1).

API sequences: the agent's task interface

A test should not need to know how an item is filled. An API sequence wraps one transfer behind a task. The example's send(x, y, sqr) starts the sequence on the sequencer and returns the design's answer. The driver copies that answer into the item before item_done.

In incr_send_seq. The test calls api.send(10, y, port_a.sqr).
task send(bit [7:0] x, output bit [7:0] y,
          input uvm_sequencer_base sqr, input uvm_sequence_base parent = null);
  value = x;
  start(sqr, parent);       // body() sends one item with req.x = value
  y = result;               // body() copied req.y after finish_item
endtask

A bus agent ships the same shape as read(addr, data, sqr) and write(addr, data, sqr). Worker and virtual sequences call these tasks instead of building items, the hierarchy of U5.4. The register adapter of U13.2 turns register operations into the same items.

The is_active setting

uvm_agent has a variable is_active, of the enum type uvm_active_passive_enum. Its two values are UVM_ACTIVE (1), the default, and UVM_PASSIVE (0). The method get_is_active() returns it.

You do not set the variable directly. The test or environment stores a setting named "is_active" in the configuration database for the agent's path. uvm_agent::build_phase looks that setting up and copies it into the variable.

Ask for a passive agent at one path.
// in the test's build_phase, before the agent is built
uvm_config_db #(uvm_active_passive_enum)::set(this, "env.port_b", "is_active", UVM_PASSIVE);

The lookup accepts more than the enum. In uvm-core 2020.3.1 it also reads an int setting (0 or 1) and a string setting such as "UVM_PASSIVE". Older benches often store an int, so both forms keep working. The lookup happens only inside uvm_agent::build_phase, which is why an agent that skips super.build_phase stays active whatever the test asked for.

Build only what the mode needs

The agent's own build_phase calls super.build_phase first, so is_active holds the requested mode. It then always creates the monitor, and creates the sequencer and driver only when get_is_active() returns UVM_ACTIVE.

Reads the mode too late

function void build_phase(uvm_phase phase);
  mon = incr_monitor::type_id::create("mon", this);
  if (get_is_active() == UVM_ACTIVE) begin
    sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
    drv = incr_driver::type_id::create("drv", this);
  end
endfunction

No super.build_phase, so is_active keeps its default, UVM_ACTIVE. A passive request still builds a driver.

Reads the mode first

function void build_phase(uvm_phase phase);
  super.build_phase(phase);   // reads is_active
  mon = incr_monitor::type_id::create("mon", this);
  if (get_is_active() == UVM_ACTIVE) begin
    sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
    drv = incr_driver::type_id::create("drv", this);
  end
endfunction

The requested mode is known before any child is built, so a passive agent gets only its monitor.

connect_phase uses the same test. A passive agent has no driver, so drv.seq_item_port.connect(...) would use a null handle and stop the run. Guard the connection with get_is_active() as well, or with a check that the driver exists.

Anything outside the agent that reaches into it must respect the mode too. A test that starts a sequence on port_b.sqr finds a null sequencer when the agent is passive. Start sequences only on active agents, and let the environment decide which those are.

When the mode must be known

The mode decides which children exist, so it must be set before the agent's build_phase runs. Build runs top-down: the test's build_phase finishes before the environment's, and the environment's before the agent's. So a setting made anywhere in the test or environment build_phase arrives in time.

Changing the mode later does nothing useful. After build, the driver either exists or it does not, and its run_phase will start. A uvm_config_db setting made in connect_phase is never read, because uvm_agent looks it up once, in build_phase. Only a direct assignment to is_active changes what get_is_active() reports, and then it reports passive while the driver keeps running. Stopping all sequences is not the same as passive either: the driver still exists, and many drivers write idle values on every cycle.

Where passive agents appear

Passive agents are common once a bench grows past one block. Three situations account for most of them.

First, internal interfaces. At chip level, the incrementer's input comes from another block, not from the bench. The block-level agent is reused there in passive mode, so its monitor, scoreboard and coverage keep checking that interface inside the chip.

Second, outputs. Many designs produce traffic on a second interface, such as a result bus. The bench has nothing to drive there, only something to check, so the agent for that interface is passive from the start.

Third, a bench that only listens. A bench may watch a real system, an emulator or a recorded run, with nothing to drive at all. Every agent is passive, and only the checks remain.

In all three cases the agent's code is the same as at block level. Only one setting changes, which is the reason to write agents this way: the checking you build once keeps paying off at every level of integration.

Keep in mind what a passive agent cannot do. It cannot make traffic happen, so its coverage only fills when the real source exercises the interface. Suppose the chip-level tests never send a value above 100 into port B. The passive agent's coverage shows that gap, and the fix belongs in the tests that drive the chip.

Agent monitor and bus monitor

The monitor inside an agent is an agent monitor. It watches one agent's interface and reports the transfers that interface carries, in both modes. A shared bus with several masters and slaves often also gets a bus monitor: one component in the environment that watches the whole bus.

Agent monitorBus monitor
WatchesOne agent's interfaceThe whole shared bus
Lives inThe agent, active or passiveThe environment, often behind an on/off knob
ReportsThat interface's transfersEvery transfer, with which master and slave took part, and bus state
FeedsThat agent's coverage and checks, and the predictorSystem checks, such as address decoding and arbitration

Both read pins and never drive. U7.1 builds the monitor itself.

The incrementer has one point-to-point interface per port, so its agent monitor is enough. A bus monitor earns its place when a check needs the view of every master at once.

Watching a port someone else drives

The example has two copies of the incrementer. The bench drives port A through an active agent. A process in the top module stands in for the upstream block on port B, and a passive agent watches it.

Both ports at each rising clock edge (10 ns clock)
012345time (ns)51525354555port A validport A x01011port B validport B x0200201port_a.mon: x=10both monitors report

Port A is driven by port_a.drv; port B by the stand-in block. The same monitor class reports both.

Each monitor prints one line per edge with valid high. Port A shows the two items its sequence sent, x = 10 and 11. Port B shows x = 200 and 201, which no UVM driver sent at all. The passive agent saw them because its monitor only reads the pins.

Try it

The first two lines come from end_of_elaboration_phase, after build: port A has all three children, port B only its monitor. The monitor lines match the trace above, and each send returns at the falling edge that ends its transfer, with the answer the driver read back.

Delete super.build_phase(phase); from incr_agent and run again. Port B now reports UVM_ACTIVE and builds a driver, although the test asked for passive. Then store the setting as uvm_config_db #(int)::set(this, "port_b", "is_active", 0); and check that the agent still becomes passive.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: port_a UVM_ACTIVE children: mon sqr drv port_b UVM_PASSIVE children: mon 15 ns port_a saw x=10 y=11 20 ns test send(10) returned 11 35 ns port_a saw x=11 y=12 35 ns port_b saw x=200 y=201 40 ns test send(11) returned 12 45 ns port_b saw x=201 y=202 UVM_ERROR : 0

Check yourself

Predict the outputThree agents get a passive request in three ways. What line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class plain_agent extends uvm_agent;
  `uvm_component_utils(plain_agent)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
  endfunction
endclass
class hasty_agent extends uvm_agent;
  `uvm_component_utils(hasty_agent)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
  endfunction
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  plain_agent a, b;
  hasty_agent c;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    uvm_config_db #(string)::set(this, "a", "is_active", "UVM_PASSIVE");
    uvm_config_db #(int)::set(this, "b", "is_active", 0);
    uvm_config_db #(uvm_active_passive_enum)::set(this, "c", "is_active", UVM_PASSIVE);
    a = plain_agent::type_id::create("a", this);
    b = plain_agent::type_id::create("b", this);
    c = hasty_agent::type_id::create("c", this);
  endfunction
  function void end_of_elaboration_phase(uvm_phase phase);
    $display("%s %s %s", a.get_is_active().name(), b.get_is_active().name(), c.get_is_active().name());
  endfunction
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugThe test asks for port A active and port B passive. Each option is the body of the agent's build_phase. Which one leaves a driver in the passive agent?
ConceptAn agent becomes passive. What must change in its monitor?
ConceptA test wants port B passive. Where can it make the is_active setting so that it takes effect?
Read the waveformThis is port B in the chip bench, where the upstream block drives and the agent is passive. At which edges does port_b.mon publish an item?
Port B at each rising edge
012345time (ns)152535455565validx0404142

The monitor publishes one item at every rising edge where valid is 1.

Predict the outputThis agent falls back to a default configuration object when get finds none, with only a warning. The test asks for a passive port B by storing its object for "env.port_b", a line copied from a bench that had an environment, but here the agents sit directly under the test. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  function new(string name = "incr_item"); super.new(name); endfunction
endclass
class incr_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(incr_driver)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclass
class incr_monitor extends uvm_monitor;
  `uvm_component_utils(incr_monitor)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclass
class incr_agent_cfg extends uvm_object;
  `uvm_object_utils(incr_agent_cfg)
  uvm_active_passive_enum is_active = UVM_ACTIVE;
  function new(string name = "incr_agent_cfg"); super.new(name); endfunction
endclass
// Carries on with defaults when the object is missing.
class lenient_agent extends uvm_agent;
  `uvm_component_utils(lenient_agent)
  incr_agent_cfg cfg;
  incr_driver drv;
  uvm_sequencer #(incr_item) sqr;
  incr_monitor mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(incr_agent_cfg)::get(this, "", "cfg", cfg)) begin
      `uvm_warning("NOCFG", "no incr_agent_cfg, using defaults")
      cfg = incr_agent_cfg::type_id::create("cfg");
    end
    is_active = cfg.is_active;
    mon = incr_monitor::type_id::create("mon", this);
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
      drv = incr_driver::type_id::create("drv", this);
    end
  endfunction
endclass
class chip_test extends uvm_test;
  `uvm_component_utils(chip_test)
  lenient_agent port_a, port_b;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    incr_agent_cfg cfg_b = incr_agent_cfg::type_id::create("cfg_b");
    cfg_b.is_active = UVM_PASSIVE;
    uvm_config_db #(incr_agent_cfg)::set(this, "env.port_b", "cfg", cfg_b);
    port_a = lenient_agent::type_id::create("port_a", this);
    port_b = lenient_agent::type_id::create("port_b", this);
  endfunction
  function void end_of_elaboration_phase(uvm_phase phase);
    $display("port_b is %s with %0d children", port_b.get_is_active().name(), port_b.get_num_children());
  endfunction
endclass
module tb;
  initial run_test("chip_test");
endmodule

Remember

  • An active agent builds a sequencer, driver and monitor; a passive agent builds only the monitor.
  • Set "is_active" in the configuration database before the agent's build_phase; uvm_agent::build_phase reads it as an enum, an int or a string.
  • Call super.build_phase first, then guard the sequencer and driver, and their connection, with get_is_active().
  • A monitor reads only the pins, so the same monitor works whoever drives them.
  • A reusable agent ships as a package with its item, config object, driver, monitor, coverage and API sequences; only the interface sits outside, and a missing config object should stop the run, not fall back to defaults.