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.
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() returns | Meaning | Children built |
|---|---|---|
UVM_ACTIVE | TB drives this interface | driver + sequencer + monitor |
UVM_PASSIVE | TB only observes | monitor 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 guardget_is_active() == UVM_ACTIVEis what decides whether the driver and sequencer exist. If you callset_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.
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.
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.
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:
- The config object is the only public API. A user sets
fifo_agent_cfg, callstype_id::create, and connects the analysis port. Nothing else. - 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.
- 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
| Symptom | Cause |
|---|---|
| Passive agent still drives pins | Driver created before set_is_active(), or get_is_active() not consulted in build_phase |
| Scoreboard null-handle crash in a passive test | Scoreboard connects to a passive agent's sequencer (which doesn't exist) |
| Config applies to the wrong agent | Wildcard set(this, "*", ...) instead of an instance path |
| Two subscribers see different transactions | Monitor reuses a single transaction handle across write() calls (Day 4, section 5) |
| Virtual sequence does nothing | Virtual sequencer handles never assigned, because the env's connect_phase forgot them |
| Agent can't be reused in another project | Scoreboard 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.
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