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 | What the bench needs |
|---|---|---|
Duplicate a transaction |
|
|
Decide pass or fail |
|
|
Debug a mismatch | A handle prints as an opaque id |
|
Drive pins or cross to C | Fields must be flattened by hand |
|
Generator (makes transactions): randomizes one, then queues t.copy() so re-randomizing t cannot corrupt the queued object.
Monitor (turns pin activity into transactions): builds a fresh transaction per transfer and shares a copy, never a handle it will reuse.
Scoreboard (decides pass or fail): calls exp.compare(act), and prints both convert2string() lines side by side on mismatch.
Pin boundary: calls pack() to flatten a transaction for the driver, and unpack() to rebuild one from observed bits.
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 |
|---|---|---|
| 1 | 1 |
| 16 | 17 |
| 32 | 49 |
| 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 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.
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.
== and always passes. What went wrong?== on handles tests identity. An object compared with itself reads equal regardless of fields. Field comparison needs compare().
unpack do?It returns 0 and leaves the object unchanged. The size check rejects short queues before any field is touched.
One self-contained transaction plus a testbench that proves detach, compare, string and 53-bit pack discipline.
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
PASS: copy detaches, compare decides, string matches, pack round-trips in 53 bitsOne way to write it:
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
endmoduleDATA 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 refusedRun 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.