The two-kingdom problem — why class-based testbenches cannot directly access DUT signals, what a virtual interface is, the recommended config_db pattern for passing virtual interfaces, clocking blocks, and the most common pitfalls.
SystemVerilog has two fundamentally different worlds that cannot directly communicate with each other:
The problem: your UVM driver (dynamic class world) needs to drive DUT signals (static module world). A class object cannot have a wire or port declaration, and cannot make hierarchical references to module signals.
A virtual interface is a SystemVerilog language construct: a variable whose type is virtual <interface_name>. It acts as a handle — a reference — to an actual interface instance in the static world. It is not a copy of the interface; it is a pointer to the original.
The word “virtual” here does not mean the same as virtual in object-oriented programming (polymorphism). It is the IEEE 1800 committee’s term for “a reference to an interface instance that can be used inside a class.” Think of it as a pointer to a module-world object, usable from the class world.
// ── Actual interface (static, in module world) ──────────── interface apb_if ( input logic PCLK, input logic PRESETn ); logic [11:0] PADDR; logic PSELx, PENABLE, PWRITE, PREADY, PSLVERR; logic [31:0] PWDATA, PRDATA; logic [3:0] PSTRB; endinterface // ── Virtual interface type declaration (in class world) ─── // This is just a type — no memory allocated yet virtual apb_if vif; // handle — starts as null // ── After assignment, use it to drive signals ───────────── vif.PADDR <= 12'h004; // drives the actual interface signal vif.PSELx <= 1; @(posedge vif.PCLK); // waits on the interface clock data = vif.PRDATA; // reads from the interface
The SystemVerilog interface declaration lives in its own .sv file. It contains all the signals for one protocol bus. Clocking blocks (covered in the next section) can also be declared inside the interface to control signal sampling timing.
// File: apb_if.sv interface apb_if ( input logic PCLK, input logic PRESETn ); // APB signals logic [11:0] PADDR; logic PSELx, PENABLE, PWRITE; logic [31:0] PWDATA, PRDATA; logic [3:0] PSTRB; logic [2:0] PPROT; logic PREADY, PSLVERR; // Clocking block for driver — synchronises signal drives clocking driver_cb @(posedge PCLK); default input #1 output #1; output PADDR, PSELx, PENABLE, PWRITE, PWDATA, PSTRB, PPROT; input PRDATA, PREADY, PSLVERR; endclocking // Clocking block for monitor — defines sampling point clocking monitor_cb @(posedge PCLK); default input #1; input PADDR, PSELx, PENABLE, PWRITE, PWDATA, PSTRB, PPROT; input PRDATA, PREADY, PSLVERR; endclocking // Modport for driver (restricts which signals it can access) modport driver_mp (clocking driver_cb, input PRESETn); modport monitor_mp (clocking monitor_cb, input PRESETn); endinterface
The recommended pattern for passing virtual interfaces through the testbench has four steps. This is the only approach that cleanly separates the static and dynamic worlds while maintaining full reusability.
module tb_top; import uvm_pkg::*; `include "uvm_macros.svh" logic clk, rst_n; apb_if apb_bus(.PCLK(clk), .PRESETn(rst_n)); // static instance gpio_dut dut( .PCLK(clk), .PRESETn(rst_n), .PADDR(apb_bus.PADDR), .PWRITE(apb_bus.PWRITE) /* ... */ ); initial begin // ① Pass virtual interface BEFORE run_test() uvm_config_db #(virtual apb_if)::set( null, // null = from top-level static context "uvm_test_top", // scope: accessible to test and all below "apb_vif", // key name — test retrieves with this name apb_bus // the actual interface instance handle ); rst_n = 0; #20ns; rst_n = 1; run_test(); end endmodule
function void build_phase(uvm_phase phase); super.build_phase(phase); m_env_cfg = gpio_env_cfg::type_id::create("m_env_cfg"); m_env_cfg.m_agent_cfg = gpio_agent_cfg::type_id::create("m_agent_cfg"); // ② Retrieve vif from config_db and assign into agent config if (!uvm_config_db #(virtual apb_if)::get( this, // this component's context "", // no path suffix — search from this component up "apb_vif", // same key as the set() call m_env_cfg.m_agent_cfg.vif)) `uvm_fatal("TEST", "No virtual interface in config_db!") endfunction
function void build_phase(uvm_phase phase); super.build_phase(phase); if (!uvm_config_db #(gpio_agent_cfg)::get( this, "", "cfg", m_cfg)) `uvm_fatal("AGT", "No agent config!") m_mon = gpio_monitor::type_id::create("m_mon", this); if (m_cfg.is_active == UVM_ACTIVE) begin m_seqr = gpio_sequencer::type_id::create("m_seqr", this); m_drv = gpio_driver::type_id::create("m_drv", this); end // ③ Push vif (from config object) down to driver and monitor uvm_config_db #(virtual apb_if)::set( this, "m_mon", "vif", m_cfg.vif); if (m_cfg.is_active == UVM_ACTIVE) uvm_config_db #(virtual apb_if)::set( this, "m_drv", "vif", m_cfg.vif); endfunction
class gpio_driver extends uvm_driver #(gpio_seq_item); `uvm_component_utils(gpio_driver) virtual apb_if vif; // local handle — null until build_phase function void build_phase(uvm_phase phase); super.build_phase(phase); // ④ Get vif from config_db — fatal if not found if (!uvm_config_db #(virtual apb_if)::get( this, "", "vif", vif)) `uvm_fatal("DRV", "No virtual interface!") endfunction task run_phase(uvm_phase phase); // Now safe to use vif — handle is assigned @(posedge vif.PCLK); vif.PSELx <= 1; // ... endtask endclass
A clocking block declared inside a SystemVerilog interface defines the timing relationship between the testbench and the DUT clock. It specifies when signals are sampled (input skew) and when they are driven (output skew) relative to the clock edge. This eliminates race conditions where the testbench drives signals at exactly the same edge the DUT samples them.
// Inside apb_if — clocking block for the driver clocking driver_cb @(posedge PCLK); default input #1step // sample 1 step before posedge output #1; // drive 1 time unit after posedge output PADDR, PSELx, PENABLE, PWRITE, PWDATA, PSTRB; input PRDATA, PREADY, PSLVERR; endclocking // In the driver — access through clocking block task drive_transfer(gpio_seq_item item); @(vif.driver_cb); // wait for clock edge vif.driver_cb.PADDR <= item.addr; // drives 1ns after posedge vif.driver_cb.PSELx <= 1; vif.driver_cb.PENABLE <= 0; @(vif.driver_cb); // next cycle vif.driver_cb.PENABLE <= 1; endtask
@(posedge clk) is a race — the DUT samples at the same edge you drive. With a clocking block using output #1, your drives happen 1ns after the edge, giving the DUT a clean setup window. This is especially important for fast DUTs running at hundreds of MHz.
| Method | Signal timing | Race safe? | Recommended for |
|---|---|---|---|
Direct assignment vif.signal <= val | Driven at NBA phase of current timestep | Risk of race at posedge | Simple testbenches, slow DUTs |
Clocking block vif.cb.signal <= val | Driven #output_skew after clock edge | Race-free | Production testbenches, all designs |
Clocking block input vif.cb.signal | Sampled #input_skew before clock edge | Stable value guaranteed | Monitor signal sampling |
A DUT with multiple protocol interfaces requires one SV interface per bus, one agent per interface, and one virtual interface handle per interface in the relevant config object. Each virtual interface is placed into config_db with a unique key name.
// tb_top — two interfaces for a DUT with APB + AHB apb_if apb_bus(.PCLK(clk), .PRESETn(rst_n)); ahb_if ahb_bus(.HCLK(clk), .HRESETn(rst_n)); initial begin uvm_config_db #(virtual apb_if)::set( null, "uvm_test_top", "apb_vif", apb_bus); uvm_config_db #(virtual ahb_if)::set( null, "uvm_test_top", "ahb_vif", ahb_bus); run_test(); end // test — retrieve both, assign into respective agent configs if (!uvm_config_db #(virtual apb_if)::get( this, "", "apb_vif", m_env_cfg.m_apb_cfg.vif)) `uvm_fatal("TEST", "No APB vif!") if (!uvm_config_db #(virtual ahb_if)::get( this, "", "ahb_vif", m_env_cfg.m_ahb_cfg.vif)) `uvm_fatal("TEST", "No AHB vif!")
| Pitfall | Symptom | Fix |
|---|---|---|
| config_db::set() after run_test() | get() returns 0. Virtual interface is null in driver. Simulation crashes on first signal access. | All set() calls must happen in the initial block before run_test(). |
| Wrong path string in set() | get() returns 0. The key exists in config_db but the path does not match any component that calls get(). | Use "uvm_test_top" (not "*" or "") in the tb_top set() for global visibility. Debug with uvm_config_db::dump(). |
| Wrong key name in get() | get() returns 0. The name string in get() must match the name in set() exactly — case-sensitive. | Use constants or package parameters for key strings to avoid typos across multiple files. |
| Using vif before build_phase assigns it | Null handle dereference. Runtime fatal. Typically in a constructor or static initialiser. | Only access vif in run_phase or later. build_phase must complete before run_phase starts. |
| Race condition without clocking block | Intermittent failures at specific frequencies. DUT samples signals at the same edge they are driven. | Use clocking blocks with output skew in the interface. Drive through vif.cb.signal <= val. |
| Sharing one virtual interface handle across multiple drivers | Signal contention. Two drivers overwriting each other’s values. | Each driver must have its own virtual interface instance. Multi-master designs need separate interface instances or interface arrays. |
| Virtual interface is parameterised | Type mismatch in config_db. virtual my_if #(32) and virtual my_if #(64) are different types. | The type parameter in set() and get() must match exactly. Use a fixed parameter in the interface or specialised config objects per parameter value. |
| Item | Key fact |
|---|---|
| Two kingdoms | Static (modules/interfaces) cannot be directly accessed by dynamic (classes). Virtual interface bridges the two. |
| Virtual interface definition | A class-compatible variable holding a reference/handle to a static interface instance |
| Declaration | virtual apb_if vif; — starts as null, must be assigned before use |
| tb_top set() syntax | config_db #(virtual apb_if)::set(null, "uvm_test_top", "key", instance) |
| Component get() syntax | config_db #(virtual apb_if)::get(this, "", "key", vif) |
| set() must happen when | In initial block of tb_top, before run_test() |
| Recommended vif storage | Inside agent config object — not directly in driver/monitor’s config_db path |
| Why config object not direct | Keeps driver/monitor reusable — they only need their config object, not config_db key names |
| Clocking block purpose | Defines sampling (input skew) and drive timing (output skew) relative to clock — eliminates races |
| Drive with clocking block | vif.driver_cb.SIGNAL <= value — driven at specified skew after clock edge |
| Sample with clocking block | value = vif.monitor_cb.SIGNAL — sampled at specified skew before clock edge |
| Multiple interfaces | One interface instance per bus, one virtual interface handle per config object, unique key per bus |
| Debug get() failure | uvm_config_db::dump() prints all entries — compare path and key with what get() expects |
| Null handle error | Occurs when vif is used before build_phase assigns it, or when get() silently returned 0 |