Module 1: Scalable Environment Architecture

UVM Testbench Partitioning

Learning objectives

  • Explain the core mental model behind UVM Testbench Partitioning
  • Apply UVM Testbench Partitioning within Scalable Environment Architecture
  • Identify important boundaries, trade-offs, and failure modes
  • Produce concrete evidence from the practice exercise

Related: UVM Active vs Passive Agents | UVM Environment Topology | UVM Sequences and Items | UVM Register Model | UVM Scoreboard and Checking


Why Testbench Partitioning Matters

Verification effort typically exceeds design effort by 2-3x. If you build an IP-level testbench and then throw it away when moving to SoC integration, you are paying that cost twice. UVM's component architecture was designed specifically to enable reuse across abstraction levels — the same agents, monitors, and coverage collectors that proved an IP works in isolation should plug directly into the SoC-level testbench.

But reuse is not automatic. You have to plan for it. The decisions you make about environment structure, sequence abstraction, configuration, and address mapping at IP level directly determine how painful (or painless) the SoC transition will be.


IP Verification Closure

What It Means

IP verification closure means you have proven, with measurable evidence, that the IP block works correctly in isolation. This includes:

  • Functional coverage targets met (cross-coverage of modes, boundary conditions)
  • Code coverage analyzed and justified (toggle, line, branch, FSM, condition)
  • Assertion coverage — all protocol checkers and internal assertions hit
  • Corner cases exercised (error injection, back-pressure, boundary lengths)
  • Regression is clean and stable

Why It Must Happen Before SoC

Debugging at SoC level is dramatically harder than at IP level:

FactorIP LevelSoC Level
Simulation speedFast10-100x slower
Signal visibilityFullLimited (too many signals)
Root cause isolationDirectMust trace through interconnect
Waveform sizeManageableGigabytes per test
Iteration timeMinutesHours

If you skip IP closure and discover bugs at SoC level, you pay the SoC debugging cost for what should have been a cheap IP-level fix.


ASCII Diagram: IP-Level Testbench

 +====================================================================+
 |  IP-LEVEL TESTBENCH                                                |
 |                                                                    |
 |  +--------------------------------------------------------------+  |
 |  |  uart_env (uvm_env)                                         |  |
 |  |                                                              |  |
 |  |  +------------------+          +------------------+          |  |
 |  |  | apb_agent        |          | uart_agent       |          |  |
 |  |  | (UVM_ACTIVE)     |          | (UVM_ACTIVE)     |          |  |
 |  |  | sqr + drv + mon  |          | sqr + drv + mon  |          |  |
 |  |  +--------+---------+          +--------+---------+          |  |
 |  |           |                             |                    |  |
 |  |           v                             v                    |  |
 |  |  ~~~~ APB i/f ~~~~~           ~~~~ UART i/f ~~~~~           |  |
 |  |           |                             |                    |  |
 |  |           v                             v                    |  |
 |  |  +----------------------------------------------+            |  |
 |  |  |              UART IP (DUT)                   |            |  |
 |  |  +----------------------------------------------+            |  |
 |  |                                                              |  |
 |  |  +------------------+          +------------------+          |  |
 |  |  | uart_scoreboard  |          | uart_coverage    |          |  |
 |  |  +------------------+          +------------------+          |  |
 |  |                                                              |  |
 |  |  +------------------+                                        |  |
 |  |  | reg_model        |   <-- register abstraction layer       |  |
 |  |  +------------------+                                        |  |
 |  +--------------------------------------------------------------+  |
 |                                                                    |
 |  Sequences: focused on UART features                               |
 |  - baud rate configs, parity modes, FIFO depth, error injection    |
 |  Address map: simple, base_addr = 0x0                              |
 +====================================================================+

At IP level, you have direct access to every interface. Both agents are active — you drive the APB config port and the UART serial interface. The address map is trivial (registers start at offset 0). Sequences target specific IP features.


