UVM-23: Integration-Level Testbench — VLSI Trainers
VLSI Trainers UVM Series · UVM-23
UVM Series · UVM-23

Integration-Level Testbench

Vertical reuse — how block-level UVM environments are composed into an integration-level testbench. Setting block environments to passive, the integration env wrapping sub-envs, nested config objects at integration level, signal binding for internal monitoring, and coordination of stimulus across multiple interfaces.

📋 What Integration-Level Verification Is

Integration-level verification occurs when two or more previously-verified blocks are connected together and the combined system must be verified as a whole. The goal is to verify the interactions between blocks — the handshakes, data paths, and control flows that cross block boundaries — while reusing as much as possible from the block-level verification environments.

A typical example: a peripheral sub-system DUT contains a GPIO block and an SPI block, both connected to an internal APB bus. At block level, each block has its own testbench that drives the APB bus directly. At integration level, the APB bus is internal — only the AHB bus is exposed. The integration testbench drives the AHB bus and must verify that both GPIO and SPI blocks respond correctly to stimulus routed through the bridge.

Block Level vs Integration Level — What Changes Block Level APB Agent (ACTIVE) drives APB bus GPIO DUT block APB port exposed APB Agent (ACTIVE) drives APB bus SPI DUT block APB port exposed Each block tested in isolation. APB bus is directly accessible. Active APB agent drives all stimulus. Block interactions NOT tested. Integration Level AHB Agent (ACTIVE) drives AHB bus PSS DUT (integrated) AHB→APB bridge GPIO block SPI block APB Agent (PASSIVE) monitors internal bus observe internal APB signals via signal bindingAPB bus internal — not accessible AHB drives → bridge → APB → blocks Passive APB agents observe internally Block interactions NOW tested vlsitrainers.com
Figure 1 — Block level vs integration level. At block level, the APB bus is exposed and driven directly by active agents. At integration level, both blocks are integrated inside a PSS DUT; only the AHB bus is exposed. The APB agents from the block-level environments are switched to passive mode and observe the internal APB bus via signal binding.

📋 Vertical Reuse Principles

Vertical reuse means taking verification components from a lower level of integration and using them at a higher level without modification. UVM supports this through four mechanisms:

What changes at integration levelWhat stays the same
Block APB agents switched to passive (is_active = UVM_PASSIVE)Agent class code — driver/monitor/sequencer classes unchanged
Integration env wraps block envs as sub-componentsBlock env class code — build, connect, analysis connections unchanged
New integration-level agent (AHB) addedBlock-level sequences — can still be used at integration level via register abstraction
Signal binding for internal bus observationMonitor classes — still sample the same signals via the same virtual interface
Integration-level virtual sequence coordinates across all agentsBlock-level covergroups — still sample from the same monitor analysis ports

📋 Integration Hierarchy

Integration-Level UVM Component Hierarchy pss_test (uvm_test) pss_env (uvm_env) ← integration level ahb_agent ACTIVE — new at IL gpio_env (reused) gpio apb_agent → PASSIVE gpio_sb gpio_cov gpio_predictor spi_env (reused) spi apb_agent → PASSIVE spi_sb spi_cov spi_predictor vlsitrainers.com
Figure 2 — Integration-level component hierarchy. pss_env wraps gpio_env and spi_env as sub-components, reused from block level without modification. A new ahb_agent (active) is added at integration level to drive the exposed AHB bus. Both block-level APB agents are switched to passive mode via their config objects. All block-level analysis components (scoreboard, coverage, predictor) are retained.

📋 Passive Agents at Integration Level

The single most important configuration change when reusing a block-level environment at integration level: switch the block-level agents from active to passive. This is done entirely through the config object — the agent class code is not touched.

// ── In pss_test build_phase: switch APB agents to passive ─
// gpio_env's APB agent — was active at block level
m_gpio_apb_cfg.is_active = UVM_PASSIVE;   // switch to passive
// APB agent now builds monitor only — no driver/sequencer

// spi_env's APB agent — same switch
m_spi_apb_cfg.is_active  = UVM_PASSIVE;

