TO Be coming

Nov 6, 2026

UVM Course :Day 6: Agents and Environment Layering — Reusable UVM Testbench Building Blocks

Day 6: Agents and Environment Layering — Reusable UVM Testbench Building Blocks
Advertisement
Top banner — drop your 728×90 or responsive ad unit here.

Day 6: Agents and Environment Layering — Reusable UVM Testbench Building Blocks

A 30-day SystemVerilog/UVM verification series. Days 1–3 built the skeleton. Day 4 wired the plumbing. Day 5 installed the knobs. Today we make the testbench modular, so a protocol block you write once can be dropped into any environment.

By Day 5 your FIFO env worked, but it was flat: driver, monitor, scoreboard, and sequencer were all direct children of the environment. That is fine for one protocol and one project. It stops being fine the moment you add a second bus, reuse the FIFO checker in another testbench, or want to run the same agent in passive mode on the DUT's response side.

Today's mechanism is the agent — the encapsulation boundary for one protocol interface. Everything an agent owns is specific to that interface: driver, sequencer, monitor, and a config object. Everything that connects between agents stays outside, in the environment. Get this split right and your testbench becomes a library. Get it wrong and every new protocol means another 800 lines of copy-pasted scoreboard.


1. What an Agent Owns — and What It Doesn't

An agent bundles everything that belongs to a single protocol interface:

  • Driver (active only) — turns sequence items into pin activity.
  • Sequencer (active only) — routes items from sequences to the driver.
  • Monitor — turns pin activity back into transactions. Always present.
  • Config object — virtual interface, active/passive mode, protocol knobs.

The agent exposes exactly two things to the outside world: its analysis port(s) (for observation) and, if active, its sequencer handle (so a sequence can be started on it). Everything else is private.

The rule: an agent owns everything specific to one protocol interface. The environment owns everything that connects two or more agents — scoreboard, coverage collector, virtual sequencer, and any adapters between them.

2. Active vs Passive Agents

Real DUTs often have two instances of the same bus: one your testbench drives, and one the DUT drives while you only observe. The same agent class handles both, controlled by a single flag.

get_is_active() returnsMeaningChildren built
UVM_ACTIVETB drives this interfacedriver + sequencer + monitor
UVM_PASSIVETB only observesmonitor only

The flag comes from the agent's config object. The agent reads it in build_phase, applies it with set_is_active(), and then guards child creation with get_is_active():

class fifo_agent extends uvm_agent;
    `uvm_component_utils(fifo_agent)

    fifo_driver    drv;
    fifo_monitor   mon;
    fifo_sequencer sqr;
    fifo_agent_cfg cfg;

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);

        if (!uvm_config_db#(fifo_agent_cfg)::get(this, "", "cfg", cfg))
            `uvm_fatal("NOCFG", {"no cfg for ", get_full_name()})

        // Apply mode BEFORE building children
        if (cfg.is_active == UVM_PASSIVE)
            set_is_active(UVM_PASSIVE);

        mon = fifo_monitor::type_id::create("mon", this);

        if (get_is_active() == UVM_ACTIVE) begin
            drv = fifo_driver::type_id::create("drv", this);
            sqr = fifo_sequencer::type_id::create("sqr", this);
        end
    endfunction

    virtual function void connect_phase(uvm_phase phase);
        if (get_is_active() == UVM_ACTIVE)
            drv.seq_item_port.connect(sqr.seq_item_export);
    endfunction
endclass

Two subtleties worth calling out:

  • set_is_active() must run before any child is created. The guard get_is_active() == UVM_ACTIVE is what decides whether the driver and sequencer exist. If you call set_is_active() after creating children, they exist but are never told the mode is passive — usually a bug.
  • The monitor is always built. Passive means "don't drive," not "don't observe." A passive agent without a monitor is not an agent, it's an empty shell.
Common mistake: reading the inherited is_active field directly instead of calling get_is_active(). Before set_is_active() runs, the field still holds its default (usually UVM_ACTIVE) and doesn't reflect the config. Always use the getter.