ASCII Diagram: SoC-Level Testbench

 +===========================================================================+
 |  SOC-LEVEL TESTBENCH                                                      |
 |                                                                           |
 |  +---------------------------------------------------------------------+  |
 |  |  soc_env (uvm_env)                                                  |  |
 |  |                                                                     |  |
 |  |  +-------------------+                                              |  |
 |  |  | cpu_agent          |    Drives all register access through       |  |
 |  |  | (UVM_ACTIVE)       |    the real interconnect                    |  |
 |  |  | AXI master         |                                            |  |
 |  |  +---------+---------+                                              |  |
 |  |            |                                                        |  |
 |  |            v                                                        |  |
 |  |  +----------------------------------------------+                   |  |
 |  |  |          AXI INTERCONNECT (RTL)              |                   |  |
 |  |  +------+----------+----------+--------+-------+                   |  |
 |  |         |          |          |        |                            |  |
 |  |         v          v          v        v                            |  |
 |  |  +-----------+ +-----------+ +-------+ +-------+                   |  |
 |  |  | UART IP   | | SPI IP    | | DMA   | | GPIO  |                   |  |
 |  |  +-----------+ +-----------+ +-------+ +-------+                   |  |
 |  |         |          |          |        |                            |  |
 |  |         v          v          v        v                            |  |
 |  |  +-----------+ +-----------+ +-------+ +-------+                   |  |
 |  |  | uart_env  | | spi_env   | |dma_env| |gpio_env|                  |  |
 |  |  | (reused)  | | (reused)  | |(reused)| |(reused)|                 |  |
 |  |  +-----------+ +-----------+ +-------+ +-------+                   |  |
 |  |                                                                     |  |
 |  |  +------------------+          +------------------+                 |  |
 |  |  | soc_scoreboard   |          | soc_coverage     |                |  |
 |  |  | (end-to-end)     |          | (integration)    |                |  |
 |  |  +------------------+          +------------------+                 |  |
 |  |                                                                     |  |
 |  |  +------------------+                                               |  |
 |  |  | soc_reg_model    |   <-- unified address map, all IPs           |  |
 |  |  +------------------+                                               |  |
 |  +---------------------------------------------------------------------+  |
 |                                                                           |
 |  Key differences from IP level:                                           |
 |  - APB agent inside uart_env may become PASSIVE (interconnect drives it)  |
 |  - UART agent stays ACTIVE (external interface still needs stimulus)      |
 |  - Address map: UART regs at 0x4000_1000 (not 0x0)                       |
 |  - Sequences: system-level scenarios (boot + config + data flow)          |
 +===========================================================================+

What Changes Between IP and SoC

1. Agent Modes Flip

At IP level, the APB agent driving the UART config port is active — you generate APB transactions directly. At SoC level, the real AXI interconnect generates APB transactions (after address decoding). So the APB agent inside the reused uart_env becomes passive — it monitors but does not drive. Stimulus now enters through a single CPU/AXI agent at the top.

  IP Level:   test -> apb_agent (ACTIVE) -> APB bus -> UART regs
  SoC Level:  test -> cpu_agent (ACTIVE) -> AXI bus -> interconnect -> APB bus -> UART regs
                                                        uart_env's apb_agent (PASSIVE) monitors here

2. Address Maps Change

At IP level, register 0 of the UART might be at address 0x0000_0000. At SoC level, the same register is at 0x4000_1000 because the system memory map places the UART peripheral there.

The UVM register model handles this via address maps and offsets:

