In IP-level verification you build one testbench for one DUT: a single agent drives a single interface, and a single scoreboard checks the result. At SoC level that neat picture breaks down. You are now verifying dozens of interconnected blocks, each with its own protocol, its own UVC, and its own idea of what "correct" means - and a CPU that runs its own software on top of all of it. The industry hasn't solved this by writing a bigger monolithic testbench. It solves it by reusing the block-level environments you already have, giving software its own place in the hierarchy, and pushing some checks out of simulation entirely.

12 min read | Intermediate Level

What You'll Learn

  • The two competing strategies for reusing a block-level testbench, and why you shouldn't mix them
  • The standard layer stack: agents, sub-environments, virtual sequencer, scoreboard, RAL
  • What a virtual sequencer actually orchestrates, and why it exists
  • Where embedded software fits into this structure as a second stimulus engine
  • Why some SoC-level properties are proven with formal tools instead of this testbench at all

Block-Level UVCs Don't Get Rewritten - They Get Reused

Every block in the SoC - the DMA controller, the memory subsystem, each peripheral - was already verified standalone with its own UVM verification component (UVC): driver, monitor, sequencer, and a set of sequences. Rewriting that infrastructure at SoC level would waste the effort already spent proving it correct, so the SoC testbench instantiates the same UVCs again, unmodified.

What changes is who drives the bus. At block level, the UVC's driver actively generates every transaction. At SoC level, many of those interfaces are now driven by something else - embedded software running on an RTL or ISS-modeled CPU, or another block's controller - so the matching agent is reconfigured to run passive. It still monitors the bus and feeds the scoreboard; it just stops driving. A smaller number of agents, typically on interfaces with no other natural driver (an external memory model, a test-only debug port), stay active.

This active-to-passive switch is normally a single is_active field in the agent's configuration object, set through uvm_config_db from the top-level test - not a code change in the UVC itself.

Two Ways to Reuse a Block-Level Testbench

"Reuse the block-level UVC" still leaves a real design decision open: reuse at the agent level, or reuse at the environment level. Both show up in production testbenches, and they solve the same problem differently.

  • Agent-level reuse. The SoC environment instantiates the block's individual agents directly - apb_agent, dma_agent - and wires them up itself alongside its own scoreboard and virtual sequencer. Nothing from the block-level environment class comes along except the agent.
  • Environment-level reuse. The SoC environment instantiates the block's entire existing uvm_env - agent, local scoreboard, local coverage collector and all - as a child component, then reconfigures what it doesn't need (typically disabling the block-level scoreboard and coverage, since the SoC-level scoreboard now owns that checking).

Environment-level reuse means less new code - you're not re-declaring agent handles the block env already manages - but it drags in components you have to actively silence. Agent-level reuse means more boilerplate up front, but nothing sits in the hierarchy that isn't doing something at SoC level.

Pick one strategy and stay consistent.

Mixing the two within the same testbench - some sub-environments reusing whole block-level envs, others hand-picking agents - makes it unpredictable where a given piece of checking or coverage actually lives, which is exactly the kind of ambiguity a large SoC testbench can't afford. Whichever strategy a team picks, it should apply it the same way at every sub-environment boundary.

The Standard Layer Stack

Most production SoC testbenches settle into three layers, regardless of vendor or methodology flavor:

  • soc_env - the top-level environment. Owns the virtual sequencer, the top-level (or chained) scoreboard, and the unified register model (RAL).
  • Sub-environments - group related blocks that naturally interact, such as a CPU subsystem or a peripheral cluster. These exist mainly for hierarchy and reuse across chip variants, not for new checking logic.
  • Block-level UVC environments - the reused, largely unmodified IP-level agents, now mostly passive, sitting at the bottom of the tree.

Keep the sub-environment layer thin. If it starts collecting its own checking or coverage logic instead of just instantiating and connecting child UVCs, that logic usually belongs in the scoreboard or a dedicated coverage collector instead.

One sub-environment deserves special mention: the cpu_subsystem_env. Once the DUT has a CPU, that sub-environment isn't just hosting a passive cpu_agent the way a peripheral sub-environment hosts a passive UVC. It also has to get compiled software onto the CPU and running in the first place, which is a structural concern of the testbench, not just a test-writing concern.

The Virtual Sequencer Ties the Blocks Together