3. The Agent Config Object

Every agent gets its own config object. Keep it focused — virtual interface, mode, protocol knobs. Do not stuff env-level settings in here; those belong to a separate env config.

class fifo_agent_cfg extends uvm_object;
    `uvm_object_utils(fifo_agent_cfg)

    virtual fifo_if        vif;
    uvm_active_passive_enum is_active = UVM_ACTIVE;
    rand int unsigned      depth      = 16;

    function new(string name = "fifo_agent_cfg");
        super.new(name);
    endfunction
endclass

In the test's build_phase, create one config per agent and set it at the agent's instance path — not with a wildcard:

fifo_agent_cfg cfg_wr = fifo_agent_cfg::type_id::create("cfg_wr");
cfg_wr.vif       = wr_vif;
cfg_wr.is_active = UVM_ACTIVE;

fifo_agent_cfg cfg_rd = fifo_agent_cfg::type_id::create("cfg_rd");
cfg_rd.vif       = rd_vif;
cfg_rd.is_active = UVM_PASSIVE;

uvm_config_db#(fifo_agent_cfg)::set(this, "env.wr_agent", "cfg", cfg_wr);
uvm_config_db#(fifo_agent_cfg)::set(this, "env.rd_agent", "cfg", cfg_rd);

Note the instance-scoped set. A wildcard "*" would deliver both configs to both agents, and whichever the agent's get picked up last would win. Use wildcards for shared settings; use instance paths for per-agent settings.


4. Where the Scoreboard Belongs

The scoreboard connects two or more agents. That makes it an environment child, never an agent child. Putting it inside one of the agents couples the agent to the other, and the agent can no longer be reused alone.

The environment wires everything:

class fifo_env extends uvm_env;
    `uvm_component_utils(fifo_env)

    fifo_agent      wr_agent;
    fifo_agent      rd_agent;
    fifo_scoreboard sb;
    fifo_coverage   cov;

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        wr_agent = fifo_agent::type_id::create("wr_agent", this);
        rd_agent = fifo_agent::type_id::create("rd_agent", this);
        sb       = fifo_scoreboard::type_id::create("sb", this);
        cov      = fifo_coverage::type_id::create("cov", this);
    endfunction

    virtual function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        // The environment is the only place that knows both sides
        wr_agent.mon.ap.connect(sb.wr_export);
        rd_agent.mon.ap.connect(sb.rd_export);

        // Same transactions, multiple subscribers
        wr_agent.mon.ap.connect(cov.wr_export);
        rd_agent.mon.ap.connect(cov.rd_export);
    endfunction
endclass

That is the entire environment. It knows about agents by name, knows which analysis port belongs to which agent, and knows the scoreboard receives both sides. It does not know about the driver, the sequencer, or the protocol timing inside each agent.

Rule of thumb: if a component needs a reference to two agents, it lives in the env. If it only needs the interface of one agent, it can live inside the agent — but typically it doesn't need to, and moving it to the env keeps the agent reusable.

5. The Virtual Sequencer

With two active agents, you eventually need to coordinate them: write on the write port, then read on the read port, in a specific order. A virtual sequence does this by starting sub-sequences on each agent's sequencer. The virtual sequencer holds the handles.