// IP level: registers start at base 0
uart_reg_block.default_map.set_base_addr('h0);

// SoC level: registers at their system address
uart_reg_block.default_map.set_base_addr('h4000_1000);

If you hard-coded 0x0 in your IP-level sequences, they break at SoC level. This is why you should always use the register model's API (reg.write(), reg.read()) instead of raw addresses in sequences.

3. Sequences Scope Expands

IP-level sequences focus on the IP's features:

  • "Configure UART for 115200 baud, 8N1, send 256 bytes"
  • "Test FIFO overflow with back-pressure"

SoC-level sequences are system scenarios:

  • "Boot CPU, initialize clock tree, configure UART, configure DMA to feed UART, verify end-to-end data"
  • "Run UART and SPI simultaneously, check no resource conflicts on shared interconnect"

The IP-level sequences can still run at SoC level (through the register model, routed via the CPU agent), but you also need new integration sequences.

4. Scoreboards Need End-to-End Checking

At IP level, the UART scoreboard checks: "APB wrote config X, UART output matches expected serial frame."

At SoC level, you need additional checks:

  • Data written by CPU through interconnect arrives correctly at UART registers
  • DMA transfers complete without corruption across the bus fabric
  • Interrupt propagates from UART through interrupt controller to CPU

The IP-level scoreboard still works for its local checks, but the SoC env adds system-level scoreboards for cross-IP verification.

5. Timing and Arbitration Effects

The interconnect introduces latency and arbitration that does not exist at IP level. Tests that passed at IP level might expose new issues:

  • Burst transfers getting split by the interconnect
  • Ordering violations when multiple masters access the same slave
  • Wait states from bus contention affecting protocol timeouts

Hierarchical Environment Architecture

The key reuse pattern is hierarchical composition: the SoC environment instantiates IP-level environments as sub-components.

// ============================================================
// IP-Level Environment (designed for reuse)
// ============================================================
class uart_env extends uvm_env;
  `uvm_component_utils(uart_env)

  apb_agent     apb_agt;
  uart_agent    uart_agt;
  uart_scoreboard sb;
  uart_coverage   cov;
  uart_reg_block  reg_model;

  // Configuration object controls agent modes and addresses
  uart_env_cfg  cfg;

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

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

    // Get configuration — this is how SoC env controls our behavior
    if (!uvm_config_db#(uart_env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "uart_env_cfg not found in config_db")

    // Agent mode is driven by configuration
    uvm_config_db#(uvm_active_passive_enum)::set(
      this, "apb_agt", "is_active", cfg.apb_agent_is_active);
    uvm_config_db#(uvm_active_passive_enum)::set(
      this, "uart_agt", "is_active", cfg.uart_agent_is_active);

    apb_agt  = apb_agent::type_id::create("apb_agt", this);
    uart_agt = uart_agent::type_id::create("uart_agt", this);
    sb       = uart_scoreboard::type_id::create("sb", this);
    cov      = uart_coverage::type_id::create("cov", this);

    // Register model
    reg_model = uart_reg_block::type_id::create("reg_model");
    reg_model.build();
    reg_model.lock_model();
    reg_model.default_map.set_base_addr(cfg.base_addr);

    // Connect register model to the appropriate adapter
    if (cfg.apb_agent_is_active == UVM_ACTIVE) begin
      // IP level: reg model drives through our own APB agent
      reg_model.default_map.set_sequencer(apb_agt.sequencer, cfg.reg_adapter);
    end
    // At SoC level, the SoC env's register model handles access routing
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    apb_agt.monitor.ap.connect(sb.apb_in);
    uart_agt.monitor.ap.connect(sb.uart_in);
    uart_agt.monitor.ap.connect(cov.analysis_export);
  endfunction
endclass


// ============================================================
// Configuration Object — controls reuse behavior
// ============================================================
class uart_env_cfg extends uvm_object;
  `uvm_object_utils(uart_env_cfg)

  uvm_active_passive_enum apb_agent_is_active  = UVM_ACTIVE;
  uvm_active_passive_enum uart_agent_is_active = UVM_ACTIVE;
  bit [31:0] base_addr = 32'h0;
  reg2apb_adapter reg_adapter;

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

SoC Environment Instantiating Sub-Envs

// ============================================================
// SoC-Level Environment
// ============================================================
class soc_env extends uvm_env;
  `uvm_component_utils(soc_env)

  // Top-level CPU agent — the single entry point for all register access
  axi_agent  cpu_agt;

  // Reused IP-level environments
  uart_env   uart0_env;
  uart_env   uart1_env;   // two UART instances on this SoC
  spi_env    spi0_env;
  dma_env    dma0_env;

  // SoC-level components
  soc_scoreboard    soc_sb;
  soc_virtual_sequencer v_sqr;

  // Unified register model
  soc_reg_block  soc_reg_model;

  // Configs
  uart_env_cfg  uart0_cfg;
  uart_env_cfg  uart1_cfg;
  spi_env_cfg   spi0_cfg;
  dma_env_cfg   dma0_cfg;

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

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

    // ---- CPU agent: active, drives AXI transactions ----
    uvm_config_db#(uvm_active_passive_enum)::set(
      this, "cpu_agt", "is_active", UVM_ACTIVE);
    cpu_agt = axi_agent::type_id::create("cpu_agt", this);

    // ---- UART0: APB passive (interconnect drives), UART active (external) ----
    uart0_cfg = uart_env_cfg::type_id::create("uart0_cfg");
    uart0_cfg.apb_agent_is_active  = UVM_PASSIVE;  // interconnect drives APB
    uart0_cfg.uart_agent_is_active = UVM_ACTIVE;    // we still drive serial line
    uart0_cfg.base_addr            = 32'h4000_1000; // SoC address map
    uvm_config_db#(uart_env_cfg)::set(this, "uart0_env", "cfg", uart0_cfg);
    uart0_env = uart_env::type_id::create("uart0_env", this);

    // ---- UART1: same agent, different address ----
    uart1_cfg = uart_env_cfg::type_id::create("uart1_cfg");
    uart1_cfg.apb_agent_is_active  = UVM_PASSIVE;
    uart1_cfg.uart_agent_is_active = UVM_ACTIVE;
    uart1_cfg.base_addr            = 32'h4000_2000;
    uvm_config_db#(uart_env_cfg)::set(this, "uart1_env", "cfg", uart1_cfg);
    uart1_env = uart_env::type_id::create("uart1_env", this);

    // ---- SPI0 ----
    spi0_cfg = spi_env_cfg::type_id::create("spi0_cfg");
    spi0_cfg.apb_agent_is_active = UVM_PASSIVE;
    spi0_cfg.base_addr           = 32'h4000_3000;
    uvm_config_db#(spi_env_cfg)::set(this, "spi0_env", "cfg", spi0_cfg);
    spi0_env = spi_env::type_id::create("spi0_env", this);

    // ---- DMA0 ----
    dma0_cfg = dma_env_cfg::type_id::create("dma0_cfg");
    dma0_cfg.axi_agent_is_active = UVM_PASSIVE;  // DMA master port is observed
    dma0_cfg.base_addr           = 32'h4000_4000;
    uvm_config_db#(dma_env_cfg)::set(this, "dma0_env", "cfg", dma0_cfg);
    dma0_env = dma_env::type_id::create("dma0_env", this);

    // ---- SoC-level components ----
    soc_sb  = soc_scoreboard::type_id::create("soc_sb", this);
    v_sqr   = soc_virtual_sequencer::type_id::create("v_sqr", this);

    // ---- Unified register model ----
    soc_reg_model = soc_reg_block::type_id::create("soc_reg_model");
    soc_reg_model.build();
    soc_reg_model.lock_model();
    // All register access routes through the CPU agent
    soc_reg_model.default_map.set_sequencer(cpu_agt.sequencer, axi_reg_adapter);
  endfunction

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

    // Virtual sequencer gets handles to all agent sequencers
    v_sqr.cpu_sqr    = cpu_agt.sequencer;
    v_sqr.uart0_sqr  = uart0_env.uart_agt.sequencer;
    v_sqr.uart1_sqr  = uart1_env.uart_agt.sequencer;

    // SoC scoreboard gets end-to-end transaction streams
    cpu_agt.monitor.ap.connect(soc_sb.cpu_export);
    uart0_env.uart_agt.monitor.ap.connect(soc_sb.uart0_export);
    uart1_env.uart_agt.monitor.ap.connect(soc_sb.uart1_export);
    dma0_env.dma0_agt.monitor.ap.connect(soc_sb.dma0_export);
  endfunction
endclass

The Unified Register Model at SoC Level

At IP level, each IP has its own register block. At SoC level, you compose them:

class soc_reg_block extends uvm_reg_block;
  `uvm_object_utils(soc_reg_block)

  uart_reg_block  uart0_regs;
  uart_reg_block  uart1_regs;
  spi_reg_block   spi0_regs;
  dma_reg_block   dma0_regs;

  function new(string name = "soc_reg_block");
    super.new(name, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);

    // Instantiate IP register blocks at their SoC addresses
    uart0_regs = uart_reg_block::type_id::create("uart0_regs");
    uart0_regs.build();
    uart0_regs.lock_model();
    default_map.add_submap(uart0_regs.default_map, 'h4000_1000);

    uart1_regs = uart_reg_block::type_id::create("uart1_regs");
    uart1_regs.build();
    uart1_regs.lock_model();
    default_map.add_submap(uart1_regs.default_map, 'h4000_2000);

    spi0_regs = spi_reg_block::type_id::create("spi0_regs");
    spi0_regs.build();
    spi0_regs.lock_model();
    default_map.add_submap(spi0_regs.default_map, 'h4000_3000);

    dma0_regs = dma_reg_block::type_id::create("dma0_regs");
    dma0_regs.build();
    dma0_regs.lock_model();
    default_map.add_submap(dma0_regs.default_map, 'h4000_4000);
  endfunction
endclass

The add_submap call is the key: it takes an IP-level register map (with offsets relative to 0) and places it at the correct SoC address. Sequences that use reg_model.uart0_regs.baud_div.write(status, value) automatically get the right address regardless of whether they run at IP or SoC level.


Integration Challenges

1. Address Translation

SoCs often have multiple address spaces and translation layers (MMU, IOMMU, bus bridges). At IP level, addresses are flat. At SoC level, a DMA engine might see physical addresses while the CPU uses virtual addresses. Your testbench must model or account for these translations.

  CPU virtual addr   -->  MMU  -->  Physical addr  -->  Interconnect  -->  IP registers
  0xFFFF_0000_4001_0000         0x4000_1000                              UART0.baud_div

2. Clock Domain Crossings

IP-level testbenches typically use a single clock. SoC integration introduces multiple clock domains:

  • CPU domain (e.g., 500 MHz)
  • Peripheral bus domain (e.g., 100 MHz)
  • UART baud clock (e.g., 1.8432 MHz)

CDC bugs only manifest at SoC level. Your IP-level testbench cannot catch them because there is no domain crossing. This is one reason SoC-level verification exists despite thorough IP closure.

3. Interconnect Effects

The bus fabric introduces behaviors absent at IP level:

  • Arbitration delays — multiple masters competing for the same slave
  • Burst splitting — interconnect may break long bursts into shorter ones
  • Outstanding transactions — responses returning out of order
  • Error responses — decode errors for unmapped addresses

Your IP-level agent and scoreboard may not handle these gracefully if they were designed for a clean, dedicated bus.

4. Power Domain Interactions

At SoC level, IPs may be in different power domains. Power gating, retention, and isolation affect register values and interface behavior. None of this exists at IP level.

5. Shared Resources

Multiple IPs may share DMA channels, interrupt lines, or memory regions. Resource conflicts only appear at SoC level when multiple subsystems are active simultaneously.


Side-by-Side Comparison

 +-------------------------------+--------------------------------------+
 |       IP-LEVEL TB             |         SOC-LEVEL TB                 |
 +-------------------------------+--------------------------------------+
 | Single IP under test          | Full SoC with interconnect           |
 | 1-3 agents, all active        | Many agents, mix of active/passive   |
 | Simple address map (base=0)   | System address map (translated)      |
 | Focused feature sequences     | System scenario sequences            |
 | IP-local scoreboard           | End-to-end + IP-local scoreboards    |
 | Single clock domain           | Multiple clock domains               |
 | Fast simulation (minutes)     | Slow simulation (hours)              |
 | Full signal visibility        | Selective visibility                 |
 | No interconnect effects       | Arbitration, splitting, latency      |
 | No CDC issues                 | CDC bugs surface here                |
 | 100% IP coverage target       | Integration coverage targets         |
 +-------------------------------+--------------------------------------+

Design Principles for Reusable IP-Level Testbenches

1. Configuration Objects, Not Hard-Coded Values

Every parameter that might change at SoC level should live in a configuration object: agent modes, base addresses, clock frequencies, timeout values.

2. Use the Register Model for All Register Access

Never hard-code addresses in sequences. Always go through reg_model.some_reg.write(). This makes address remapping transparent.

3. Make Agent Modes Configurable

The IP-level environment should accept configuration for which agents are active vs. passive. The SoC wrapper sets these appropriately.

4. Keep Scoreboards Protocol-Aware, Not System-Aware

An IP-level scoreboard should check protocol correctness (e.g., "UART frame matches configured baud rate and parity"). It should not assume it is the only consumer of the bus. SoC-level scoreboards handle system-level checking.

5. Separate Sequences from Tests

Sequences should be independent objects, not embedded in test classes. This way, SoC-level tests can invoke IP-level sequences through the register model without modifying them.

// Good: reusable sequence
class uart_baud_config_seq extends uvm_reg_sequence;
  `uvm_object_utils(uart_baud_config_seq)

  uart_reg_block reg_model;

  task body();
    uvm_status_e status;
    // Works at both IP and SoC level because it uses the register model
    reg_model.baud_div.write(status, 16'h001B);  // 115200 baud
    reg_model.line_ctrl.write(status, 8'h03);     // 8N1
    reg_model.fifo_ctrl.write(status, 8'h07);     // enable + reset FIFOs
    reg_model.ier.write(status, 8'h01);           // enable RX interrupt
  endtask
endclass

// SoC test reuses the same sequence, just with a different reg_model handle
class soc_uart_test extends uvm_test;
  task run_phase(uvm_phase phase);
    uart_baud_config_seq seq;
    phase.raise_objection(this);

    seq = uart_baud_config_seq::type_id::create("seq");
    seq.reg_model = env.soc_reg_model.uart0_regs;  // SoC address map
    seq.start(env.cpu_agt.sequencer);               // routed through CPU agent

    // ... rest of SoC test scenario ...
    phase.drop_objection(this);
  endtask
endclass

Verification Planning Across Levels

A well-structured verification plan maps features to the appropriate level:

What to VerifyLevelWhy
Register read/write correctnessIPDirect access, fast iteration
Protocol corner cases (error injection)IPEasier to control and observe
Functional modes (all configurations)IPExhaustive coverage is tractable
Address decode through interconnectSoCRequires real bus fabric
Multi-IP interaction scenariosSoCCross-IP dependencies
Interrupt routing and prioritizationSoCRequires interrupt controller
DMA end-to-end data integritySoCRequires real memory and bus arbitration
Clock domain crossing robustnessSoCRequires real multi-clock design
Power state transitionsSoCRequires power management unit
Boot and initialization sequencesSoCSystem-level orchestration

Summary

Testbench partitioning is not just a UVM coding exercise — it is a verification strategy decision. The goal is:

  1. Prove IP correctness cheaply at IP level with fast, targeted tests
  2. Reuse IP testbench components at SoC level without rewriting them
  3. Add system-level verification that covers integration effects IP-level cannot reach
  4. Minimize SoC debugging by ensuring most bugs are caught at IP level

The UVM mechanisms that enable this — configurable agent modes, hierarchical environments, register models with composable address maps, and virtual sequencers — exist precisely because the industry learned the hard way that throwing away IP testbenches and starting fresh at SoC level does not scale.

Practice lab

Create a minimal executable SystemVerilog or UVM artifact that demonstrates UVM Testbench Partitioning. Record the expected behavior, inject one deliberate defect, and capture the evidence that identifies the failure. Add an operational constraint such as concurrency, recovery, security, latency, or cost, and defend the resulting design trade-off.

Review questions

  1. What problem does UVM Testbench Partitioning solve, and what assumptions does it rely on?
  2. Which boundary or failure case is easiest to miss, and how would you expose it?
  3. What alternative design would you consider, and what trade-off would change the decision?
  4. What artifact, trace, test, or metric proves that your implementation is correct?

Completion evidence

  • A working artifact, annotated trace, or reproducible experiment
  • At least one normal case and one deliberately failing or boundary case
  • A concise explanation of the design choice and its trade-offs
  • Saved output showing how correctness was evaluated