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.
// 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
endclassThe 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.
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
endclassPORTABILITY 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 LEFT→RIGHT only:
UVM adapters may use plain classes; plain classes never import uvm_pkgWhen 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.