class fifo_vseqr extends uvm_sequencer;
    `uvm_component_utils(fifo_vseqr)

    fifo_sequencer wr_sqr;
    fifo_sequencer rd_sqr;

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
endclass

The env assigns the handles in connect_phase — after both agents and the virtual sequencer exist — and only if the agents are active. A passive agent has no sequencer, so a null handle is the correct state:

virtual function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);

    wr_agent.mon.ap.connect(sb.wr_export);
    rd_agent.mon.ap.connect(sb.rd_export);

    if (wr_agent.get_is_active() == UVM_ACTIVE)
        vseqr.wr_sqr = wr_agent.sqr;
    if (rd_agent.get_is_active() == UVM_ACTIVE)
        vseqr.rd_sqr = rd_agent.sqr;
endfunction

The virtual sequence then does something like:

class fifo_write_then_read_seq extends uvm_sequence;
    `uvm_object_utils(fifo_write_then_read_seq)
    `uvm_declare_p_sequencer(fifo_vseqr)

    fifo_write_seq wseq;
    fifo_read_seq  rseq;

    task body();
        wseq = fifo_write_seq::type_id::create("wseq");
        rseq = fifo_read_seq::type_id::create("rseq");

        if (p_sequencer.wr_sqr != null)
            wseq.start(p_sequencer.wr_sqr);
        if (p_sequencer.rd_sqr != null)
            rseq.start(p_sequencer.rd_sqr);
    endtask
endclass

The null checks make the sequence safe to run when one side is passive — useful when a test uses the same sequence against a full TB and a passive-side regression.

Do not put analysis ports on a virtual sequencer. A virtual sequencer holds sequence handles only. Observation flows through the agents' monitors straight to the scoreboard and coverage. Mixing the two roles makes the virtual sequencer impossible to reuse.

6. When an Agent Becomes a VIP

An agent graduates to "VIP" the moment you can hand it to another team and they can use it without reading its internals. Three conditions make that true:

  1. The config object is the only public API. A user sets fifo_agent_cfg, calls type_id::create, and connects the analysis port. Nothing else.
  2. Active/passive is honored end to end. Passive mode must produce identical transactions to active mode — otherwise the VIP is only useful in one role.
  3. The agent ships with a base sequence library. A user should not have to write write_addr(); write_data(); write_resp(); from scratch. The VIP provides a protocol sequence library, and the user writes the scenario on top.

Real VIPs also expose a factory override point for the driver and monitor, so a user can extend behavior — error injection, coverage extensions, or a different timing mode — without editing your code. That is exactly the Day 5 factory pattern applied to the agent's internals.


7. Common Layering Bugs

SymptomCause
Passive agent still drives pinsDriver created before set_is_active(), or get_is_active() not consulted in build_phase
Scoreboard null-handle crash in a passive testScoreboard connects to a passive agent's sequencer (which doesn't exist)
Config applies to the wrong agentWildcard set(this, "*", ...) instead of an instance path
Two subscribers see different transactionsMonitor reuses a single transaction handle across write() calls (Day 4, section 5)
Virtual sequence does nothingVirtual sequencer handles never assigned, because the env's connect_phase forgot them
Agent can't be reused in another projectScoreboard or coverage lives inside the agent, coupling it to a specific pair of interfaces

8. What Day 7 Will Add

Tomorrow is the Week 1 Review and Mini-Project. You will take everything from Days 1–6 and assemble a complete FIFO verification environment from scratch: a write agent, a read agent, an env with a scoreboard, two sequences, and a test that runs end to end and reports pass or fail. No new concepts — just the muscle memory of building the whole thing yourself.


9. Quiz

Ten questions on agents, active/passive modes, environment layering, and virtual sequencers. One at a time, options shuffled per page load, Submit reveals all answers with explanations.

Question 1 of 10
Loading…
    0 / 10
    Advertisement
    Bottom banner — drop your 728×90 or responsive ad unit here.


    Coming Up Next: Day 7 — Week 1 Review + Mini-Project

    • Assemble the FIFO testbench end to end — write agent, read agent, env, scoreboard, and a test, built from scratch.
    • Run a smoke test and a longer run — confirm the scoreboard reports pass, and that a deliberately broken DUT causes a fail.
    • Review the key mistakes from Week 1 — race conditions, null config, factory overrides that silently don't apply.
    • Prepare for Week 2 — virtual sequences, advanced scoreboards, coverage, and the leap from "working" to "reusable."

    Series: 30 days of SystemVerilog/UVM verification. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 put it in motion. Day 4 wired the plumbing. Day 5 installed the knobs. Day 6 made it modular.

    No comments:

    Post a Comment

    SiteMap