Skip to content

UVM RAL · Chapter 9 · Callbacks

Register Callbacks

Callbacks let you extend what happens on a register access without editing the register class itself, which matters when the model is generated or shared. You subclass the base callback class, override the hook methods you care about, create an instance, and register it onto a register so UVM invokes your code on every access to add side effects, extra checks, or shadow-model updates. Two ideas run through the chapter. Callbacks are the sanctioned, non-invasive way to add register behaviour, and a callback does absolutely nothing until it is registered onto a specific register. This page teaches the mechanism and breaks the classic bug where a perfectly written callback is never added, so it silently never fires and the real fault is a missing registration.

Foundation12 min readUVM RALcallbacksuvm_reg_cbsuvm_callbacksregistration

Chapter 9 · Section 9.1 · Callbacks

1. Why Should I Learn This?

Callbacks are how you add behaviour to register accesses — shadow updates, side effects, checks, error injection — without forking the register model, which is essential when the model is generated or shared. Understanding the mechanism (subclass uvm_reg_cbs, override hooks, register with add) and, above all, that a callback is inert until registered, is what lets you extend register behaviour cleanly — and saves you from the near-universal 'my callback isn't firing' confusion that is really a missing registration.

Learning the callback mechanism and the register-it-or-it-does-nothing rule opens Chapter 9 and underpins everything after it — field callbacks (9.2), the pre/post hooks (9.3), advanced uses (9.4), and callback ordering (9.5) all build on this base.

2. Industry Story — the callback that was never attached

A verification engineer needs a shadow model updated whenever a configuration register is written, so they write a clean uvm_reg_cbs subclass overriding the post-write hook to update the shadow. The code is reviewed, the logic is correct, and it is committed. But the shadow model stays stale in every test — as if the callback did not exist.

It effectively did not, because the callback was never registered. The class was written and instantiated in one place, but uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(cfg_reg, cb) was never called, so UVM had no idea the callback should fire on cfg_reg accesses — and it silently did nothing. Days were lost debugging the hook logic (which was perfect) because the failure gave no signal: no error, no warning, just a callback that never ran. The moment add was called to attach the instance to the register, the same untouched hook code worked immediately. The post-mortem lesson: a callback does nothing until it is registered onto a register with uvm_callbacks::add — a perfectly written, instantiated callback that is never add-ed is completely inert and fires nothing, silently; 'my callback isn't working' is almost always 'my callback isn't registered' (or is registered on the wrong register).

3. Concept — subclass, override, and register the callback

A register callback is added in four steps, and the fourth is the one that actually connects it:

  • Subclass uvm_reg_cbs. It is the base callback class with hook methods (the pre/post read/write hooks, covered in 9.3). Your subclass overrides the hooks you need.
  • Override the hook(s). Put your extra behaviour — shadow update, side effect, check, error injection — in the relevant overridden method.
  • Instantiate the callback. Create an object of your subclass.
  • Register it onto the register. uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(reg, cb) attaches the instance to a specific register (or a type-wide registration attaches to all instances of a register type). This is what makes UVM invoke the callback on that register's accesses.

The crucial property: the first three steps produce an inert object. Only add connects it — an unregistered callback fires nothing, with no error. Here is the model, from base class to firing:

Register callback model: uvm_reg_cbs base -> subclass overriding hooks -> add(reg, cb) registers -> UVM invokes on accessextendinstantiate +registerconnects thecallbackuvm_reg_cbsbase callback class — hookmethodsmy_reg_cbsubclass — override hookswith your behaviouradd(reg, cb)uvm_callbacks#(uvm_reg,uvm_reg_cbs)::add —REGISTER onto a registerUVM invokes on accesshooks fire on regread/write — ONLY after add12
Figure 1 — the register callback model. uvm_reg_cbs is the base callback class; you subclass it and override its hook methods with your extra behaviour (shadow update, side effect, check). You then instantiate it and — the connecting step — REGISTER it onto a specific register with uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(reg, cb). Only after add does UVM invoke your callback's hooks on that register's accesses. A written, instantiated-but-unregistered callback is INERT — it fires nothing, silently. Registration, not existence, is what attaches a callback.

4. Mental Model — a callback is a guest that must be invited, not just hired

5. Working Example — a registered post-write callback

