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.
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.
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:
is_active flag in the config object changes.bind constructs, allowing passive agents to observe them.| What changes at integration level | What 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-components | Block env class code — build, connect, analysis connections unchanged |
| New integration-level agent (AHB) added | Block-level sequences — can still be used at integration level via register abstraction |
| Signal binding for internal bus observation | Monitor classes — still sample the same signals via the same virtual interface |
| Integration-level virtual sequence coordinates across all agents | Block-level covergroups — still sample from the same monitor analysis ports |
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.
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.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
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
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
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
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
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:
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.
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.
| Level | Driving agent | Stimulus approach | Block sequences reused? |
|---|---|---|---|
| Block level | APB agent (active) | APB sequences drive registers directly | N/A — original sequences |
| Integration level (no RAL) | AHB agent (active) | New AHB sequences written for each test | No — rewritten per interface |
| Integration level (with RAL) | AHB agent (active) | Same RAL sequences, AHB adapter | Yes — full stimulus reuse |
| Item | Key fact |
|---|---|
| Integration env structure | Top-level env wraps block-level envs as sub-components. New agents added at IL. |
| Making block agent passive | m_apb_cfg.is_active = UVM_PASSIVE in integration test — no code changes to agent |
| Passive agent still does | Builds and runs monitor. Analysis ports still active. Scoreboard and coverage still receive data. |
| Passive agent no longer does | Does not build driver or sequencer. Does not drive DUT signals. |
| Integration env config | Nests sub-env configs (which in turn nest agent configs). Mirror of component hierarchy. |
| pss_env distributes | Gets pss_env_cfg, extracts sub-env cfgs, sets each into config_db for the matching sub-env |
| Internal signal access | Use bind target_module intf inst_name(.port(signal)...) — non-intrusive, no RTL change |
| bind result in config_db | Reference the bound instance in tb_top: tb_top.instance_name and set as vif |
| Virtual sequence at IL | Targets AHB sequencer (and possibly SPI). Uses init_vseq() pattern from base test. |
| Block scoreboard at IL | Still receives from passive monitor — still checks block-level correctness at IL |
| New IL-level scoreboard | Added in pss_env to check cross-block scenarios (AHB transaction → both blocks respond correctly) |
| Stimulus reuse with RAL | Same RAL sequences work at any level — swap the adapter (APB→AHB) without changing tests |