A single UVC's sequencer only knows about its own driver. SoC-level scenarios - "start a DMA transfer while the CPU polls its status register" - span multiple sequencers at once, so the environment needs a coordinating point above them: the virtual sequencer. It doesn't drive any interface itself; it just holds handles to the sequencers of the active agents underneath it.


class soc_virtual_sequencer extends uvm_sequencer;
  `uvm_component_utils(soc_virtual_sequencer)

  // Handles into each active block's sequencer.
  // Populated in soc_env::connect_phase() from the child UVC handles.
  uvm_sequencer #(apb_seq_item) apb_sqr;
  uvm_sequencer #(dma_seq_item) dma_sqr;

  // Rest of the code (constructor)
endclass

A virtual sequence runs on this sequencer and forks child sequences onto the individual block sequencers it holds:


class dma_while_cpu_polls_vseq extends uvm_sequence;
  `uvm_object_utils(dma_while_cpu_polls_vseq)
  `uvm_declare_p_sequencer(soc_virtual_sequencer)

  virtual task body();
    fork
      // Kick off a DMA transfer on the DMA block's sequencer
      dma_kickoff_seq::type_id::create("dma_kickoff_seq").start(p_sequencer.dma_sqr);
      // Simultaneously poll the status register on the APB sequencer
      apb_poll_status_seq::type_id::create("apb_poll_status_seq").start(p_sequencer.apb_sqr);
    join
  endtask
endclass

`uvm_declare_p_sequencer gives the virtual sequence a correctly typed p_sequencer handle, so p_sequencer.dma_sqr and p_sequencer.apb_sqr resolve without manual casting. If you haven't used virtual sequencers before, ChipVerify's virtual sequencer article covers the block-level mechanics this builds on.

Only the agents left active need a handle on the virtual sequencer - the passive ones have nothing to receive stimulus from, so they're absent here by design, not by oversight.

Two Stimulus Engines: The Virtual Sequencer and the CPU

The virtual sequencer only accounts for half the picture. As soon as the DUT includes a CPU, the SoC testbench has two independent stimulus engines running side by side: the virtual sequencer driving whatever agents are left active in SV, and the CPU itself, fetching and executing a compiled software image that drives every interface reconfigured to passive. Structurally, the environment has to make room for both, not just the one carried over from block-level verification.

That means cpu_subsystem_env typically owns a second kind of component alongside its agents - one whose job isn't to drive a protocol, but to get the compiled test image into memory and release the CPU from reset so it starts fetching:


