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.
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.
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.
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.
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
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.
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
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
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 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.
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
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
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.
| Level | is_active | What changes | What stays the same |
|---|---|---|---|
| Block-level (driving interface) | UVM_ACTIVE | Nothing — default behaviour | Agent code unchanged |
| Integration-level (observing only) | UVM_PASSIVE | Only m_cfg.is_active | Agent code unchanged; monitor still active |
| Chip-level (multiple instances) | Mixed | Each agent’s config object | Agent class is identical for all instances |
| Error injection test | UVM_ACTIVE | Factory override: driver → error_driver | Agent build/connect code unchanged |
| Coverage-only run | UVM_PASSIVE | is_active + coverage enable flag | Agent code unchanged |
| Item | Key fact |
|---|---|
| Agent base class | uvm_agent — extends uvm_component |
| Agent purpose | Self-contained VIP for one protocol interface — bundles seqr + drv + mon |
| is_active type | uvm_active_passive_enum — values UVM_ACTIVE or UVM_PASSIVE |
| UVM_ACTIVE builds | Sequencer + Driver + Monitor |
| UVM_PASSIVE builds | Monitor only — no sequencer, no driver |
| Monitor built when | Always — both active and passive modes. Never put monitor inside the active block. |
| Config object base | uvm_object (not component — no phases, no hierarchy) |
| Config mandatory fields | Virtual interface handle, is_active flag |
| Config passed via | uvm_config_db::set() in test build_phase, ::get() in agent build_phase |
| Sequencer typedef | typedef uvm_sequencer #(my_seq_item) my_sequencer; — often a simple typedef |
| Seq-driver TLM connection | m_drv.seq_item_port.connect(m_seqr.seq_item_export) in connect_phase |
| Analysis port forwarding | m_mon.ap.connect(this.ap) — hides internal monitor path from env |
| Env connects to | agent.ap — not agent.m_mon.ap. Never reach inside an agent. |
| Fan-out | One analysis port → multiple subscribers (scoreboard, coverage, any analysis component) |
| Virtual interface distribution | Agent’s build_phase calls config_db::set() to push vif to m_drv and m_mon |