UVM
Case Study — A Complete SoC Environment
Compose the APB, AXI, and UART agents and the register layer into a system-level UVM environment — reusing each agent active or passive per integration level, coordinating them with a virtual sequencer and virtual sequences, checking end to end with a system scoreboard, and the bus-contention bug that an agent left active at SoC level causes.
Industry Case Studies · Module 30 · Page 30.5
The Engineering Problem
You built an APB agent, an AXI agent, a UART agent, and a register layer — each a self-contained piece of reusable verification IP. This final case study is the payoff: compose them into a system-level environment for an SoC where those interfaces all coexist. The system env does not rebuild anything; it instantiates the agents you already have, reuses each active or passive depending on who drives that bus at the system level, coordinates them with a virtual sequencer and virtual sequences, and checks end to end with a system scoreboard.
Three system-level mechanisms tie the agents together. Active/passive reuse: at the SoC, the real on-chip masters drive most buses, so the testbench agents on those buses are configured passive (observe only) — driving them would collide with the RTL. A virtual sequencer holds handles to each agent's real sequencer, and a virtual sequence runs on it to coordinate multi-agent scenarios from one place — configure the DUT over APB, then drive UART traffic, while observing AXI. A system scoreboard subscribes to the agents' monitors and checks behavior across interfaces. The one mistake that turns a working block-level agent into a system-level disaster: leaving an agent active on a bus the RTL now drives, causing bus contention.
How do you compose existing APB, AXI, and UART agents into an SoC environment — reusing each active or passive per integration level, coordinating them with a virtual sequencer and virtual sequences, and checking end to end — so the system env is an assembly of reusable IP rather than a rebuild?
Motivation — why composition is the payoff of building IP
The whole point of building agents instead of directed tests is this moment — system-level reuse:
- The system env reuses, it does not rebuild. Every block-level agent drops into the SoC env unchanged, reconfigured active or passive. If the agents were not built for reuse — hardcoded paths, baked-in checks — this composition would be impossible, which is why the earlier discipline mattered.
- Active/passive is what makes reuse real. At block level an agent drives; at SoC level the real RTL drives and the agent observes. One configuration field flips the role, and getting it wrong causes bus contention — the system-level failure that proves the active/passive design earns its keep.
- Virtual sequences coordinate what single sequences cannot. A system scenario spans interfaces — configure over APB, traffic over AXI, side-band over UART — and a virtual sequence on a virtual sequencer is how you orchestrate that from one place, the system-level stimulus mechanism.
- End-to-end checking lives at the system level. Block scoreboards check their interface; the interesting SoC bugs are in the interactions, so a system scoreboard checks behavior across interfaces — where the real integration bugs hide.
The motivation, in one line: the SoC environment is the payoff of building reusable agents — it composes them by reconfiguration into a coordinated, end-to-end-checked system, and the active/passive, virtual-sequence, and system-scoreboard mechanisms are what make that composition work.
Mental Model
Hold the SoC environment as a conductor leading an orchestra of musicians you already trained, most of them now listening rather than playing:
You have spent the season training individual musicians, each excellent at their instrument — that was building the agents. The concert is the SoC, and conducting it is a different job from training a player. You do not re-teach anyone their instrument; you assemble them and coordinate them. The first thing a conductor decides is who plays and who rests in each passage. In the system performance, the real soloists — the actual on-chip masters — carry most of the melody, so your trained musicians on those parts do not play over them; they rest and listen, ready to follow, contributing by their presence and attention rather than their sound. A musician who keeps playing their full part while the soloist is also playing the same line does not add richness — they clash, two instruments fighting over one line, and the result is noise that drowns both. So the conductor is meticulous about who is active and who is passive in each moment. Then the conductor coordinates: from the podium, a single gesture cues the strings to begin, then the brass to enter, then the percussion — one point of control orchestrating independent sections into a coherent whole, which no individual player could do from their seat. And the conductor listens to the whole, not just one section, hearing whether the ensemble is together, because the performance is judged as a whole, not instrument by instrument. The art is not playing every instrument — it is deciding who plays and who rests, cueing them in coordination, and judging the result end to end. You trained the musicians individually — that was building the agents. The concert is the SoC, and conducting is a different job. You do not re-teach instruments; you assemble and coordinate. First you decide who plays and who rests: the real soloists (on-chip masters) carry the melody, so your musicians on those parts rest and listen rather than play over them — a player who keeps playing the soloist's line clashes, two instruments fighting over one line, producing noise. So you are meticulous about active versus passive. Then you coordinate from the podium — one gesture cues strings, then brass, then percussion — one point of control orchestrating independent sections. And you listen to the whole, judging the ensemble end to end.
So building the SoC env is conducting: the agents are the trained musicians, reused; active/passive is who plays and who rests (and the contention bug is two playing the same line); the virtual sequencer and virtual sequence are the podium and the conductor's gestures coordinating the sections; and the system scoreboard is listening to the whole ensemble. The disaster you must avoid is the musician left playing over the soloist — an agent left active on a bus the RTL drives.
The SoC Environment Hierarchy
The system env composes the register env (APB), the AXI agent, and the UART agent, plus a virtual sequencer and a system scoreboard — agents reused, configured by integration level.
The hierarchy is a composition of finished parts. The reg_env brings the APB agent and the register model from the register-verification case study. The AXI agent is reused passive because the real on-chip master drives AXI at the system level. The UART agent may be active (the testbench provides the serial peer). The virtual sequencer is a coordination component — it drives no pins; it holds handles to the agents' real sequencers. The system scoreboard subscribes to the agents' monitors for end-to-end checks. None of the agents changed; they are configured and wired together, which is exactly what reusable IP is for.
Composition, Configuration, and the Virtual Sequencer
The env builds the sub-components, sets each agent active or passive through nested config, builds a virtual sequencer, and wires the sequencer handles and scoreboard in connect_phase.
class soc_config extends uvm_object;
apb_config apb_cfg;
axi_config axi_cfg;
uart_config uart_cfg;
`uvm_object_utils(soc_config)
function new(string name = "soc_config"); super.new(name); endfunction
endclass
class soc_virtual_sequencer extends uvm_sequencer;
`uvm_component_utils(soc_virtual_sequencer)
apb_sequencer apb_seqr; // handles to the agents' REAL sequencers
uart_sequencer uart_seqr; // (no axi handle: AXI is passive here)
function new(string name, uvm_component parent); super.new(name, parent); endfunction
endclassclass soc_env extends uvm_env;
`uvm_component_utils(soc_env)
reg_env regs; // APB agent + register model (reused)
axi_agent axi; // reused, configured passive
uart_agent uart; // reused
soc_virtual_sequencer vseqr;
soc_scoreboard sb;
soc_config cfg;
// ... new() ...
function void build_phase(uvm_phase phase);
if (!uvm_config_db#(soc_config)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "soc_config not provided")
// INTEGRATION-LEVEL CHOICE: the real master drives AXI at SoC level → observe only
cfg.axi_cfg.is_active = UVM_PASSIVE;
uvm_config_db#(axi_config)::set(this, "axi", "cfg", cfg.axi_cfg);
uvm_config_db#(uart_config)::set(this, "uart", "cfg", cfg.uart_cfg);
uvm_config_db#(apb_config)::set(this, "regs.agent","cfg", cfg.apb_cfg);
regs = reg_env::type_id::create("regs", this);
axi = axi_agent::type_id::create("axi", this);
uart = uart_agent::type_id::create("uart", this);
vseqr = soc_virtual_sequencer::type_id::create("vseqr", this);
sb = soc_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
// hand the virtual sequencer the agents' real sequencers (active agents only)
vseqr.apb_seqr = regs.agent.seqr;
vseqr.uart_seqr = uart.seqr;
// end-to-end checking: agent monitors → system scoreboard
axi.mon.ap.connect(sb.axi_in);
uart.mon.ap.connect(sb.uart_in);
regs.agent.mon.ap.connect(sb.apb_in);
endfunction
endclassThe build_phase is composition plus the integration-level choice: it sets cfg.axi_cfg.is_active = UVM_PASSIVE because the real master drives AXI here, then distributes each sub-config down the hierarchy and creates the reused agents and the coordination components — nested config mirroring the component tree. The connect_phase does the two system-level wirings: it hands the virtual sequencer the handles to each active agent's real sequencer (no AXI handle, since AXI is passive and has no driver), and connects each agent's monitor to the system scoreboard for end-to-end checking. Note the AXI monitor still connects to the scoreboard even though the AXI agent is passive — observation is exactly what a passive agent is for.
The Virtual Sequence — coordinating the agents
A virtual sequence runs on the virtual sequencer and orchestrates a multi-agent scenario by starting sub-sequences on the agents' real sequencers, bracketed by one objection.
class soc_smoke_vseq extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(soc_smoke_vseq)
`uvm_declare_p_sequencer(soc_virtual_sequencer) // typed access to the handles
function new(string name = "soc_smoke_vseq"); super.new(name); endfunction
task body();
reg_smoke_seq cfg_seq = reg_smoke_seq::type_id::create("cfg_seq");
uart_tx_seq tx_seq = uart_tx_seq::type_id::create("tx_seq");
// 1. configure the DUT through its registers, on the APB sequencer
cfg_seq.regmodel = p_sequencer.regmodel;
cfg_seq.start(p_sequencer.apb_seqr);
// 2. then drive UART traffic, on the UART sequencer
assert(tx_seq.randomize());
tx_seq.start(p_sequencer.uart_seqr);
endtask
endclass
class soc_test extends uvm_test;
`uvm_component_utils(soc_test)
soc_env env;
// ... build_phase: set soc_config, create env ...
task run_phase(uvm_phase phase);
soc_smoke_vseq vseq = soc_smoke_vseq::type_id::create("vseq");
phase.raise_objection(this); // one objection brackets the whole scenario
vseq.start(env.vseqr); // run the virtual sequence on the virtual sequencer
phase.drop_objection(this);
endtask
endclassThe virtual sequence is the conductor's score. `uvm_declare_p_sequencer gives it typed access to the virtual sequencer's handles via p_sequencer. Its body coordinates across agents: first it starts the register-config sub-sequence on the APB sequencer and waits for it (configure the DUT first), then it starts the UART traffic sub-sequence on the UART sequencer — a multi-interface scenario orchestrated from one place. The test wraps the whole thing in one objection so the run phase stays alive for the entire coordinated scenario, and runs the virtual sequence on env.vseqr. The virtual sequence drives no pins itself; it conducts the real sequencers.
Waveform Perspective — what an agent left active does to the bus
Active/passive is not cosmetic. If the AXI agent is left active while the real master also drives AXI, both drive the same signals and the bus goes to X — the contention this section's DebugLab is about.
Bus contention — the AXI agent left active drives the same bus as the real master
5 cyclesThe waveform shows why the active/passive choice is load-bearing. While the leftover active agent happens not to drive (cycles 1–2), the bus is clean and the bug is latent — exactly why it survives a casual smoke test. The moment the active agent drives (cycle 3), two sources fight over the same wires and the bus resolves to X, which propagates into the DUT and corrupts every downstream check. The cure is the one configuration field: set the agent passive so only the real master drives and the agent observes. This is the system-level consequence of the active/passive design introduced all the way back in the APB agent.
DebugLab — the agent left active at SoC level
X everywhere and intermittent failures — an agent drove a bus the real master owns
The block-level tests all passed, but at SoC level the environment failed with X values propagating through the AXI datapath and into the scoreboard — intermittently. Some runs passed, some failed, and the failures showed X on the AXI bus signals that no block-level test had ever shown. The AXI agent's code was unchanged from the block level where it worked perfectly.
The AXI agent was reused at SoC level but left UVM_ACTIVE, so its driver drove the AXI bus that the real on-chip master also drives — two drivers contending on the same wires:
block level: axi_cfg.is_active = UVM_ACTIVE; // testbench drives AXI — correct there
SoC level: axi_cfg.is_active = UVM_ACTIVE; // ✗ real master ALSO drives AXI now
effect: two drivers (RTL master + testbench agent's driver) on the same bus signals
result: wherever they disagree, the bus resolves to X → propagates into DUT + scoreboard
intermittent: only when the active agent actually drives (depends on stimulus/timing)
fix: axi_cfg.is_active = UVM_PASSIVE; // SoC: real master drives, agent observes ✓At block level the testbench is the AXI master, so the agent is correctly active. At SoC level the real RTL master drives AXI, so an active testbench agent is a second master on the same bus. When both drive, the signals contend and resolve to X wherever the two disagree, and that X spreads through the DUT and into the checks. The intermittency is the giveaway: the contention only manifests on cycles the active agent actually drives, which depends on the stimulus, so some runs look fine. The agent code is blameless — the configuration for the integration level was wrong.
The tell is X on a bus at SoC level that was clean at block level, often intermittent. Localize it at the agent's active/passive configuration:
- Check
is_activefor every agent on a bus the RTL drives. Any agent on a bus with a real on-chip master must beUVM_PASSIVE. An active one is a contending second master. - Trace the
Xto a contended signal.Xthat appears only at SoC level and originates on a bus (not from uninitialized DUT state) points at two drivers; identify which agent shares that bus with the RTL. - Correlate failures with when the agent drives. Intermittent
Xthat coincides with the testbench agent's driving activity confirms contention rather than a DUT bug.
Make the integration-level role explicit and checked:
- Configure agents passive on any bus the RTL drives. At SoC level, set
is_active = UVM_PASSIVEfor every agent whose bus has a real master; only agents on testbench-driven interfaces stay active. - Drive active/passive from the integration-level config, not the agent default. The env's config sets each agent's role for that integration, so the same agent is active at block level and passive at SoC by configuration alone.
- Add bus-contention assertions or a zero-driver check. An assertion that at most one driver is active on a shared bus catches a mis-set agent immediately, instead of as intermittent downstream
X.
The one-sentence lesson: at SoC level the real masters drive the buses, so reuse each testbench agent passive on any bus the RTL drives — an agent left active becomes a second master, contends with the RTL, and resolves the bus to X that propagates everywhere, intermittently.
Common Mistakes
- Leaving an agent active on an RTL-driven bus. Two masters contend and the bus goes
X— the DebugLab. Configure agents passive on any bus a real master drives. - Rebuilding instead of reusing. The system env should instantiate and reconfigure the existing agents, not re-author them. Re-authoring duplicates and drifts.
- Wiring a virtual-sequencer handle to a passive agent. A passive agent has no sequencer; only active agents' sequencers go into the virtual sequencer. Wiring a passive one is a null handle.
- Coordinating without a virtual sequence. Multi-agent scenarios need a virtual sequence on a virtual sequencer; starting independent sequences with no coordination loses the ordering the scenario needs.
- Per-sub-sequence objections instead of one. The objection should bracket the whole coordinated scenario at the virtual-sequence or test level, not be managed independently by each sub-sequence.
- Flat config instead of nested. System configuration should be a nested config tree mirroring the env; scattering individual sets does not scale and invites scope mismatches.
Senior Design Review Notes
Interview Insights
You instantiate the existing block-level agents inside a system env, configure each active or passive depending on who drives its bus at the SoC level, wrap them under one env with nested configuration, coordinate them with a virtual sequencer and virtual sequences, and check across them with a system scoreboard — reusing, not rebuilding. The system env creates the same APB, AXI, UART agents and the register env you built at block level; nothing is re-authored. The key configuration decision is active versus passive: at the SoC, the real on-chip masters drive most buses, so the testbench agents on those buses are set passive, observing and feeding coverage and checking without driving — driving would collide with the RTL. Agents on interfaces the testbench still drives stay active. Configuration flows through a nested config tree: a top soc_config containing each agent's config, distributed down the hierarchy, so the structure mirrors the components. Coordination is through a virtual sequencer that holds handles to the active agents' real sequencers, and virtual sequences that run on it and start sub-sequences on those sequencers to orchestrate multi-interface scenarios from one place. Checking moves up: block scoreboards may remain, but a system scoreboard subscribes to the agents' monitors and checks end-to-end behavior across interfaces, where the integration bugs are. So the system env is an assembly: reuse the agents, set their roles by integration level, coordinate with virtual sequences, check end to end. The understanding to convey is reuse-by-reconfiguration plus virtual-sequence coordination plus system-level checking, which is the payoff of having built each agent as reusable IP.
Agents are passive at SoC level on any bus the real on-chip master drives because the testbench should observe those buses, not drive them, and if you leave such an agent active, its driver contends with the real master on the same wires and the bus resolves to X, which propagates everywhere. At block level the testbench is the master of an interface, so the agent is active and generates stimulus. At the SoC, the real RTL blocks drive each other over those same interfaces, so the testbench's job on them shifts from driving to observing — collecting coverage and feeding the scoreboard. An agent configured passive builds only its monitor and does not drive, which is exactly right. If you mistakenly leave it active, it builds its driver and drives the bus that the real master is also driving, so there are two drivers on the same signals; wherever they disagree, the resolved value is X, the unknown that propagates into the DUT and corrupts every downstream check. The bug is often intermittent, because the contention only shows when the active agent actually drives, which depends on the stimulus, so some runs pass and some fail with X — a classic SoC-integration symptom. The fix is the one configuration field: set the agent passive for that integration level, which the env's config does. This is the system-level reason active/passive is designed into the agent from the start. The understanding to convey is that passive observes a real-master bus while active would contend and produce propagating X, intermittently — which shows you understand active/passive as the mechanism that makes agent reuse across integration levels actually work.
A virtual sequencer is a sequencer that drives no interface but holds handles to the real sequencers of the agents in the environment, and a virtual sequence runs on it and coordinates the agents by starting sub-sequences on those real sequencers, orchestrating a multi-interface scenario from one place. In a system env with several agents, you often need a scenario that spans interfaces — configure the DUT over one bus, then drive traffic on another, perhaps handle a side channel on a third — and that coordination has to live somewhere central. The virtual sequencer is that central point: it is a uvm_sequencer subclass that does not connect to a driver but carries handles to the agents' actual sequencers, wired up in the env's connect_phase. A virtual sequence runs on the virtual sequencer and, through a p_sequencer reference declared with uvm_declare_p_sequencer, reaches those handles; its body starts sub-sequences on the specific agents' sequencers — a register-config sequence on the APB sequencer, then a UART traffic sequence on the UART sequencer — in the order the scenario needs. So the virtual sequence is the conductor: it does not drive pins itself; it conducts the real sequencers. Objections are typically managed once around the whole virtual sequence, at the test or virtual-sequence level, so the run phase stays alive for the coordinated scenario as a whole rather than each sub-sequence managing its own. Only active agents' sequencers go into the virtual sequencer, since passive agents have no sequencer. The understanding to convey is the virtual sequencer as the handle-holding coordination point and the virtual sequence as the conductor that starts sub-sequences on the real sequencers, which is how multi-agent system scenarios are orchestrated.
Block-level checking verifies one interface in isolation — a block scoreboard checks that block's transactions — while system-level end-to-end checking verifies behavior across interfaces, because the interesting SoC bugs are in the interactions between blocks, not within one. At block level, an agent's scoreboard receives that interface's monitored transactions and checks them against an expected model for that block: did the APB register write take, did the AXI read return the right data. That is necessary but not sufficient at the SoC, where the question becomes whether the blocks together do the right thing: a value written over APB to configure a block should change how that block responds to AXI traffic, and a UART byte should produce the expected downstream effect. A system scoreboard subscribes to multiple agents' monitors — APB, AXI, UART — and checks these cross-interface relationships, often with a system-level reference model that predicts the end-to-end behavior from the stimulus across all interfaces. So the difference is scope: block checking is within-interface correctness, system checking is across-interface, end-to-end behavior. Both can coexist — block scoreboards still catch local issues — but the system scoreboard is what catches integration bugs, like a misrouted transaction, a configuration that did not propagate, or an interaction the blocks get wrong together. The understanding to convey is within-interface versus across-interface checking, and that the SoC bugs live in the interactions, so a system scoreboard checking end-to-end across the agents' monitors is what finds them — which shows you understand why checking moves up at the system level.
An agent is composable into an SoC env when it is fully configuration-driven, active/passive capable, factory-built, and free of environment-specific assumptions, so it can be instantiated and reconfigured for the system without editing — and what blocks it is anything hardcoded: a fixed interface path, a baked-in active/passive choice, checks wired into the monitor, or construction with new instead of the factory. The active/passive capability is essential, because the same agent must drive at block level and observe at SoC level; an agent that cannot be passive cannot be reused on an RTL-driven bus. Configuration-driven means everything variable — the interface, options, role — comes from a config object, so the system env supplies a new config rather than touching the agent. Factory construction means the system test can override the agent's item or components if needed. Freedom from environmental assumptions means the agent does not reach up the hierarchy by absolute path, assume a particular parent, or hardcode a config-db scope, so it works wherever it is placed. A monitor kept protocol-pure — observing and broadcasting, not checking — is reusable passive, whereas a monitor with block-specific checks baked in drags assumptions into the SoC. What blocks reuse, concretely, is the inverse of each: hardcoded paths, fixed is_active, new instead of create, checks in the monitor, assumptions about the surrounding env. This is why the discipline at block level — building each agent as reusable IP — is what makes the system composition possible at all; an agent built as a one-off cannot be composed. The understanding to convey is the properties that make an agent composable and the hardcoding that blocks it, and that SoC composition is the test of whether the agents were really built for reuse — which ties the whole case-study arc together.
Exercises
- Set the roles. For an SoC where the RTL drives AXI and APB but the testbench drives UART, state which agents are active and which passive, and what goes wrong if AXI is left active.
- Wire the virtual sequencer. Write the
connect_phaselines that hand the virtual sequencer the active agents' sequencers, and explain why no AXI handle is wired. - Coordinate a scenario. Outline a virtual sequence that configures a block over APB registers, then drives AXI traffic and observes a UART side-band, with one objection.
- Diagnose the X. Given intermittent
Xon the AXI bus at SoC level that never appeared at block level, give your ordered diagnosis and the configuration fix. - Justify reuse. Explain what property of the agents lets the SoC env reuse them unchanged, and name two kinds of hardcoding that would have made composition impossible.
Summary
- A complete SoC environment composes the agents you already built — APB/register env, AXI agent, UART agent — by instantiation and reconfiguration, not by rebuilding.
- Active/passive per integration level is load-bearing: at the SoC the real masters drive the buses, so agents on those buses are passive (observe only); leaving one active contends with the RTL and resolves the bus to
Xthat propagates — intermittently — the DebugLab. - A virtual sequencer holds handles to the active agents' real sequencers, and a virtual sequence (
`uvm_declare_p_sequencer) runs on it to coordinate multi-interface scenarios from one place, bracketed by one objection. - A system scoreboard subscribes to the agents' monitors and checks end-to-end behavior across interfaces — where the integration bugs live, distinct from block-level within-interface checks.
- Nested config mirrors the env hierarchy, and the composition only works because each agent was built reusable — config-driven, active/passive, factory-built, assumption-free.
- The durable rule of thumb: build the SoC environment by reusing each agent active or passive per integration level, coordinating them with a virtual sequencer and virtual sequences and checking end to end with a system scoreboard — because the system env is the payoff of building reusable IP, and the one rule that keeps it from self-destructing is that only one master drives each bus, so every agent on an RTL-driven bus is passive.
Next — Protocol Verification Projects: with a complete SoC environment assembled from reusable agents, the final case study steps back to the project level — how these pieces become a verification plan, a regression, and a coverage-closure effort on a real protocol-verification project, turning the methodology you have built into a shippable verification campaign.