dma_reset_test overrides axis_driver with axis_driver_ra. dma_truncate_test overrides it with axis_truncating_driver. Now the test plan asks for a short packet and a reset in the middle of the next job, in one test. The factory has one slot for axis_driver, and it holds one class. You could write axis_truncating_driver_ra. Next month someone wants corrupted data with reset, then truncation with corruption, and the class list grows as the product of every feature. Is there a way to add fault behaviour without choosing one class?
A factory override replaces the worker: the bench builds a different class in that slot, chosen once, at build time. A callback keeps the worker and hands them an extra step on their checklist: an object attached to one component instance, called at a fixed point, which can be added, stacked with others and removed while the test runs. Overrides are the right tool when a whole component behaves differently. Callbacks are the right tool for small behaviours that must combine.
Factory override | Callback | |
|---|---|---|
What changes | The class that is built | Extra objects called by one instance |
When it is decided | Before the component is created (build) | Any time after it exists, add or delete |
Combining two behaviours | Needs a class that has both | Add two callbacks; they run in order |
Scope | Every instance (type) or one path (instance) | One instance, or all instances with a null object |
What the VIP must provide | Virtual methods to override | A registered callback class and a call site |
A callback needs three pieces, and missing any one of them fails quietly. The VIP author writes the first two once; tests write the third.
The callback base class extends uvm_callback and declares the hook as a virtual method that does nothing. It receives the driver (so a callback can read its configuration) and the beat by inout, so each callback in a list sees the result of the one before. Setting the beat to null drops it.
// snippet: scratch run alongside the DMA bench
typedef class axis_cb_driver;
class axis_driver_cb extends uvm_callback;
`uvm_object_utils(axis_driver_cb)
function new(string name = "axis_driver_cb");
super.new(name);
endfunction
// beat: the beat to drive; set it to null to drop it.
virtual function void pre_drive(axis_cb_driver drv, inout axis_item beat);
endfunction
endclassuvm_register_cb(T, CB) tells UVM that objects of type T accept callbacks of type CB. uvm_do_callbacks(T, CB, METHOD) walks every callback attached to this and calls the method on each. In a VIP, both lines go into axis_driver itself, inside its pre_drive. The course bench is not edited, so the scratch run puts them in a small subclass and swaps it in once by override:
// snippet: scratch run alongside the DMA bench
class axis_cb_driver extends axis_driver;
`uvm_component_utils(axis_cb_driver)
`uvm_register_cb(axis_cb_driver, axis_driver_cb)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function axis_item pre_drive(axis_item it);
axis_item beat = it;
`uvm_do_callbacks(axis_cb_driver, axis_driver_cb, pre_drive(this, beat))
return beat; // the item, a modified copy, or null
endfunction
endclassThe truncation becomes a callback with the same logic as axis_truncating_driver, including the clone. The test adds it after the driver exists, so in connect_phase (or later), and can delete it at run time, which no override can do:
// snippet: scratch run alongside the DMA bench
class truncate_cb extends axis_driver_cb;
`uvm_object_utils(truncate_cb)
int unsigned cut = 3;
int unsigned beat_no;
function new(string name = "truncate_cb");
super.new(name);
endfunction
virtual function void pre_drive(axis_cb_driver drv, inout axis_item beat);
axis_item b;
int unsigned i = beat_no;
if (beat == null) return; // an earlier callback already dropped it
beat_no = beat.last ? 0 : beat_no + 1;
if (i > cut) begin beat = null; return; end
if (i < cut) return;
if (!$cast(b, beat.clone())) `uvm_fatal("CLONE", "axis_item clone failed")
b.last = 1'b1;
beat = b;
`uvm_info("TRUNC_CB", $sformatf("TLAST forced on beat %0d", i), UVM_MEDIUM)
endfunction
endclass
// In dma_cb_truncate_test (d is env.in_agt.drv, cast to axis_cb_driver):
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
tcb = truncate_cb::type_id::create("tcb");
if (!$cast(d, env.in_agt.drv)) `uvm_fatal("CAST", "in_agt.drv is not an axis_cb_driver")
uvm_callbacks#(axis_cb_driver, axis_driver_cb)::add(d, tcb);
endfunction
// In run_phase, after job 1 (LEN=8, cut) and before job 2 (LEN=4, clean):
// uvm_callbacks#(axis_cb_driver, axis_driver_cb)::delete(d, tcb);Callbacks run in the order they were added (UVM_APPEND, the default); pass UVM_PREPEND as a third argument to put one at the front. Passing null as the object adds a callback to every instance of the type. uvm_callbacks#(T, CB)::display() prints what is attached where, which is the first thing to run when a callback seems not to fire.
A scratch run of dma_cb_truncate_test: job 1 with the callback attached, then the callback deleted and a clean job 2:
UVM_INFO scratch_tb.sv(92) @ 275000: reporter [TRUNC_CB] TLAST forced on beat 3
UVM_INFO scratch_tb.sv(133) @ 385000: uvm_test_top [CB] job 1 STATUS=00000004
UVM_INFO scratch_tb.sv(137) @ 735000: uvm_test_top [CB] job 2 (callback removed) STATUS=00000002
UVM_INFO dma_tb.sv(411) @ 835000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0The cut happens at the same beat and time as with the override (275 ns): the behaviour moved, not changed.
job 1 STATUS=4 (ERR), then job 2 STATUS=2 (DONE) after the delete: one test turned the fault on and off, which an override cannot do.
The callback reports as reporter because a callback is an object, not a component. Pass the driver to uvm_info_context if you want the driver's path on the line.
A team wrote a callback for their stream driver and added it type-wide in the test, uvm_callbacks#(axis_driver, axis_driver_cb)::add(null, cb), so every driver of that type would get it. The driver class that actually registered and called the callbacks was a subclass. The fault test passed. Weeks later someone noticed the fault had never been injected: the test checked only that no mismatches occurred, which is easy when nothing goes wrong. The only trace was one warning among hundreds:
UVM_WARNING @ 0: reporter [CBUNREG] Callback tcb cannot be registered with object uvm_test_top.env.in_agt.drv because callback type truncate_cb is not registered with object type axis_driverThat line is real, from a scratch run that makes the same mistake. The T in uvm_callbacks#(T, CB) must be the type that has uvm_register_cb(T, CB), and the add is checked against that registration, not against the object's actual class. The trap is specific to the type-wide form: a type-wide add under the wrong T lands in a queue the subclass never reads, so the fault is never injected. An instance add under the wrong T prints the same CBUNREG warning, but the callback still fires, because instance queues are kept per object in one global pool (verified in a scratch run: STATUS=4, the fault happened). The warning means "check this" either way. Three habits prevent it: promote CBUNREG to an error in the base test (uvm_root::get().set_report_severity_id_override(UVM_WARNING, "CBUNREG", UVM_ERROR), since uvm_root is the reporter that issues it), have every fault callback count how many times it fired, and make every fault test check that the fault happened on the pins, not only that nothing mismatched.
Add the callback to that one driver instance, for example env.dma0.in_agt.drv, after build. The other driver has no callback attached and stays clean. With overrides it would need an instance override on that path instead; with callbacks the choice is made per instance at run time.
truncate_cb and then corrupt_cb (which flips bit 0 of each beat's data) to the same driver. What does a dropped beat look like to corrupt_cb?It receives beat == null, because truncate_cb ran first and dropped the beat, and it must return without touching it. Callbacks in a list see each other's results, so every callback has to handle a null beat. Reversing the order with UVM_PREPEND would corrupt beats that are then dropped anyway.
connect_phase or later, not in build_phase before super.build_phase?The add needs the driver object, and the driver does not exist until the agent's build_phase has run, which happens after the test's build_phase returns. An override is registered before creation because it changes what gets created; a callback is attached to something that already exists.
Next: whatever injects the fault, the bench must know a fault is coming. Checking expected faults makes the scoreboard predict it from what the monitors saw.