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
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
    • OOP Patterns for Testbenches
      • The Transaction Base Class
      • The Factory Pattern from Scratch
      • Singletons and Config Objects
      • Callbacks and Hooks
      • Building a Component Hierarchy by Hand
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹OOP Patterns for Testbenches
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. OOP Patterns for Testbenches
  5. The Transaction Base Class

Part 2 · OOP for Verification · Intermediate

The Transaction Base Class

Copy, compare, print and pack discipline for transactions, with a self-contained bus example.

On this page

    Why this matters

    A scoreboard compared handles with == and passed every packet, including the wrong ones. Transactions need their own copy and compare, because handles only say which object, never what it holds.

    After this lesson you can: give a transaction copy, compare, convert2string, pack and unpack, and keep all five in step when a field is added.

    Why handles force the discipline

    Assignment copies the handle, not the object. After b = a, both names reach one object, so a write through either name shows through both.

    Handle equality has the same trap. a == b reports whether two names reach the same object, never whether two objects hold equal fields.

    Operation

    What = and == do

    What the bench needs

    Duplicate a transaction

    b = a aliases one object

    copy() builds a detached twin

    Decide pass or fail

    == tests handle identity

    compare() tests field equality

    Debug a mismatch

    A handle prints as an opaque id

    convert2string() prints one dense line

    Drive pins or cross to C

    Fields must be flattened by hand

    pack() and unpack() move a fixed bit order

    1. Generator (makes transactions): randomizes one, then queues t.copy() so re-randomizing t cannot corrupt the queued object.

    2. Monitor (turns pin activity into transactions): builds a fresh transaction per transfer and shares a copy, never a handle it will reuse.

    3. Scoreboard (decides pass or fail): calls exp.compare(act), and prints both convert2string() lines side by side on mismatch.

    4. Pin boundary: calls pack() to flatten a transaction for the driver, and unpack() to rebuild one from observed bits.

    Four methods on every transaction

    Every transaction implements the same four methods from day one. The first scoreboard debug always needs all four, so waiting costs more than writing them.

    Each method is virtual. A scoreboard holding base handles then runs derived methods on derived objects, comparing full fields instead of half.

    Field

    Bits

    Running total

    dir

    1

    1

    addr

    16

    17

    data

    32

    49

    len

    4

    53

    The budget fixes the pack size at 53 bits. The testbench checks that number directly, so a later field addition fails loudly instead of skewing silently.

    Pack order is a contract

    Pack writes fields in one fixed order and unpack reads the same order. Any skew lands every value in the wrong field, sometimes plausibly.

    Unpack guards the contract first. It returns 0 on any queue that is not exactly 53 bits, before touching a field.

    Common mistakes

    Common pitfall

    A compare() that forgets a newly added field blinds the scoreboard to exactly that field. Update copy(), compare(), pack() and unpack() together whenever a field is added.

    Interview

    Likely questions: “what is wrong with b = a for objects”, “why must copy() be virtual”, and “which UVM methods these become”.

    • Alias trap: assignment shares one object; only copy() detaches.

    • Virtual dispatch: base handles must run derived copy() and compare().

    • UVM map: do_copy, do_compare, convert2string and do_pack under uvm_object.

    Check yourself Why does b = a followed by b.addr = 1 change a.addr?

    Assignment copies the handle, not the object. Both names point at one object, so the write is visible through either name.

    Check yourself A scoreboard compares handles with == and always passes. What went wrong?

    == on handles tests identity. An object compared with itself reads equal regardless of fields. Field comparison needs compare().

    Check yourself Pack produces 53 bits and a receiver passes 40. What does unpack do?

    It returns 0 and leaves the object unchanged. The size check rejects short queues before any field is touched.

    Testbench

    One self-contained transaction plus a testbench that proves detach, compare, string and 53-bit pack discipline.

    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
    typedef enum bit { READ, WRITE } dir_e;
    
    class bus_txn;
      dir_e      dir;
      bit [15:0] addr;
      bit [31:0] data;
      bit [3:0]  len;
    
      virtual function bus_txn copy();
        bus_txn t;
        t = new();
        t.dir  = dir;
        t.addr = addr;
        t.data = data;
        t.len  = len;
        return t;
      endfunction
    
      virtual function bit compare(bus_txn rhs);
        if (rhs == null) return 0;
        return (dir == rhs.dir) && (addr == rhs.addr) && (data == rhs.data) && (len == rhs.len);
      endfunction
    
      virtual function string convert2string();
        return $sformatf("%s addr=%04h data=%08h len=%0d", dir.name(), addr, data, len);
      endfunction
    
      virtual function void pack(output bit bits[$]);
        bits = {};
        bits.push_back(dir);
        for (int i = 15; i >= 0; i--) bits.push_back(addr[i]);
        for (int i = 31; i >= 0; i--) bits.push_back(data[i]);
        for (int i = 3;  i >= 0; i--) bits.push_back(len[i]);
      endfunction
    
      virtual function bit unpack(input bit bits[$]);
        int k;
        k = 0;
        if (bits.size() != 53) return 0;
        dir = dir_e'(bits[k]); k++;
        for (int i = 15; i >= 0; i--) begin addr[i] = bits[k]; k++; end
        for (int i = 31; i >= 0; i--) begin data[i] = bits[k]; k++; end
        for (int i = 3;  i >= 0; i--) begin len[i]  = bits[k]; k++; end
        return 1;
      endfunction
    endclass
    
    Terminal
    PASS: copy detaches, compare decides, string matches, pack round-trips in 53 bits

    Project step

    Give pkt_txn the four methods: copy(), compare(), convert2string(), and pack()/unpack() in wire order (header, payload, check byte). unpack() must refuse bytes whose check byte is wrong.

    One way to write it:

    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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
    typedef bit [7:0] bytes_t[$];
    
    class pkt_txn;
      kind_e    kind;
      bit [1:0] dest;
      bit [7:0] payload[$];
    
      function bit [7:0] header();
        return {kind, dest, 4'(payload.size())};
      endfunction
    
      virtual function void copy(pkt_txn rhs);   // fill this object from rhs
        kind = rhs.kind;
        dest = rhs.dest;
        payload = rhs.payload;                   // a queue assignment copies the bytes
      endfunction
    
      virtual function bit compare(pkt_txn rhs);
        return rhs != null && kind == rhs.kind && dest == rhs.dest && payload == rhs.payload;
      endfunction
    
      virtual function string convert2string();
        string s = $sformatf("%s dest=%0d len=%0d payload=", kind.name(), dest, payload.size());
        foreach (payload[i]) s = {s, $sformatf("%h ", payload[i])};
        return s;
      endfunction
    
      // Pack order is the wire order: header, payload, check byte.
      virtual function bytes_t pack();
        bytes_t b = {header()};
        bit [7:0] c = header();
        foreach (payload[i]) begin
          b.push_back(payload[i]);
          c ^= payload[i];
        end
        b.push_back(c);
        return b;
      endfunction
    
      // Rebuild from bytes; return 0 and leave the object alone if they are not a packet.
      virtual function bit unpack(bytes_t b);
        bit [7:0] x = 0;
        if (b.size() < 3 || int'(b[0][3:0]) != b.size() - 2) return 0;
        foreach (b[i]) x ^= b[i];
        if (x != 0) return 0;                    // check byte wrong
        kind = kind_e'(b[0][7:6]);
        dest = b[0][5:4];
        payload = b[1:b.size() - 2];
        return 1;
      endfunction
    endclass
    
    module tb_pkt_methods;
      int errors = 0;
      initial begin
        pkt_txn a, b, c;
        bytes_t w;
        a = new();
        a.kind = DATA; a.dest = 2; a.payload = '{8'hA5, 8'h3C};
        b = new();
        b.copy(a);                               // a detached twin
        b.payload[0] = 8'h00;
        if (a.payload[0] != 8'hA5) errors++;     // a is untouched
        if (a.compare(b)) errors++;
        w = a.pack();
        $display("%s-> %p", a.convert2string(), w);
        c = new();
        if (!c.unpack(w) || !c.compare(a)) errors++;
        w[1] = 8'hFF;                            // corrupt one payload byte
        if (c.unpack(w)) errors++;               // the check byte catches it
        if (errors == 0) $display("PASS: copy detached, compare saw the change, pack and unpack round-tripped, and a corrupt byte was refused");
        else $fatal(1, "FAIL: %0d pkt_txn method checks failed", errors);
        $finish;
      end
    endmodule
    Terminal
    DATA dest=2 len=2 payload=a5 3c -> '{'h22, 'ha5, 'h3c, 'hbb}
    PASS: copy detached, compare saw the change, pack and unpack round-tripped, and a corrupt byte was refused

    Remember

    • copy makes a detached twin; compare checks fields; == only checks identity.

    • convert2string gives one line per object for logs.

    • Pack order is a contract shared by driver, monitor and any C model.

    Rule of thumb: a new field touches all five methods or none.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Related topics

    • Next: The Factory Pattern from Scratch

    Previous

    60 / 259

    Next: The Factory Pattern from Scratch

    Up next

    Up nextThe Factory Pattern from Scratch

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Singletons and Config Objects
    • Callbacks and Hooks
    • Building a Component Hierarchy by Hand