Part 5 · Sequences · Intermediate
Copy, Clone & Scoreboard Queue Safety
type_id::create, copy/clone before queueing, nested objects, and the monitor reuse trap.
Why copy before queue
Monitors and drivers reuse a single transaction object for performance. The monitor fills fields on object t, calls ap.write(t), then on the next clock overwrites t's fields. If the scoreboard queues the handle instead of a snapshot, every queued entry points to the same object — and that object's fields always reflect the last sample. Compare then passes or fails randomly. Clone before queue is non-negotiable.
The same rule applies on the expected path: ref models often mutate a working copy. Queue exp snapshots with copy/clone, never the live handle from predict().
[ITEM] handle queue vs copy queue
Monitor reuses txn object M:
Cycle 1: M.addr=0x100 → ap.write(M) → scb queues HANDLE to M
Cycle 2: M.addr=0x200 → ap.write(M) → M.addr overwritten!
Cycle 3: scoreboard pops queue[0] → M.addr is 0x200, not 0x100 ← BUG
FIX: snap = create(); snap.copy(M); queue.push_back(snap);type_id::create — always, not new()
UVM factory integration requires type_id::create() for items. new() bypasses factory overrides — tests cannot swap item types without editing sequences.
// CORRECT — factory-aware
apb_item req = apb_item::type_id::create("req");
// WRONG — bypasses factory
apb_item req = new("req");
// Override in test:
set_type_override_by_type(apb_item::get_type(), apb_item_errata::get_type());
// Only affects type_id::create calls[UVM] factory path for [ITEM]
[SEQ] req = apb_item::type_id::create("req")
│
▼
UVM factory lookup → apb_item or override type
│
▼
[ITEM] randomized and driven
new("req") skips factory — override silently ignoredEvery create in sequences, monitors, scoreboards — consistent factory path.
Named create string appears in debug — use descriptive names.
Factory override per test — errata items without VIP edits.
copy vs clone — API reference
Both duplicate registered fields. copy(dest) fills an existing object; clone(dest) is equivalent convenience. Always create dest first.
function void write_exp(apb_item t);
apb_item snap;
snap = apb_item::type_id::create("exp_snap");
snap.copy(t); // deep copy registered fields
exp_queue.push_back(snap);
`uvm_info("SCB", $sformatf("queued exp addr=0x%08x", snap.addr), UVM_HIGH)
endfunction
function void write_act(apb_item t);
apb_item exp;
if (exp_queue.size() == 0) begin
`uvm_error("SCB", "unexpected actual — no expected in queue")
return;
end
exp = exp_queue.pop_front();
if (!t.compare(exp))
`uvm_error("SCB", $sformatf("mismatch exp=%s act=%s",
exp.sprint(), t.sprint()))
endfunctioncopy requires pre-allocated destination — type_id::create first.
clone(dest) same effect — stylistic preference in UVM codebases.
Only UVM_FIELD-registered members copy — verify registration.
APB scoreboard walkthrough
Follow one APB write through ref model predict and monitor sample — both paths must clone before queue.
[CHECK] APB write scoreboard — copy at both streams
Step 1: [SEQ] drives write addr=0x4000 data=0xAA
Step 2: ref_model.predict(req)
exp = create(); exp.copy(predicted_rsp);
scb.write_exp(exp); ← snapshot queued
Step 3: [UVM] monitor samples pins (reuses mon_txn)
mon_txn.addr=0x4000 mon_txn.data=0xAA
scb.write_act(mon_txn);
compare uses queued exp snapshot — NOT live mon_txn handle
Step 4: monitor reuses mon_txn for next beat — queue unaffectedclass apb_scoreboard extends uvm_scoreboard;
apb_item exp_q[$];
function void write_exp(apb_item t);
apb_item snap = apb_item::type_id::create("exp_snap");
snap.copy(t);
exp_q.push_back(snap);
endfunction
function void write_act(apb_item t);
if (exp_q.size() == 0) begin
`uvm_error("SCB", "unexpected act")
return;
end
apb_item exp = exp_q.pop_front();
if (!t.compare(exp))
`uvm_error("SCB", "APB mismatch")
endfunction
endclassNested objects — uvm_field_object
Items containing nested uvm_objects need uvm_field_object for deep copy. Shallow copy duplicates the handle — nested object still shared.
class burst_beat extends uvm_object;
rand bit [31:0] data;
`uvm_object_utils(burst_beat)
endclass
class axi_burst_item extends uvm_sequence_item;
rand burst_beat beats[];
`uvm_object_utils_begin(axi_burst_item)
`uvm_field_array_object(beats, UVM_ALL_ON)
`uvm_object_utils_end
endclass
// copy now deep-copies beats array and each element[ITEM] shallow vs deep copy trap
beats[$] without field_array_object:
snap.copy(orig) → snap.beats[i] SAME HANDLE as orig.beats[i]
mutate orig.beats[0] → snap.beats[0] changes too
With uvm_field_array_object: independent nested copiesDriver item reuse — finish_item boundary
After finish_item returns, the driver has called item_done — safe to reuse or re-randomize req. Before finish_item returns, the driver may still read req fields during drive_apb. Do not randomize mid-flight.
[SEQ] item lifetime during handshake
start_item(req)
randomize(req) ← [SEQ] owns req content
finish_item(req) ← BLOCKS — [DRV] reading req
│
├── get_next_item(req)
├── drive_apb(req) ← reads addr, data, write
└── item_done()
finish_item returns ← NOW safe to re-randomize reqKey takeaways
type_id::create for every item — factory overrides depend on it.
copy/clone before ANY queue — monitor and ref model reuse objects.
Register nested objects with uvm_field_object for deep copy.
Common pitfalls
Queueing handle not copy — #1 scoreboard silent corruption bug.
new() instead of type_id::create — factory override ignored.
Unregistered field not copied — compare misses mismatches on that field.
Randomizing req before finish_item returns — race with driver reads.