// The AHB agent at integration level stays ACTIVE
m_ahb_agent_cfg.is_active = UVM_ACTIVE;

// ── What this achieves ────────────────────────────────────
// Block-level APB monitors still run — they observe the
// internal APB bus and broadcast transactions as before.
// Block-level scoreboards and coverage still receive
// and process those transactions.
// No APB stimulus is driven — the AHB agent provides all
// stimulus at integration level.
Zero lines of block-level agent code change. The agent’s build_phase checks m_cfg.is_active — that check was written into the block-level agent at design time. Switching to passive at integration level requires only one field assignment in the integration test’s config setup. This is exactly the reuse dividend that UVM’s is_active flag was designed to deliver.

📋 Integration tb_top Module

The integration tb_top is similar to a block-level tb_top but with multiple interface instances — one per exposed protocol bus. Internal buses that are not exposed also need interfaces, which are connected via bind or wrapper modules.

module pss_tb_top;
  import uvm_pkg::*;
  `include "uvm_macros.svh"
  `include "pss_test_pkg.sv"

  // ── Clock and reset ───────────────────────────────────────
  logic HCLK, HRESETn;
  initial begin HCLK = 0; HRESETn = 0; end
  always  #5ns HCLK = ~HCLK;
  initial begin #40ns; HRESETn = 1; end

  // ── Exposed interfaces ────────────────────────────────────
  ahb_if   AHB(.HCLK(HCLK), .HRESETn(HRESETn));
  spi_if   SPI();
  gpio_if  GPO(.clk(HCLK));
  gpio_if  GPI(.clk(HCLK));

  // ── Internal interface (connected via bind) ───────────────
  apb_if   APB_INT(.PCLK(HCLK), .PRESETn(HRESETn));

  // ── DUT instantiation ─────────────────────────────────────
  pss_dut dut(
    .HCLK(HCLK), .HRESETn(HRESETn),
    .HADDR(AHB.HADDR), .HWDATA(AHB.HWDATA),
    .HWRITE(AHB.HWRITE), .HRDATA(AHB.HRDATA),
    .HREADY(AHB.HREADY), .HRESP(AHB.HRESP),
    .MOSI(SPI.MOSI), .MISO(SPI.MISO),
    .GPO(GPO.data), .GPI(GPI.data)
  );

  // ── Set all virtual interfaces before run_test() ─────────
  initial begin
    uvm_config_db #(virtual ahb_if)::set(
      null, "uvm_test_top", "ahb_vif",     AHB);
    uvm_config_db #(virtual spi_if)::set(
      null, "uvm_test_top", "spi_vif",     SPI);
    uvm_config_db #(virtual gpio_if)::set(
      null, "uvm_test_top", "gpo_vif",     GPO);
    uvm_config_db #(virtual gpio_if)::set(
      null, "uvm_test_top", "gpi_vif",     GPI);
    // Internal APB bus — observed via bind
    uvm_config_db #(virtual apb_if)::set(
      null, "uvm_test_top", "apb_int_vif", APB_INT);
    run_test();
  end
endmodule

📋 Integration Environment

The integration env wraps the block-level envs as sub-components. It retrieves the integration env config, distributes nested sub-env configs to each block env, creates them, and wires analysis connections between levels.

class pss_env extends uvm_env;
  `uvm_component_utils(pss_env)

  // ── Sub-components: new at IL + reused block envs ────────
  ahb_agent   m_ahb_agent;  // new at integration level
  gpio_env    m_gpio_env;   // reused block-level env
  spi_env     m_spi_env;    // reused block-level env
  pss_env_cfg m_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 integration env config
    if (!uvm_config_db #(pss_env_cfg)::get(
          this, "", "cfg", m_cfg))
      `uvm_fatal("PSS", "No env config!")

    // ② Push AHB agent config down
    uvm_config_db #(ahb_agent_cfg)::set(
      this, "m_ahb_agent", "cfg", m_cfg.m_ahb_cfg);

    // ③ Push block-env configs down (already have is_active=PASSIVE)
    uvm_config_db #(gpio_env_cfg)::set(
      this, "m_gpio_env", "cfg", m_cfg.m_gpio_env_cfg);
    uvm_config_db #(spi_env_cfg)::set(
      this, "m_spi_env", "cfg",  m_cfg.m_spi_env_cfg);

    // ④ Create all sub-components
    m_ahb_agent = ahb_agent::type_id::create("m_ahb_agent", this);
    m_gpio_env  = gpio_env::type_id::create("m_gpio_env",  this);
    m_spi_env   = spi_env::type_id::create("m_spi_env",   this);
  endfunction

  function void connect_phase(uvm_phase phase);
    // Connect AHB monitor to integration-level scoreboard
    // (block-level SBs remain connected to their own monitors)
    m_ahb_agent.ap.connect(m_pss_sb.analysis_export);
  endfunction
