UVM RAL · Chapter 9 · Callbacks
Advanced Callback Use Cases
This page shows what callbacks are actually for, the recurring patterns you compose them into, and the one discipline that keeps them safe: managing a callback's scope and lifetime. Error injection uses the pre-write and pre-read hooks to corrupt data, redirect an address, or force an error status for negative testing. Shadow and scoreboard sync uses the post-predict hook to keep a reference model in step with every field-value change. Coverage sampling uses the post-read and post-write hooks to sample a covergroup on each access, and callbacks can also model protocol quirks. The catch is that a callback is a live registration with a scope and a lifetime, so you must register at the right scope and delete it when done. It ends by breaking a type-wide error-injection callback that is never deleted and leaks into later tests.
Foundation12 min readUVM RALerror injectioncoveragecallback scopecallback lifetime
Chapter 9 · Section 9.4 · Callbacks
1. Why Should I Learn This?
Callbacks earn their keep in the advanced patterns — error injection, shadow sync, coverage sampling, protocol quirks — but each of those attaches a live registration that keeps firing until you remove it, at whatever scope you chose. Knowing to register at the right scope and to delete a callback when its purpose ends is what keeps a powerful callback from silently corrupting tests it was never meant to touch.
Learning the common callback patterns and the scope/lifetime discipline turns the hooks of 9.1–9.3 into safe, reusable tools, and it leads directly into RAL coverage (Chapter 10, which is largely callback-sampled) and into callback debugging (9.5).
2. Industry Story — the injector that outlived its test
A team writes an error-injection callback to corrupt writes for a negative test — verifying the DUT flags bad data. To make it apply everywhere in that test, they register it type-wide across a register type. The negative test passes. But they never delete the callback when the test ends, and the same simulation (or a chained set of sequences under one build) then runs other tests.
Those later tests began failing bizarrely: writes to registers of that type were corrupted — because the type-wide error-injection callback was still registered and still firing, long after the one test that wanted it. It had leaked: a callback fires from add until delete, and there was no delete, so the injector kept corrupting every register of that type across every subsequent test. The failures looked like real DUT bugs (corrupted writes everywhere) and took days to trace back to a callback that should have been removed two tests earlier. The fix was two disciplines: register the injector at the tightest scope that the negative test needed (ideally one register instance, not type-wide), and delete it when the test ended so it could not outlive its purpose. The post-mortem lesson: a callback fires from add until delete at whatever scope it was registered — so an error-injection callback registered type-wide and never deleted leaks into later tests and keeps corrupting every register of that type; advanced callbacks must be scoped tightly and removed when their purpose ends, because callbacks do not clean themselves up.
3. Concept — patterns mapped to hooks, plus scope and lifetime
The advanced value of callbacks is composing the hooks into recurring patterns — and managing each registration's scope and lifetime:
- Error injection —
pre_write/pre_read(9.3): corrupt outgoing data, redirect the address, or force an error status for negative testing. - Shadow / scoreboard sync —
post_predict(9.2): keep a reference model in step with every field-value change, on all update paths. - Functional coverage sampling —
post_read/post_write: sample a covergroup on each access (the mechanism behind much of Chapter 10). - Protocol-specific behaviour — model quirks the base access lacks (a lock that gates writes, a companion side-effect).
And the discipline that makes them safe:
- Scope —
add(reg, cb)attaches to one instance; type-wide registration attaches to every instance of a type. Register at the tightest scope your purpose needs; a broader scope affects more registers than you intend. - Lifetime — a callback fires from
adduntildelete(uvm_callbacks#(...)::delete(...)). It does not clean itself up, so a callback whose purpose is temporary (a per-test injector) must be removed when that purpose ends, or it leaks into later tests.
Here is the catalog of patterns mapped to their hooks:
4. Mental Model — a callback is a running subscription, not a one-time action
5. Working Example — a scoped, cleaned-up error injector
Register an error-injection callback at the tightest scope, and delete it when the negative test ends:
// Error injection for a NEGATIVE test: corrupt the outgoing write in pre_write (9.3).
class inject_cb extends uvm_reg_cbs;
`uvm_object_utils(inject_cb)
function new(string name = "inject_cb"); super.new(name); endfunction
virtual task pre_write(uvm_reg_item rw);
rw.value[0] ^= 32'h0000_0001; // corrupt outgoing data so the DUT should flag it
endtask
endclass// Register at the TIGHTEST scope the test needs — ONE register instance, not type-wide:
inject_cb cb = inject_cb::type_id::create("cb");
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(target_reg, cb); // instance scope, not add_by_name(type)
// ... run the negative test: write target_reg, check the DUT flags the corruption ...
// DELETE it when the test's purpose ends — so it cannot outlive this test and leak into the next:
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::delete(target_reg, cb); // cancel the subscription
// After delete, target_reg writes are clean again; no other test is affected.// Other patterns compose the hooks similarly — each a scoped, managed registration:
// shadow sync : post_predict, added on the field(s) it tracks (9.2), lives as long as the model
// coverage sample : post_read/post_write, sampling a covergroup per access (Ch10)
// protocol quirk : model a lock/side-effect the base access lacks
// The constant discipline: choose scope deliberately and remove temporary callbacks when done.Scoping the injector to one register and deleting it when the test ends keeps it from affecting anything else, ever. Skipping either — type-wide scope, or no delete — is the leak of the next section.
6. Debugging Session — an error injector that leaked into later tests
An error-injection callback registered type-wide and never deleted keeps firing in later tests, corrupting every register of that type long after the negative test that wanted it
SCOPE TIGHTLY + DELETE// A negative-test injector registered TYPE-WIDE and never removed:
inject_cb cb = inject_cb::type_id::create("cb");
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add_by_name("ctrl_reg_t", cb, reg_model); // BUG 1: type-wide scope
// ... negative test runs and passes ...
// BUG 2: no uvm_callbacks#(uvm_reg, uvm_reg_cbs)::delete(...) — the injector is NEVER removed.
// It keeps firing on EVERY ctrl_reg_t instance in EVERY later test -> writes corrupted everywhere.The negative test itself passes. Then later tests (running in the same simulation, or chained sequences under one build) start failing bizarrely: writes to registers of type ctrl_reg_t come out corrupted, in tests that have nothing to do with error injection. The failures look exactly like real DUT bugs — data written does not match data read, across many registers — so the investigation chases the DUT and the environment, not the callback. Because the corruption is broad (every instance of the type) and appears in tests that never mentioned a callback, the connection to a negative test two runs earlier is far from obvious.
Two compounding scope/lifetime errors. First, scope: the injector was registered type-wide (add_by_name on the register type), so it attached to every instance of ctrl_reg_t, not just the one register the negative test targeted — far broader than intended. Second, lifetime: a callback fires from add until delete, and it was never deleted, so it kept firing indefinitely. Together, these mean the error injector remained live and type-wide after its test ended, corrupting every ctrl_reg_t write in every subsequent test — because callbacks do not clean themselves up and are not scoped to a test unless you scope and remove them. The later tests are not buggy and the DUT is fine; a callback that should have been narrow and temporary was instead broad and permanent, so it leaked its effect across the whole session. This is not a registration-missing bug (9.1 — it is very much registered), a hook-path bug (9.2), or a pre/post timing bug (9.3); it is lifetime and scope mismanagement.
Two changes. Scope: register the injector at the tightest scope the negative test needs — ideally the single target register instance (add(target_reg, cb)), not type-wide — so it can only affect what the test intends. Lifetime: delete the callback when the negative test's purpose ends (uvm_callbacks#(uvm_reg, uvm_reg_cbs)::delete(target_reg, cb)), so it cannot outlive the test and leak into later ones. With both, the injector affects only its target register and only during its test. The rule the bug teaches: a callback fires from add until delete at whatever scope it was registered — so temporary, powerful callbacks (error injectors, overrides) must be scoped tightly and explicitly removed when done; a type-wide, never-deleted callback leaks into later tests and corrupts registers no test asked it to touch. The tell in the wild is broad corruption appearing in tests that never registered a callback — that is a leaked callback from an earlier test, and the fix is scope plus delete, not the DUT.
7. Common Mistakes
- Registering a temporary callback type-wide. A negative-test injector meant for one register attaches to every instance of the type — scope to the single instance you actually target.
- Never deleting a callback whose purpose is temporary. Callbacks fire from
adduntildelete; an un-deleted injector or override leaks into later tests. - Treating a callback as a one-time action. It is a running subscription that fires on every future access until removed — plan its removal with its addition.
- Blaming the DUT for broad corruption in unrelated tests. Corruption across many registers in tests that never registered a callback is usually a leaked callback from an earlier test.
- Over-broad protocol/override callbacks. A quirk-modelling callback registered too widely changes behaviour on registers that do not have the quirk.
8. Industry Best Practices
- Register at the tightest scope. One instance unless you genuinely mean every instance of a type — scope limits blast radius.
- Plan the
deletewith theadd. Remove temporary callbacks (injectors, overrides) when their test or phase ends — callbacks do not self-clean. - Compose hooks to their purpose. Error injection in
pre_*(9.3), shadow sync inpost_predict(9.2), coverage inpost_*, protocol quirks as needed. - Suspect a leaked callback when unrelated tests show broad corruption. Trace to an earlier test's un-deleted, over-scoped callback before blaming the DUT.
- Keep advanced callbacks focused and documented. One purpose, clear scope, explicit lifetime — so registration and removal stay auditable (and ordering stays clear, 9.5).
9. Interview / Review Questions
10. Key Takeaways
- Callbacks compose into recurring patterns: error injection (
pre_write/pre_read, 9.3), shadow/scoreboard sync (post_predict, 9.2), coverage sampling (post_read/post_write, Ch10), and protocol quirks. - A callback is a live registration with a scope and a lifetime — it fires on every matching access, across however many registers its scope covers, from
adduntildelete. - Register at the tightest scope (one instance vs type-wide) and
deletetemporary callbacks when done — callbacks do not self-clean, so a leaked one keeps firing. - The signature advanced bug is an error-injection callback registered type-wide and never deleted: it leaks into later tests, corrupting every register of that type long after the test that wanted it — a scope/lifetime bug, not a registration (9.1), hook-path (9.2), or timing (9.3) bug.
- Broad, DUT-looking corruption in tests that registered no callback is the tell of a leaked callback — the fix is scope plus
delete, and the danger scales with how much a callback changes, so injectors and overrides demand the strictest scope and removal.