Skip to content

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 injectionpre_write/pre_read (9.3): corrupt outgoing data, redirect the address, or force an error status for negative testing.
  • Shadow / scoreboard syncpost_predict (9.2): keep a reference model in step with every field-value change, on all update paths.
  • Functional coverage samplingpost_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:

  • Scopeadd(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 add until delete (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:

Advanced callback patterns: error injection (pre), shadow sync (post_predict), coverage (post), protocol quirks — each a scoped, time-bounded registrationscope +deleteError injectionpre_write / pre_read — corrupt data, redirect addr, force error (negative test)pre_write / pre_read —corrupt data, redirectaddr, force error (negativ…Shadow / scoreboardsyncpost_predict — track everyfield-value change (9.2)Coverage samplingpost_read / post_write —sample covergroup peraccess (Ch10)Protocol quirksmodel locks / side-effectsthe base access lacksManage SCOPE +LIFETIMEregister at the tightestscope; DELETE when done —callbacks do not self-clean12
Figure 1 — the common advanced callback patterns and the hook each uses. Error injection -> pre_write/pre_read (shape the outgoing request, 9.3). Shadow/scoreboard sync -> post_predict (track every field-value change, 9.2). Coverage sampling -> post_read/post_write (sample a covergroup per access, bridging to Ch10). Protocol quirks -> assorted hooks (model behaviour the base access lacks). Overlaying all of them: each is a LIVE registration with a SCOPE (instance vs type-wide) and a LIFETIME (fires from add until delete) — both must be managed, or a powerful callback leaks beyond its intended registers and its intended test.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

1

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
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.
Symptom

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.

Root Cause

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.

Fix

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 add until delete; 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 delete with the add. 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 in post_predict (9.2), coverage in post_*, 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 add until delete.
  • Register at the tightest scope (one instance vs type-wide) and delete temporary 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.