class cpu_subsystem_env extends uvm_env;
  `uvm_component_utils(cpu_subsystem_env)

  cpu_agent   cpu_agt;   // passive - reused IP-level UVC, observes the bus
  sw_loader   sw_ldr;    // loads the compiled C test image, releases reset

  virtual function void build_phase(uvm_phase phase);
    cpu_agt = cpu_agent::type_id::create("cpu_agt", this);
    sw_ldr  = sw_loader::type_id::create("sw_ldr", this);
  endfunction

  // Rest of the code (connect_phase, run_phase)
endclass

sw_loader is a plain uvm_component, not a UVC - it has no driver or sequencer of its own. Its job is purely to get software running; the resulting bus traffic is what the (now passive) cpu_agt observes. Chapter 2 covers how it actually gets the image in - backdoor memory loads versus a frontdoor boot ROM - and how the reset-to-fetch sequence is timed.

The two engines aren't interchangeable, and a testbench that only builds for one of them is missing half its coverage. The virtual sequencer can hit corner cases and error conditions no real driver would ever construct; the CPU running production or near-production software is the only thing that proves the chip works the way it will actually be used. Both need to be able to run in the same test, often concurrently, and both need to report into the same top-level scoreboard so a failure caught by either one fails the test.

This is a hand-off point, not a deep dive. How the two engines actually coordinate - a C test signaling completion to an SV sequence, an SV sequence injecting a fault while software is mid-transaction - is structural enough to affect cpu_subsystem_env's design, but the mechanics belong to the next article in this chapter, The Role of Software in SoC Verification.

Not Everything Runs Through This Testbench: Where Formal Fits

Even a well-structured soc_env with both stimulus engines wired up isn't where every SoC-level property gets proven. Some checks are structural rather than behavioral - they're about how the chip is wired, not what it computes - and those are usually handed to a formal tool running as a separate flow alongside simulation, not built as another component inside this environment.

The clearest example is connectivity checking: confirming that every signal that's supposed to reach a pin, a reset tree, or a multiplexed pad actually does, across every mode the chip supports. A directed simulation test can pass while a watchdog reset still fails to reach some flip-flop it was never specifically checked against - the test wasn't wrong, it just never exercised that particular fan-out path. Formal connectivity tools instead prove every path exhaustively from the netlist itself, which is a better fit for a property that's about structure, not about a specific stimulus sequence.

  • Connectivity and pin muxing - every intended source-to-destination path, across every mode, proven exhaustively rather than sampled by directed tests.
  • Reset and clock-domain-crossing (CDC) checks - fan-out and synchronization properties that are impractical to cover completely with directed or even randomized simulation.
  • Register and address-decode access - confirming every register in the unified map decodes and responds as specified, often before the SoC testbench is even stable enough to run full tests.

Formal also runs earlier at the block level, signing off protocol compliance and corner-case properties before that block's UVC ever gets reused here - so by the time you're composing soc_env, some properties already have a formal proof behind them and don't need re-proving in simulation. Chapter 4's Connectivity Verification: Why Formal, Not Simulation covers this in depth.

The practical takeaway for testbench structure: soc_env doesn't have to (and shouldn't try to) simulate every property just because it's SoC-wide. The verification plan for a chip is the sum of this UVM testbench, the formal flows running beside it, and - later in the flow - emulation and FPGA prototyping. This article is about one piece of that plan, not all of it.

Configuration Makes One Testbench Fit Many Chips

Few teams verify a single fixed SoC. The same testbench usually has to support several configurations of the chip: a version with two DMA channels versus four, or an address map that shifts a peripheral's base address between product variants. Hard-coding these differences into the environment defeats the point of building a reusable SoC testbench.

Instead, a single configuration object - distributed through uvm_config_db before build_phase - carries fields like num_dma_channels or has_secure_region, and every environment's build_phase reads it to decide how many child components to construct and how to wire the address map. The test class selects which configuration to apply; the environment code itself never changes between chip variants.

Treat the configuration object the same way you treat the register model: one source of truth, built once per test, read everywhere else. Scattering +define macros or hard-coded parameters through the environment recreates the maintenance problem configuration objects exist to remove.

Common Mistakes

Mistake: Treating the SoC testbench as one flat environment.

Instantiating every block-level agent directly inside a single top-level environment, instead of composing sub-environments, makes the code harder to reuse across chip variants and harder to debug when a block's UVC changes upstream.

Mistake: Leaving block-level agents active by default at SoC level.

If an agent that should be passive is left active, it drives the same bus the real CPU or software is also driving, corrupting the transaction stream in ways that look like a DUT bug but are really a testbench misconfiguration.

Mistake: Building only for the virtual sequencer and treating software as an afterthought.

A cpu_subsystem_env with no first-class way to load and run software forces every software-shaped scenario to be hand-built in SV instead, which both duplicates work and misses the bugs that only show up when real production code runs on the CPU.

Mistake: Mixing agent-level and environment-level reuse in the same testbench.

Reusing whole block-level environments in some sub-environments and hand-picking individual agents in others leaves no consistent place to look for a given block's checking or coverage, which slows down debug and code review as the testbench grows.

Mistake: Trying to prove structural properties in simulation.

Writing directed SV tests to confirm every reset reaches every flip-flop, or every pin-mux path is wired correctly, can pass without actually covering every mode and path - simulation only proves what it happens to exercise. These are exhaustive, structural properties formal tools are built to prove; spending simulation cycles on them is effort better spent on behavioral scenarios.

Summary

In this article, you learned:

  • Block-level testbenches get reused either at the agent level or the environment level - pick one strategy and apply it consistently
  • The standard hierarchy is soc_env → sub-environments → block-level UVCs, with the virtual sequencer, scoreboard, and RAL living at the top
  • A virtual sequencer holds handles to the active agents' sequencers, letting virtual sequences coordinate scenarios across multiple blocks
  • The CPU running embedded software is a second, independent stimulus engine, structurally supported through components like a sw_loader inside cpu_subsystem_env
  • Structural, exhaustively-provable properties like connectivity, reset fan-out, and CDC are typically verified by formal tools running alongside this testbench, not inside it
  • Configuration objects let one testbench codebase support multiple DUT variants without changing environment code
Did you find what you were looking for?

Quiz

No quiz questions available for this article.