Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
    • Testbench Anatomy & Layering
    • Stimulus: Generators, Transactions & Drivers
    • Monitors, Scoreboards & Reference Models
    • Self-Checking Tests & Regression
    • From Hand-Built TB to UVM
      • Concept Mapping: TB to UVM
      • What UVM Automates (and What It Costs)
      • Migrating a TB to UVM
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹From Hand-Built TB to UVM
  1. Topics
  2. SystemVerilog
  3. Testbench Architecture in SystemVerilog
  4. From Hand-Built TB to UVM
  5. Migrating a TB to UVM

Part 6 · Testbench Architecture · Intermediate

Migrating a TB to UVM

Climb the migration ladder from plain classes to sequences, keeping every test green at each rung.

On this page

    Climb the ladder, keep tests green

    The fatal migration mistake is the big-bang rewrite: weeks pass with zero working tests, and every failure is ambiguous between the rewrite and the RTL. Migrate as a staged refactor instead, with the full test list green after every stage.

    Diagram
    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 stage

    Never start a stage from a red base. Where a stage is long, typically sequences, keep the old data path alive behind a config flag so a mid-stage RTL bug can still be chased with a working testbench.

    End every stage cleanly in both worlds. The hand-built checklist is: signal stimulus exhausted, drain with a watchdog timeout, sweep leftovers in the scoreboard, then print one verdict banner.

    1. Hand-built: generator signals exhausted, then drain until the driver is idle and the expected queue empties, with a watchdog so a hang is loud.

    2. Hand-built: scoreboard final sweep treats leftover expected or orphan actual transactions as defects, then one PASS/FAIL banner plus exit code closes the run.

    3. UVM: components raise objections while work is pending, and run_phase ends when all are dropped, with drain time absorbing tail traffic.

    4. UVM: check_phase and report_phase evaluate leftovers after run_phase, and a watchdog calling uvm_fatal("ID", "msg") on timeout is still required.

    Both endings are votes-outstanding schemes: a done counter is a single vote, while objections let every component vote independently.

    Why this order

    The ladder order follows dependencies, with behavior-neutral changes first and the shape change last.

    1. Transaction first: everything touches it, and extending uvm_sequence_item changes no behavior.

    2. Components second: uvm_component buys reporting and hierarchy while the existing environment still wires by hand.

    3. Phases third: the first behavioral change, moving construction and run control into phase callbacks, with objections replacing the done counter.

    4. Factory and config db fourth: mechanical substitution of new() and constructor plumbing, enabling +UVM_TESTNAME test selection.

    5. Sequences last: the largest shape change, taken once everything around it is stable.

    Stage 1 in both worlds

    Stage 1 wraps the transaction with field macros, which generate the copy, compare, and print routines. The UVM course owns the sequence item detail.

    The UVM transaction below is not run in this course's checks because it needs the UVM library, which the course's simulators here do not have.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // not-run: needs the UVM library, which is not installed for the course runner
    class bus_txn extends uvm_sequence_item;
      `uvm_object_utils_begin(bus_txn)
        `uvm_field_int(write, UVM_ALL_ON)
        `uvm_field_int(addr, UVM_ALL_ON)
        `uvm_field_int(data, UVM_ALL_ON)
      `uvm_object_utils_end
      rand bit        write;
      rand bit [31:0] addr, data;
      function new(string name = "bus_txn");
        super.new(name);
      endfunction
    endclass

    The plain check below runs under Icarus and checks the same compare on a struct. Transactions built by one path must equal golden values written out separately, both ways, and an altered transaction must compare unequal.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    module tb_bus_txn;
      typedef struct packed {
        bit        write;
        bit [31:0] addr;
        bit [31:0] data;
      } bus_txn_t;
    
      function automatic bit txn_equal(bus_txn_t a, bus_txn_t b);
        return (a.write == b.write) && (a.addr == b.addr) && (a.data == b.data);
      endfunction
    
      // The "actual" side: transactions built by a separate path
      function automatic bus_txn_t make_txn(int i);
        bus_txn_t t;
        t.write = i[0];
        t.addr  = 32'h1000 + (i * 4);
        t.data  = 32'hA5A50000 + i;
        return t;
      endfunction
    
      bus_txn_t golden[4];
      bus_txn_t got[4];
      bus_txn_t odd;
      int bad;
    
      initial begin
        bad = 0;
        // golden values, written out by hand: {write, addr, data}
        golden[0] = {1'b0, 32'h0000_1000, 32'hA5A5_0000};
        golden[1] = {1'b1, 32'h0000_1004, 32'hA5A5_0001};
        golden[2] = {1'b0, 32'h0000_1008, 32'hA5A5_0002};
        golden[3] = {1'b1, 32'h0000_100C, 32'hA5A5_0003};
        for (int i = 0; i < 4; i++) begin
          got[i] = make_txn(i);
          if (!txn_equal(golden[i], got[i])) bad = bad + 1;
          if (!txn_equal(got[i], golden[i])) bad = bad + 1;
        end
        // a compare that cannot say "different" proves nothing
        odd = golden[2];
        odd.data[0] = ~odd.data[0];
        if (txn_equal(odd, golden[2])) bad = bad + 1;
        if (bad == 0) $display("PASS: 4 transactions matched golden values both ways, 1 altered one compared unequal");
        else $fatal(1, "FAIL: bad=%0d of 9 compares", bad);
        $finish;
      end
    endmodule

    Keep plain models inside UVM

    Only components needing phasing, factory overrides, or config db access should extend uvm_component. Pure computation such as reference models and checkers stays plain, which keeps it reusable outside UVM.

    The harness below runs under Icarus and checks the same predict body that the scoreboard adapter calls.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    module tb_alu_ref;
      // Bit 32 says whether the opcode is known. An X sentinel would not
      // work: a 2-state type (or a 2-state simulator such as Verilator)
      // turns X into 0, and 0 would then pass as a real prediction.
      function automatic bit [32:0] alu_predict(bit [3:0] op, bit [31:0] a, bit [31:0] b);
        case (op)
          4'h0: return {1'b1, a + b};
          4'h1: return {1'b1, a - b};
          4'h2: return {1'b1, a & b};
          default: return {1'b0, 32'h0};   // unknown opcode: no prediction
        endcase
      endfunction
    
      int bad;
      bit [32:0] got;
      bit [31:0] want [0:2];
    
      initial begin
        bad = 0;
        want[0] = 32'd13; want[1] = 32'd7; want[2] = 32'd2;   // 10 op 3, by hand
        for (int i = 0; i < 3; i++) begin
          got = alu_predict(i[3:0], 32'd10, 32'd3);
          if (!got[32] || got[31:0] !== want[i]) bad = bad + 1;
        end
        // the unknown opcode must be reported, never read as a value
        got = alu_predict(4'h3, 32'd10, 32'd3);
        if (got[32]) bad = bad + 1;
        if (bad == 0) $display("PASS: reference model matched 3 opcodes and reported the unknown one");
        else $fatal(1, "FAIL: bad=%0d of 4 checks", bad);
        $finish;
      end
    endmodule

    The UVM scoreboard below is a thin adapter around that plain brain. It is not run in this course's checks because it needs the UVM library, which the course's simulators here do not have.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // not-run: needs the UVM library, which is not installed for the course runner
    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 [32:0] exp = model.predict(t.op, t.a, t.b);   // {known, value}
        if (!exp[32])
          `uvm_error("SCB", $sformatf("op=%0h has no prediction", t.op))
        else if (exp[31:0] !== t.result)
          `uvm_error("SCB", $sformatf("op=%0h exp=%0h act=%0h", t.op, exp[31:0], t.result))
      endfunction
    endclass

    A proven legacy bus model is wrapped the same way: a thin driver translates sequencer items into BFM calls. Reads return through a response object so the sequence sees the data. The response must carry the request's ids (set_id_info), or the sequencer stops with a fatal error, and the sequence must call get_response() for each read so the response queue does not fill up.

    The wrapper below is not run in this course's checks because it needs the UVM library, which the course's simulators here do not have.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // not-run: needs the UVM library, which is not installed for the course runner
    class legacy_bfm_driver extends uvm_driver #(bus_txn);
      `uvm_component_utils(legacy_bfm_driver)
      bus_bfm bfm;   // legacy plain-class model, handed in by the env
      task run_phase(uvm_phase phase);
        bus_txn rsp;
        forever begin
          seq_item_port.get_next_item(req);
          if (req.write) begin
            bfm.write(req.addr, req.data);
            seq_item_port.item_done();
          end else begin
            rsp = bus_txn::type_id::create("rsp");
            bfm.read(req.addr, rsp.data);   // read value lands in rsp
            rsp.addr = req.addr;
            rsp.set_id_info(req);           // route the response to its sequence
            seq_item_port.item_done(rsp);   // sequence reads it with get_response()
          end
        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 LEFT→RIGHT only:
      UVM adapters may use plain classes; plain classes never import uvm_pkg

    Common mistakes

    Common pitfall

    Three migration errors recur: the big-bang rewrite with its weeks of no working tests, sequences first with no stable base under them, and a scoreboard dropped “temporarily” while RTL bugs slip in. A fourth is rewriting proven checker logic for purity instead of wrapping it.

    Interview

    Likely questions: “How would you move your testbench to UVM?” — the stage ladder with a green regression after each rung. “Biggest migration risk?” — the window with no working tests, which staged refactoring keeps at zero. “How do you end cleanly?” — signal done, drain with a watchdog, sweep leftovers, one verdict.

    Check yourself Why does the transaction migrate first, and what breaks if sequences go first?

    The transaction touches everything and the change is behavior-neutral, so it unblocks later stages at near-zero risk. Sequences first lands the largest shape change with no stable base under it.

    Check yourself The scoreboard is disabled “temporarily” mid-migration. What is the risk, and what is the rule?

    RTL bugs land unnoticed in exactly that window. The rule is a green regression after every rung, with the scoreboard live throughout.

    Check yourself A reference model imports uvm_pkg for one reporting call. Why does that matter?

    It is no longer portable: it now compiles only inside UVM. Report through return values or callbacks instead, and keep the model free of UVM.

    Related topics

    • End-of-test objections

    • Scoreboard architecture

    Previous

    185 / 259

    Next: Advanced Language & Integration

    Up next

    Up nextAdvanced Language & Integration

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Concept Mapping: TB to UVM