Part 6 · Testbench Architecture · Intermediate

Interop: Plain Classes Inside UVM

Non-UVM reference models and checkers in UVM envs, wrapping legacy BFMs, what not to UVM-ify, and portability boundaries.

Not everything needs to be a uvm_component

A UVM environment is plain SystemVerilog underneath — any class can live inside it. Components that need phasing, hierarchy, factory overrides, or config db should extend uvm_component. Pure computation — reference models, protocol checkers, scenario generators-of-values, utility libraries — usually needs none of that, and UVM-ifying it costs portability: a plain class runs in a directed TB, a different team's UVM env, or a unit test; a uvm_component runs only inside a UVM phase tree.

systemverilog
// Plain reference model — no UVM anywhere in it
class alu_ref_model;
  function bit [31:0] predict(bit [3:0] op, bit [31:0] a, b);
    case (op)
      4'h0: return a + b;
      4'h1: return a - b;
      4'h2: return a & b;
      default: return 'x;
    endcase
  endfunction
endclass

// UVM scoreboard USING the plain model — adapter at the boundary
class alu_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(alu_scoreboard)
  uvm_analysis_imp #(alu_txn, alu_scoreboard) imp;
  alu_ref_model model;            // plain class, just new() it

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    imp   = new("imp", this);
    model = new();                // no factory, no config db needed
  endfunction

  function void write(alu_txn t);
    bit [31:0] exp = model.predict(t.op, t.a, t.b);
    if (exp !== t.result)
      `uvm_error("SCB", $sformatf("op=%0h exp=%0h act=%0h",
                                   t.op, exp, t.result))
  endfunction
endclass

The pattern: the UVM scoreboard is a thin adapter — it receives transactions through UVM plumbing and reports through UVM, but the actual checking brain is a plain class that knows nothing about UVM. That same model is reusable in a hand-built TB or a C-DPI co-simulation harness unchanged.


Wrapping a legacy BFM

Teams often own a proven, pre-UVM bus-functional model — a class (or even a module with tasks) with an API like bfm.write(addr, data). Do not rewrite it as a uvm_driver; wrap it : a thin uvm_driver pulls items from the sequencer and translates each into BFM API calls. The proven protocol-timing code keeps working, and sequences gain control of it immediately.

systemverilog
class legacy_bfm_driver extends uvm_driver #(bus_txn);
  `uvm_component_utils(legacy_bfm_driver)
  bus_bfm bfm;   // legacy plain-class BFM, handed in via config or env

  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      if (req.write) bfm.write(req.addr, req.data);   // translate
      else           bfm.read (req.addr, req.data);
      seq_item_port.item_done();
    end
  endtask
endclass
diagram
PORTABILITY BOUNDARY

  ┌── UVM WORLD ───────────────────────┐ ┌── PLAIN WORLD ───────────┐
  │ sequences  sequencer               │ │                          │
  │     │                              │ │  alu_ref_model (compute) │
  │     ▼                              │ │  protocol_checker (rules)│
  │ thin uvm_driver  ── api calls ──────►  bus_bfm (pin wiggling)  │
  │ uvm_scoreboard   ── predict() ──────►  packet_lib (utilities)  │
  │  (adapters: UVM plumbing only)     │ │  (no uvm_* anywhere)     │
  └────────────────────────────────────┘ └──────────────────────────┘
  keep dependencies pointing LEFTRIGHT only:
  UVM adapters may use plain classes; plain classes never import uvm_pkg

When NOT to UVM-ify, and keeping the boundary clean

  • Pure-function code (predictors, CRC/parity calculators, packet packers) — no phases or hierarchy needed; keep plain.

  • Utility libraries shared with non-UVM TBs or formal/emulation flows — a uvm_pkg import poisons every consumer.

  • A proven legacy BFM — wrap behind a thin driver; rewriting trades known-good timing code for new bugs.

  • Classes needing factory overrides per test, config db access, or phase callbacks — these genuinely earn uvm_component.

The discipline that keeps this working is the dependency direction: UVM adapters may hold and call plain classes, but plain classes must never reference UVM types — no uvm_pkg import, no `uvm_info inside them (report through a callback or return status instead). One stray import and the "portable" model only compiles inside UVM.

Interview angle

  • "Must everything in a UVM env extend uvm_component?" — no; only things needing phases/factory/config db; computation stays plain.

  • "You inherit a pre-UVM BFM — rewrite or reuse?" — wrap behind a thin uvm_driver translator; keep the proven timing code.

  • "How do you keep a reference model reusable across projects?" — zero UVM dependencies inside; UVM touches it only through adapters.

Key takeaways

  • Extend uvm_component only for phasing/factory/config-db needs; pure computation stays a plain class.

  • Adapter pattern: UVM plumbing on the outside, plain checking/modeling brains on the inside.

  • Wrap proven legacy BFMs behind a thin uvm_driver instead of rewriting them.

  • Enforce one-way dependencies — plain classes never import uvm_pkg.

Common pitfalls

  • UVM-ifying a utility library — every non-UVM consumer now needs uvm_pkg to compile.

  • Rewriting a known-good BFM as a uvm_driver — new bugs in code that had none.

  • A `uvm_info inside the "plain" reference model — silently breaks its portability.

  • Fat adapters that grow checking logic — the brain belongs in the plain class, the adapter stays thin.