Part 2 · OOP for Verification · Intermediate

vif Configuration Patterns

Passing vifs down a hand-built env via constructors vs config objects, a central config class, and what UVM config_db does conceptually.

The distribution problem

Binding one vif into one driver is easy. A real environment has a hierarchy — env contains agents, agents contain driver and monitor — and the vif must travel from tb_top through every layer to the leaves that actually touch pins. The middle layers (env, agent) do not use the vif themselves; they only relay it. How you relay it determines how painful it is to add the next interface.

Pattern 1: constructor chaining

Each layer takes the vif as a constructor argument and passes it down. Explicit and impossible to forget — the code does not compile with a missing argument. The cost: every intermediate constructor signature mentions every vif, so adding one interface to a 3-level hierarchy edits every level even though only the leaf cares.

systemverilog
class agent;
  driver  drv;
  monitor mon;
  function new(virtual bus_if vif);
    drv = new(vif);          // relay to leaves
    mon = new(vif);
  endfunction
endclass

class env;
  agent agt;
  function new(virtual bus_if vif);   // env never USES vif —
    agt = new(vif);                   // it only relays it
  endfunction
endclass

module tb_top;
  bus_if bif (clk);
  initial begin
    env e = new(bif);        // one argument per vif, every level
  end
endmodule

Pattern 2: a central config class

Scale fix: gather all vifs (and knobs like agent active/passive) into one configuration class, and pass a single config handle down the hierarchy. Adding an interface now touches the config class and the leaf that uses it — the relay layers are untouched, because they pass the same one handle regardless of what is inside it. Note the reference semantics doing the work: every component holds a handle to the same config object, so tb_top fills it once and everyone sees the same vifs.

systemverilog
class tb_config;
  // ALL vifs for the whole bench live here
  virtual bus_if  bus_vif;
  virtual irq_if  irq_vif;
  // plus knobs:
  bit is_active = 1;

  function void check();
    if (bus_vif == null) $fatal(1, "tb_config: bus_vif unset");
    if (irq_vif == null) $fatal(1, "tb_config: irq_vif unset");
  endfunction
endclass

class agent;
  driver drv;
  function new(tb_config cfg);
    if (cfg.is_active) drv = new(cfg.bus_vif);  // leaf picks what it needs
  endfunction
endclass

class env;
  agent agt;
  function new(tb_config cfg);   // ONE argument, forever —
    agt = new(cfg);              // adding vifs never changes this line
  endfunction
endclass

module tb_top;
  bus_if bif (clk);
  irq_if iif (clk);
  initial begin
    tb_config cfg = new();
    cfg.bus_vif = bif;       // fill once at the top
    cfg.irq_vif = iif;
    cfg.check();             // guard BEFORE building
    begin
      env e = new(cfg);
    end
  end
endmodule
diagram
CONSTRUCTOR CHAINING            CENTRAL CONFIG OBJECT

  tb_top ─(vif1,vif2,...)─► env    tb_top ──(cfg)──► env
            │ every level             fill cfg │ one handle,
            ▼ repeats the list        once     ▼ every level
          agent ─(vif1,vif2)─►              agent ──(cfg)──►
            │                                  │
            ▼                                  ▼
      driver(vif1) monitor(vif2)      drv=new(cfg.bus_vif) ...

  add a vif: edit EVERY level      add a vif: edit cfg + leaf only

What UVM config_db does conceptually

UVM's uvm_config_db#(virtual bus_if)::set/get is the same idea with the plumbing removed: a global, hierarchy-aware lookup table . The top module set()s the vif under a string key with a scope pattern ("uvm_test_top.env.agt.*"); the leaf get()s it in its build phase. No constructor or relay layer mentions the vif at all — the table replaces the hand-carried config handle. The trade: keys are strings checked at runtime, so a typo or scope mismatch becomes a 'get failed' at run time rather than a compile error. That is why every UVM tutorial insists you check the get() return value and fatal on failure — it is the null-vif guard reborn. Interview angle: 'how would you pass a vif without UVM?' expects exactly these two hand-built patterns, and 'what does config_db buy you?' expects 'decoupled distribution, paid for with runtime string lookup'.

Key takeaways

  • Middle layers only relay vifs — choose a pattern that keeps relaying cheap.

  • Constructor chaining is compile-time safe but edits every level per added interface.

  • A central config class scales: one handle down the tree, leaves pick what they need.

  • UVM config_db is the same distribution decoupled through a string-keyed table — guard every get().

Common pitfalls

  • Adding vif arguments through five constructor layers — churn at every level for one new interface.

  • Forgetting to fill one config field — null-vif crash far from the cause; add a cfg.check() before building.

  • Copying config contents per component instead of sharing the handle — late top-level fixes never reach copies.

  • In UVM, ignoring the config_db get() return value — silent null vif, fatal at first pin access.