UVM block-level to integration-level reuse means taking the environment built to verify one block and instantiating it, unchanged, inside a larger environment that verifies the subsystem or chip containing that block. The block's agents usually switch from active to passive, because the block's interfaces are now driven by other hardware. A new top-level agent drives the subsystem's external interface, and configuration objects nest so each block environment still receives its own settings.
This is called vertical reuse, and it is one of the main reasons UVM environments are built the way they are. The scoreboards, monitors, coverage and register models written at block level keep checking the block when it is surrounded by the rest of the design, often catching integration bugs that no block-level test could see.
8 min read | Intermediate
What You'll Learn
- Explain what changes, and what doesn't, when a block environment is reused at integration level
- Make an agent active or passive through its configuration object
- Nest configuration objects so each sub-environment receives its own settings
- Reuse block-level register models and sequences in a subsystem
What Changes at Integration Level
At block level, spi_env contains an active APB agent that drives the SPI block's register interface, and a scoreboard that checks it. At integration level, the SPI block sits behind an AHB-to-APB bridge in a peripheral subsystem, next to a GPIO block. The APB bus is now internal: the bridge drives it, not the testbench.
So the integration environment pss_env:
- instantiates spi_env and gpio_env as sub-environments, unchanged;
- switches their APB agents to passive, so they only monitor the internal bus;
- adds an active AHB agent to drive the subsystem's external interface;
- keeps the block scoreboards, which still receive transactions from the passive monitors.
A passive agent builds only its monitor, and coverage if present. Its driver and sequencer are never created, so it cannot interfere with traffic produced by the real hardware.
Agent Mode from the Configuration Object
Reuse depends on every agent taking its mode from configuration rather than deciding for itself. A common pattern keeps the mode in the agent's configuration object, together with its virtual interface:
class apb_agent_cfg extends uvm_object;
`uvm_object_utils(apb_agent_cfg)
uvm_active_passive_enum active = UVM_ACTIVE;
virtual apb_if vif;
bit has_coverage = 1;
// Rest of the code (constructor)
endclass
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_agent_cfg m_cfg;
// Rest of the code (constructor, handles)
virtual function uvm_active_passive_enum get_is_active();
return m_cfg.active; // the config object decides
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(apb_agent_cfg)::get(this, "", "apb_agent_cfg", m_cfg))
`uvm_fatal("CFG", "apb_agent_cfg not found")
m_mon = apb_monitor::type_id::create("m_mon", this);
if (get_is_active() == UVM_ACTIVE) begin
m_seqr = apb_sequencer::type_id::create("m_seqr", this);
m_drv = apb_driver::type_id::create("m_drv", this);
end
endfunction
endclass
get_is_active() is a virtual method of uvm_agent, and overriding it to read the configuration object is a documented use. Its default implementation returns the agent's is_active setting, which can also be set through the configuration database as an int, as described in UVM Agent. Pick one approach per agent and use it consistently.
Nesting Configuration Objects
Each block environment already expects its own configuration object. The integration environment's configuration simply contains those objects, plus the settings for whatever it adds:
class spi_env_cfg extends uvm_object;
`uvm_object_utils(spi_env_cfg)
apb_agent_cfg m_apb_cfg;
bit has_scoreboard = 1;
// Rest of the code (constructor)
endclass
class pss_env_cfg extends uvm_object;
`uvm_object_utils(pss_env_cfg)
spi_env_cfg m_spi_cfg;
gpio_env_cfg m_gpio_cfg;
ahb_agent_cfg m_ahb_cfg;
// Rest of the code (constructor)
endclass
The integration environment passes each part down to the sub-environment that expects it, using exactly the field names the block environments already use:
class pss_env extends uvm_env;
`uvm_component_utils(pss_env)
pss_env_cfg m_cfg;
spi_env m_spi_env;
gpio_env m_gpio_env;
ahb_agent m_ahb_agent;
// Rest of the code (constructor)
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(pss_env_cfg)::get(this, "", "pss_env_cfg", m_cfg))
`uvm_fatal("CFG", "pss_env_cfg not found")
// Hand each block its own configuration, under the names it already uses
uvm_config_db #(spi_env_cfg)::set(this, "m_spi_env*", "spi_env_cfg", m_cfg.m_spi_cfg);
uvm_config_db #(gpio_env_cfg)::set(this, "m_gpio_env*", "gpio_env_cfg", m_cfg.m_gpio_cfg);
uvm_config_db #(ahb_agent_cfg)::set(this, "m_ahb_agent*", "ahb_agent_cfg", m_cfg.m_ahb_cfg);
m_spi_env = spi_env::type_id::create("m_spi_env", this);
m_gpio_env = gpio_env::type_id::create("m_gpio_env", this);
m_ahb_agent = ahb_agent::type_id::create("m_ahb_agent", this);
endfunction
endclass
The test builds the whole configuration tree and sets the mode for this level of integration in one place:
// In the integration test's build_phase
m_cfg.m_spi_cfg.m_apb_cfg.active = UVM_PASSIVE; // APB is driven by the bridge now
m_cfg.m_gpio_cfg.m_apb_cfg.active = UVM_PASSIVE;
m_cfg.m_ahb_cfg.active = UVM_ACTIVE; // the testbench drives AHB
uvm_config_db #(pss_env_cfg)::set(this, "m_env", "pss_env_cfg", m_cfg);
Nothing inside spi_env or gpio_env changes. That is the test of a reusable environment: it works at a new level of integration with only configuration changes.
Quick Check
Q: At integration level, a test still starts the block-level SPI register sequence on m_spi_env.m_apb_agent.m_seqr. What happens?
Show Answer
The sequencer doesn't exist, because the passive APB agent never created it, so the sequence is started on a null handle and fails. At integration level the stimulus has to go in through the AHB agent, for example through the register model as described below.
Reusing Registers and Stimulus
Block-level sequences that drive the APB agent directly cannot run at integration level, because nothing drives APB from the testbench any more. Sequences written against the register model can, because the model hides which bus is used.
The subsystem register model contains the block register models as sub-blocks. Each block's address map is added to the subsystem's map at the block's base address, and the subsystem map is connected to the AHB agent:
// In the subsystem register block's build()
default_map.add_submap(spi_rm.default_map, 'h0000);
default_map.add_submap(gpio_rm.default_map, 'h1000);
A block-level register sequence such as "configure the SPI clock divider" writes to spi_rm.ctrl. At integration level the same write goes out as an AHB transfer at the right offset, through the bridge, to the same register. The sequence code doesn't change. Register access is covered in UVM Register Access Methods.
Block scoreboards keep working too, because they receive transactions from the passive APB monitors on the internal bus. Some checks may need adjusting. A block scoreboard that predicted register reads from its own write history is still correct, but one that assumed it saw every access may now need to allow for traffic it didn't generate.
Designing for Reuse from the Start
- Every agent takes its mode, interface and options from a configuration object, never from hard-coded values.
- Every environment gets its configuration from
uvm_config_dbwith a fixed field name, and passes parts of it down. - Scoreboards and coverage depend only on monitors, so they work when the agent is passive.
- Stimulus that should survive integration is written against the register model or as virtual sequences, not against a specific bus agent.
The general guidelines for writing such components are collected in Guide - Developing Reusable Verification Components.
Common Mistakes
Mistake #1: Hard-coding the agent mode
An agent that always builds its driver cannot be made passive without editing it, which breaks reuse. Take the mode from configuration.
Mistake #2: Scoreboards that read the driver
A scoreboard connected to the driver, or to a sequence, stops working when the agent is passive. Feed scoreboards and coverage only from monitors.
Mistake #3: Renaming configuration fields at integration level
The block environment looks for its configuration under a specific field name. The integration environment must set it under that same name, or the block's get() fails.
Mistake #4: Bus-specific block stimulus
Sequences that drive the block's own bus agent cannot run once that agent is passive. Write reusable stimulus through the register model.
Summary
In this article, you learned:
- Integration-level environments instantiate block environments unchanged as sub-environments
- Block agents on internal interfaces become passive; a new active agent drives the external interface
- Overriding
get_is_active()lets an agent take its mode from its configuration object - Configuration objects nest, and each part is passed down under the name the block already uses
- Register models and register-level sequences carry block stimulus up to integration level