Part 6 · Testbench Architecture · Intermediate
Migrating a TB to UVM
A staged migration plan — wrap the transaction, componentize, adopt phases, factory and config db, then sequences — without stopping test runs.
Migrate in stages, never in one jump
The fatal migration mistake is the big-bang rewrite: stop the world, rewrite the TB in UVM, and try to bring everything back up at once. Weeks pass with zero working tests , and every bug could be in the rewrite or the RTL. The alternative is a staged migration where the TB runs its full test list after every stage — each stage is a refactor with a regression gate, not a leap of faith.
STAGED MIGRATION LADDER (regression green after EVERY rung)
stage 0 hand-built class TB, tests passing ← start: a known-good base
│
stage 1 TXN: txn class extends uvm_sequence_item
│ (field macros or do_copy/do_compare/convert2string)
stage 2 COMPONENTS: drv/mon/scb extend uvm_component family;
│ still wired by hand, still driven by your env
stage 3 PHASES: new()/connect()/run() code moves into
│ build_phase / connect_phase / run_phase; objections
│ replace the done-counter
stage 4 FACTORY + CONFIG DB: type_id::create everywhere,
│ virtual interfaces via uvm_config_db, +UVM_TESTNAME
stage 5 SEQUENCES: generator logic becomes sequences on a
│ sequencer; mailbox handoff becomes seq_item_port
▼
full UVM env — and tests ran at every stageWhat to refactor first — and why this order
The order is dependency-driven
Transaction first — every component touches it, and extending uvm_sequence_item changes no behavior; lowest risk, unblocks everything else.
Components second — extending uvm_component buys reporting and hierarchy while your env still controls wiring; behavior unchanged.
Phases third — this is the first behavioral change (who calls what, when); objections replace your end-of-test counter here.
Factory and config db fourth — mechanical substitution of new() and constructor plumbing, enabling +UVM_TESTNAME test selection.
Sequences last — the largest shape change (push mailbox to pull handshake); do it once everything around it is stable.
// Stage 1: the transaction — smallest possible first step
class bus_txn extends uvm_sequence_item;
`uvm_object_utils(bus_txn)
rand bit write;
rand bit [31:0] addr, data;
function new(string name = "bus_txn");
super.new(name);
endfunction
// your hand-written compare/print migrate nearly verbatim
virtual function bit do_compare(uvm_object rhs, uvm_comparer comparer);
bus_txn t;
if (!$cast(t, rhs)) return 0;
return (write == t.write) && (addr == t.addr) && (data == t.data);
endfunction
endclass
// Stage 3: run() body moves into run_phase, done-counter → objection
task my_driver::run_phase(uvm_phase phase);
forever begin
// stage 3-4: still mailbox; stage 5 swaps to seq_item_port
in_mb.get(t);
drive_one(t);
end
endtaskKeeping tests running, and the classic mistakes
The discipline that makes staged migration work: after every stage, run the full existing test list and require the same pass results before starting the next stage. Where a stage is long (sequences, typically), keep the old path alive briefly — a driver can accept a mailbox in stages 2–4 and switch to seq_item_port in stage 5 behind a config flag — so a mid-stage RTL bug can still be chased with a working TB.
Common migration mistakes
Big-bang rewrite — weeks of no working tests; every failure ambiguous between TB rewrite and RTL.
Migrating stimulus (sequences) first — the hardest change taken while everything else is also moving.
Dropping the scoreboard "temporarily" during migration — RTL bugs land unnoticed in exactly that window.
Skipping the regression gate between stages — errors compound across stages and unwind is impossible.
Rewriting working checker logic "the UVM way" for purity — do_compare can wrap your existing compare verbatim.
Interview angle
"How would you move your TB to UVM?" — stage ladder: transaction, components, phases, factory/config db, sequences; regression green after each rung.
"What would you migrate first and why?" — the transaction: everything depends on it and the change is behavior-neutral.
"Biggest migration risk?" — the window with no working tests; staged refactor with gates exists to keep that window at zero.
Key takeaways
Migrate up a stage ladder — transaction, components, phases, factory/config db, sequences.
Run the full test list after every stage; never start a stage from a red base.
Order is dependency-driven: behavior-neutral changes first, the pull-handshake shape change last.
Keep the old data path switchable until the new one is proven.
Common pitfalls
Big-bang rewrite — the no-working-tests window swallows the schedule.
Sequences first — maximum shape change with no stable base under it.
No scoreboard during migration — RTL regressions slip in silently.
Treating migration as a purity exercise — wrap proven logic, do not rewrite it.