UVM RAL · Chapter 9 · Callbacks
Callback Debugging
Debugging a single callback is one thing, but the genuinely hard situation is several callbacks on the same register, where the bug is order. Callbacks on a register do not fire in isolation. They fire in a chain, in the order they were registered, each running after the last. That is fine when they are independent, but the moment more than one transforms the same value, each sees the previous one's output, so the composed result depends entirely on the order. Reverse two transforms and you get a different answer. This produces a bug that survives every single-callback check: each callback is registered, on the right hook, at the right time, and scoped correctly, yet the result is still wrong. This lesson teaches the fix: list the callback queue on the register, confirm which fired and in what order, then breaks two pre-write transforms composed the wrong way.
Foundation12 min readUVM RALcallback ordercallback chaindebuggingcompose
Chapter 9 · Section 9.5 · Callbacks
1. Why Should I Learn This?
Once more than one callback sits on a register, correctness depends not just on each callback but on the order they fire — and order bugs are invisible if you only ever look at one callback at a time. Knowing that callbacks form a registration-ordered chain, that composed transforms are order-dependent, and how to list the callback queue to see the whole chain, is what lets you debug the failures that survive every single-callback check.
Learning to debug multiple callbacks — the chain, its order, and how to inspect it — is the capstone of Chapter 9, tying together registration (9.1), hook choice (9.2), timing (9.3), and scope (9.4) with the new dimension of ordering among several.
2. Industry Story — two correct callbacks, one wrong result
A register has two pre_write callbacks: one scales the outgoing value, another offsets it. Each was written and reviewed on its own, each is correctly registered, on the right hook, at the right time, properly scoped — every single-callback check passes. Yet the value reaching the DUT is wrong, and the author is baffled because both callbacks are correct.
The problem was order. Callbacks fire in the order registered, and the two had been added such that offset ran before scale — so the DUT received (value + offset) * scale when the author intended (value * scale) + offset. Two individually-correct transforms composed in the wrong order gave a wrong result, and no amount of staring at either callback alone could reveal it, because neither was wrong — their composition was. The fix came only when the author listed the callback queue on the register, saw both callbacks and their order, and realized the sequence was reversed from the intent. Re-registering them in the intended order (or making the transforms order-independent) fixed it. The post-mortem lesson: callbacks on a register fire in registration order as a chain, so when multiple callbacks transform the same value the result is order-dependent — two individually-correct transforms can compose to a wrong answer, and the only way to see it is to inspect the whole callback queue, not any single callback.
3. Concept — callbacks form a registration-ordered chain
The key fact for debugging multiple callbacks:
- Callbacks fire in a chain, in registration order. All callbacks on a register (for a given hook) run in sequence, in the order they were
add-ed — one after another, each after the previous. - Composed transforms are order-dependent. When several callbacks modify the same value (e.g. two
pre_writehooks changingrw.value), each sees the previous callback's output, so the final value is the composition in registration order.fthenggivesg(f(x)); swap them and you getf(g(x))— generally different. - A wrong result with all-correct callbacks is an order (or extra-callback) bug. If every callback passes the single-callback checks (registered 9.1, right hook 9.2, right time 9.3, scoped 9.4) and the result is still wrong, suspect the order of composition, or an unexpected extra callback in the chain (a leftover from elsewhere, 9.4).
- Debug by listing the queue. Inspect the set of callbacks on the register (UVM can display the callback queue) to see every callback attached and the order they fire — then reason about how their transforms compose.
Here is a value flowing through a two-callback chain in registration order, where the order decides the result:
4. Mental Model — callbacks are a pipeline, and order is part of the program
5. Working Example — inspecting the chain and making order explicit
List the callback queue to see the chain, and make composed transforms order-safe or explicitly ordered:
// Two pre_write transforms on the same register — the DUT sees their COMPOSITION in registration order.
class scale_cb extends uvm_reg_cbs; virtual task pre_write(uvm_reg_item rw); rw.value[0] *= 4; endtask endclass
class offset_cb extends uvm_reg_cbs; virtual task pre_write(uvm_reg_item rw); rw.value[0] += 'h10; endtask endclass// Registration ORDER decides the result. Add in the intended composition order:
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(reg_h, scale_cb::type_id::create("scale")); // runs FIRST
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(reg_h, offset_cb::type_id::create("offset")); // runs SECOND
// -> DUT gets (value*4)+0x10. Adding offset first would give (value+0x10)*4 — a different value.// DEBUG a wrong-but-each-callback-correct result: LIST the callback queue to see every cb and the order.
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::display(reg_h); // prints the chain attached to reg_h, in order
// Look for: (a) an UNEXPECTED extra callback in the chain (a leftover, 9.4);
// (b) the two transforms in an order opposite to intent.// Confirm which fired and in what order with a marker in each hook:
class scale_cb extends uvm_reg_cbs;
virtual task pre_write(uvm_reg_item rw);
`uvm_info("CBORDER", $sformatf("scale sees %h", rw.value[0]), UVM_LOW) // trace the value through the chain
rw.value[0] *= 4;
endtask
endclassListing the queue (display) shows the whole chain and its order; markers trace the value through it. Together they make an order bug — or an unexpected extra callback — visible, where inspecting one callback never would.
6. Debugging Session — two correct callbacks composed in the wrong order
Two individually-correct pre_write transforms compose to a wrong result because they fire in an unintended registration order — visible only by inspecting the whole callback chain
ORDER OF THE CHAIN// Intent: DUT should receive (value * scale) + offset -> scale must run BEFORE offset.
// But the callbacks were registered offset-FIRST:
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(reg_h, offset_cb::type_id::create("offset")); // runs FIRST
uvm_callbacks#(uvm_reg, uvm_reg_cbs)::add(reg_h, scale_cb::type_id::create("scale")); // runs SECOND
// Chain composes as scale(offset(value)) = (value + offset) * scale -> WRONG (intended (value*scale)+offset).
// Each callback is individually correct; the ORDER of composition is wrong.The value reaching the DUT is wrong — but every single-callback check passes: both callbacks are registered (9.1), on the correct hook pre_write (9.2), at the right time before the bus (9.3), and properly scoped (9.4). Examined individually, scale_cb correctly scales and offset_cb correctly offsets — neither has a bug. The author, checking each callback in turn and finding both correct, is stuck, because the wrongness is not in any callback. The only visible symptom is the wrong composed value, with no single callback to blame.
Callbacks on a register fire in a chain, in registration order, and when multiple callbacks transform the same value the result is their composition in that order — which is not commutative. The intent was (value * scale) + offset, requiring scale to run before offset. But they were registered offset-first, so the chain composed as scale(offset(value)) = (value + offset) * scale — a different result. Neither callback is wrong; their order of composition is. This is exactly the class of bug that survives every single-callback lens (registration, hook, timing, scope), because those all check a callback in isolation, and the defect exists only in the relationship between the two — their firing order. It stayed invisible because the author was examining callbacks one at a time, and an order bug is only visible when you look at the whole chain and how the value flows through it.
Two complementary fixes. Immediately: register the callbacks in the intended composition order (scale first, then offset) so the chain composes as (value * scale) + offset. Structurally: where practical, make composed transforms order-independent or combine order-dependent steps into a single callback whose internal order is explicit, so correctness does not hinge on registration sequence. And to find such bugs, inspect the callback queue (display the register's callbacks) to see every callback and its order, and add per-hook markers to trace the value through the chain. The rule the bug teaches: callbacks fire in registration order as a chain, so multiple callbacks transforming the same value compose order-dependently — a wrong result with every callback individually correct is an ordering (or unexpected-extra-callback) bug, found only by inspecting the whole chain, never by examining one callback. When single-callback checks all pass but the result is wrong, the next move is always to list the queue.
7. Common Mistakes
- Debugging one callback when several are on the register. An order bug is invisible in any single callback — list the whole callback queue and trace the value through the chain.
- Assuming callback order does not matter. When multiple callbacks transform the same value, composition is order-dependent — registration order is part of the computation.
- Registering order-dependent transforms in the wrong sequence. Scale-then-offset is not offset-then-scale — register in the intended composition order.
- Forgetting a leftover callback is still in the chain. An unexpected extra callback (a leak, 9.4) adds a transform you did not account for — the queue reveals it.
- Not tracing the value through the chain. Per-hook markers showing what each callback sees make the composition visible; without them the result looks impossible.
8. Industry Best Practices
- Inspect the callback queue when debugging multi-callback registers.
displaythe register's callbacks to see every one and its firing order before suspecting any single callback. - Register order-dependent transforms in explicit, intended order. Treat registration sequence as part of the program when transforms compose.
- Prefer order-independent or single-callback composition. Combine order-dependent steps into one callback with explicit internal ordering to remove the hazard.
- Trace values through the chain with markers. Per-hook
uvm_infoshowing what each callback receives makes composition and order visible. - When single-callback checks all pass but the result is wrong, list the queue. The defect is in the chain's order or membership, not in any isolated callback.
9. Interview / Review Questions
10. Key Takeaways
- Callbacks on a register fire in a chain, in registration order — so when multiple callbacks transform the same value, each sees the previous one's output and the result is their composition in that order.
- Composition is order-dependent (not commutative): two individually-correct transforms (scale, offset) give different results depending on which was registered first.
- A wrong result where every callback is correct alone is an ordering bug (or an unexpected extra callback in the chain, 9.4) — it survives every single-callback check (registered 9.1, hook 9.2, timing 9.3, scope 9.4).
- Debug it by inspecting the whole callback queue (
displaythe register's callbacks) to see every callback and its firing order, and trace the value through the chain with per-hook markers — the order bug is invisible in any one callback. - Fix by registering in the intended order, combining order-dependent steps into a single explicit callback, or making transforms order-independent — and when single-callback checks all pass but the result is wrong, list the queue.