endclass

📋 Nested Configuration at Integration Level

The integration env config nests all sub-env configs. This means the test creates one top-level config tree, fills in all virtual interface handles and active/passive flags, and makes a single config_db set() call. The pss_env then distributes nested configs to each sub-env.

class pss_env_cfg extends uvm_object;
  `uvm_object_utils(pss_env_cfg)

  // New at integration level
  ahb_agent_cfg m_ahb_cfg;

  // Nested block-level env configs
  gpio_env_cfg  m_gpio_env_cfg;
  spi_env_cfg   m_spi_env_cfg;

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

📋 Integration Base Test

class pss_base_test extends uvm_test;
  `uvm_component_utils(pss_base_test)

  pss_env     m_env;
  pss_env_cfg m_cfg;

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

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

    // ── Create the full nested config tree ────────────────
    m_cfg = pss_env_cfg::type_id::create("m_cfg");

    // AHB agent config (new at integration — ACTIVE)
    m_cfg.m_ahb_cfg = ahb_agent_cfg::type_id::create("m_ahb_cfg");
    m_cfg.m_ahb_cfg.is_active = UVM_ACTIVE;
    if (!uvm_config_db #(virtual ahb_if)::get(
          this, "", "ahb_vif", m_cfg.m_ahb_cfg.vif))
      `uvm_fatal("CFG", "No AHB vif!")

    // GPIO env config — APB agent switched to PASSIVE
    m_cfg.m_gpio_env_cfg = gpio_env_cfg::type_id::create("m_gpio_env_cfg");
    m_cfg.m_gpio_env_cfg.m_apb_cfg = apb_agent_cfg::type_id::create("m_gpio_apb_cfg");
    m_cfg.m_gpio_env_cfg.m_apb_cfg.is_active = UVM_PASSIVE;  // ← key change
    if (!uvm_config_db #(virtual apb_if)::get(
          this, "", "apb_int_vif", m_cfg.m_gpio_env_cfg.m_apb_cfg.vif))
      `uvm_fatal("CFG", "No internal APB vif!")

    // SPI env config — APB agent switched to PASSIVE
    m_cfg.m_spi_env_cfg = spi_env_cfg::type_id::create("m_spi_env_cfg");
    m_cfg.m_spi_env_cfg.m_apb_cfg = apb_agent_cfg::type_id::create("m_spi_apb_cfg");
    m_cfg.m_spi_env_cfg.m_apb_cfg.is_active = UVM_PASSIVE;   // ← key change
    if (!uvm_config_db #(virtual apb_if)::get(
          this, "", "apb_int_vif", m_cfg.m_spi_env_cfg.m_apb_cfg.vif))
      `uvm_fatal("CFG", "No internal APB vif!")

    // ── Single set() — pss_env distributes the rest ───────
    uvm_config_db #(pss_env_cfg)::set(
      this, "m_env", "cfg", m_cfg);
    m_env = pss_env::type_id::create("m_env", this);
  endfunction

  task run_phase(uvm_phase phase);
    pss_vseq vseq;
    phase.raise_objection(this);
    vseq = pss_vseq::type_id::create("vseq");
    // init_vseq assigns AHB sequencer handle
    init_vseq(vseq);
    vseq.start(null);
    phase.drop_objection(this);
  endtask

  function void init_vseq(pss_vseq_base v);
    v.ahb_seqr = m_env.m_ahb_agent.m_seqr;
  endfunction
endclass

📋 Signal Binding for Internal Monitoring

When the APB bus is internal to the integrated DUT, the passive APB agent’s monitor needs a way to see those signals. SystemVerilog’s bind construct attaches an interface instance to a specific module in the hierarchy without modifying the module’s source code.

// ── Interface instance placed inside the DUT hierarchy ────
// The bind statement connects the interface signals
// to the internal signals of the DUT sub-module.

// Syntax:
// bind target_module interface_type instance_name(.port(signal) ...)

bind pss_dut.u_ahb_apb_bridge apb_if apb_probe (
  .PCLK   (PCLK),
  .PRESETn(PRESETn),
  .PADDR  (PADDR),
  .PSELx  (PSEL),
  .PENABLE(PENABLE),
  .PWRITE (PWRITE),
  .PWDATA (PWDATA),
  .PRDATA (PRDATA),
  .PREADY (PREADY),
  .PSLVERR(PSLVERR)
);

// ── The bound interface is set into config_db ─────────────
// in tb_top, after binding takes effect:
uvm_config_db #(virtual apb_if)::set(
  null, "uvm_test_top", "apb_int_vif",
  pss_tb_top.apb_probe);  // reference the bound instance
bind is the preferred alternative to modifying DUT source. Adding ports or wires to the DUT RTL to expose internal signals for the testbench creates a discrepancy between the RTL used for simulation and the RTL submitted for tapeout. bind attaches to the DUT non-intrusively — the RTL itself is unchanged. The bound interface instance exists only in simulation.

📋 Stimulus Reuse Across Levels

At integration level, stimulus must be driven on the AHB bus — but the block-level sequences were written for the APB bus. There are two approaches to stimulus reuse:

Approach 1 — Write new AHB sequences for integration level

Create new AHB sequences that perform the same logical operations as the APB block-level sequences. The AHB agent’s driver handles the AHB protocol; the bridge translates to APB internally. This is straightforward but requires rewriting stimulus for the new interface.

Approach 2 — Register abstraction layer (introduced in UVM-25)

The UVM Register Abstraction Layer (RAL) provides protocol-independent register read/write sequences. The same RAL sequence can target an APB adapter (for block-level tests) or an AHB adapter (for integration-level tests) without modifying the sequence body. This gives complete stimulus reuse across integration levels.

LevelDriving agentStimulus approachBlock sequences reused?
Block levelAPB agent (active)APB sequences drive registers directlyN/A — original sequences
Integration level (no RAL)AHB agent (active)New AHB sequences written for each testNo — rewritten per interface
Integration level (with RAL)AHB agent (active)Same RAL sequences, AHB adapterYes — full stimulus reuse

📋 Quick Reference

ItemKey fact
Integration env structureTop-level env wraps block-level envs as sub-components. New agents added at IL.
Making block agent passivem_apb_cfg.is_active = UVM_PASSIVE in integration test — no code changes to agent
Passive agent still doesBuilds and runs monitor. Analysis ports still active. Scoreboard and coverage still receive data.
Passive agent no longer doesDoes not build driver or sequencer. Does not drive DUT signals.
Integration env configNests sub-env configs (which in turn nest agent configs). Mirror of component hierarchy.
pss_env distributesGets pss_env_cfg, extracts sub-env cfgs, sets each into config_db for the matching sub-env
Internal signal accessUse bind target_module intf inst_name(.port(signal)...) — non-intrusive, no RTL change
bind result in config_dbReference the bound instance in tb_top: tb_top.instance_name and set as vif
Virtual sequence at ILTargets AHB sequencer (and possibly SPI). Uses init_vseq() pattern from base test.
Block scoreboard at ILStill receives from passive monitor — still checks block-level correctness at IL
New IL-level scoreboardAdded in pss_env to check cross-block scenarios (AHB transaction → both blocks respond correctly)
Stimulus reuse with RALSame RAL sequences work at any level — swap the adapter (APB→AHB) without changing tests
Scroll to Top