Subclass uvm_reg_cbs, override a hook, and — the essential step — register the instance onto the register:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1-2. Subclass uvm_reg_cbs and override a hook with the extra behaviour (here: update a shadow model).
class shadow_update_cb extends uvm_reg_cbs;
  `uvm_object_utils(shadow_update_cb)
  shadow_model shadow;
  function new(string name = "shadow_update_cb"); super.new(name); endfunction
 
  // post_write fires after a register write completes (hook details in 9.3).
  virtual task post_write(uvm_reg_item rw);
    shadow.update(rw.element.get_full_name(), rw.value[0]);   // extend behaviour, no edit to the reg class
  endtask
endclass
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 3-4. Instantiate AND REGISTER it onto the specific register. Step 4 is what makes it fire.
shadow_update_cb cb = shadow_update_cb::type_id::create("cb");
cb.shadow = env.shadow;
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(cfg_reg, cb);   // <-- REGISTER: without this, cb is inert
// From now on, every write to cfg_reg invokes cb.post_write — and the uvm_reg class was never modified.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Type-wide registration attaches to ALL instances of a register type (vs one instance above):
// uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add_by_name("cfg_reg_t", cb, reg_model);
// The key point either way: it is the add call, not the class's existence, that connects the callback.

The add call is the whole game: the hook logic can be perfect, but only registration makes UVM invoke it. Omit add, and you get the inert-callback bug of the next section.

6. Debugging Session — a perfect callback that never fires

1

A callback with flawless hooks does nothing because it was never registered — a callback is inert until add() attaches it to a register

REGISTER THE CALLBACK
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The callback class is correct and even instantiated — but never REGISTERED onto the register:
class shadow_update_cb extends uvm_reg_cbs;
  virtual task post_write(uvm_reg_item rw);
    shadow.update(rw.element.get_full_name(), rw.value[0]);   // perfect hook logic
  endtask
endclass
// ...
shadow_update_cb cb = shadow_update_cb::type_id::create("cb");
cb.shadow = env.shadow;
// BUG: no uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(cfg_reg, cb);  -> cb is INERT, never fires.
Symptom

The shadow model is never updated — it stays stale through every test, exactly as if the callback did not exist. There is no error, no warning, no diagnostic of any kind: writes to cfg_reg proceed normally, and post_write simply never runs. The engineer, seeing the shadow not update, naturally suspects the hook logic and spends time scrutinizing post_write (which is perfect), because nothing points at registration. The tell — invisible unless you look for it — is that the callback object exists and is well-formed but there is no add call anywhere associating it with cfg_reg.

Root Cause

UVM invokes a register's callbacks by consulting the set of callbacks registered on that register (or its type) via the uvm_callbacks pool; it does not scan for or run callback objects merely because they exist. The callback here was subclassed correctly, its hook was written correctly, and it was even instantiated — but uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(cfg_reg, cb) was never called, so from UVM's perspective cfg_reg has no callback attached, and there is nothing to invoke on its writes. The callback is not broken — it is absent: perfectly capable and completely unconnected. The failure is silent precisely because an unregistered callback is not an error condition — UVM cannot warn about a callback it was never told exists; it simply runs the (empty) set of registered callbacks. So the stale shadow is not a hook bug at all; it is a missing registration, and the correct code was never given a chance to run.

Fix

Register the callback: call uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(cfg_reg, cb) (or the type-wide add_by_name) after instantiating it, so UVM associates cb with cfg_reg and invokes post_write on every write — the unchanged hook logic then works immediately. The rule the bug teaches: a callback is inert until it is registered onto a register with uvm_callbacks::add; writing and instantiating it is not enough, and an unregistered callback fires nothing with no error. The debugging corollary is just as important: when a callback does not fire, check registration first — confirm add was called, on the correct register or type — before suspecting the hook code, because the silent, no-diagnostic nature of a missing registration makes it masquerade as a logic bug in code that is actually perfect.

7. Common Mistakes

  • Writing a callback but never registering it. The class existing is not the same as it being attached — always call uvm_callbacks::add(reg, cb), or it is inert and silent.
  • Registering on the wrong register (or wrong type). A callback add-ed to a different register fires on that one, not the one you meant — verify the target of add.
  • Debugging the hook logic first when a callback does not fire. Suspect registration first; a missing add gives no error and masquerades as a logic bug.
  • Editing the uvm_reg subclass to add behaviour. Callbacks exist so you do not fork the model — extend via a callback, not by modifying a generated/shared register class.
  • Confusing instance registration with type-wide registration. add(reg, cb) attaches to one instance; type-wide add_by_name attaches to all instances of a type — pick the intended scope.

8. Industry Best Practices

  • Pair every callback with its add. Treat registration as the real act of installing a callback; a callback without an add does nothing.
  • Extend register behaviour via callbacks, not by editing the model. Keeps generated/shared register classes closed for modification, open for extension.
  • When a callback does not fire, check registration first. Confirm add on the correct register/type before touching hook logic — the silent failure is almost always a missing/misdirected registration.
  • Choose instance vs type-wide registration deliberately. add(reg, cb) for one register; type-wide for every instance of a register type — match the scope to intent.
  • Keep callbacks focused. One clear extension per callback (shadow update, side effect, check) so registration and ordering (9.5) stay comprehensible.

9. Interview / Review Questions

10. Key Takeaways

  • Callbacks extend what happens on a register access without modifying the register class — the open/closed principle in RAL, essential when the model is generated or shared.
  • The mechanism is four steps: subclass uvm_reg_cbs, override its hook(s) with your behaviour, instantiate it, and register it with uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(reg, cb).
  • A callback is inert until registered — a perfectly written, instantiated callback that is never add-ed fires nothing, silently; registration (not existence) is what connects it.
  • The opener bug is exactly that: a callback with flawless hooks that never runs because it was never add-ed (or was added to the wrong register) — the 'my callback isn't working' that is really 'my callback isn't registered.'
  • When a callback does not fire, suspect registration first — confirm add on the correct register/type and scope before debugging the hook code, because a missing registration gives no error and masquerades as a logic bug.