UVM RAL · Chapter 9 · Callbacks
pre/post Read & Write Callbacks
The four bus-access callback hooks are the pre-write, post-write, pre-read, and post-read routines, and the key idea is not their names but when each fires relative to the bus transaction. That timing decides what a hook is allowed to change. The pre-write hook runs before the write reaches the bus, so it can modify the outgoing data or the address, or abort the access. The post-write hook runs after the write completes, so it can inspect the result but is too late to change what was already sent. The pre-read hook runs before the read is issued, and the post-read hook runs after data returns, so it can transform the value the model hands back to the caller. The rule to remember is that pre hooks shape the outgoing request while post hooks shape the incoming result. This lesson maps each hook to its timing and breaks a bug where write data is mangled too late in the post-write hook.
Foundation12 min readUVM RALpre_writepost_writepre_readpost_readhook timing
Chapter 9 · Section 9.3 · Callbacks
1. Why Should I Learn This?
The bus-access hooks are how you inject errors, remap addresses, transform returned data, and inspect results — but each of those only works if you place it in the hook that fires at the right time. Knowing that pre_* shapes the outgoing request (and can change what the bus sends) while post_* shapes the incoming result (and is too late to change what was sent) is what makes the difference between a callback that does what you intended and one that fires harmlessly at the wrong moment.
Learning the timing of pre_write/post_write/pre_read/post_read — and matching each intended change to the hook that can make it — builds on the callback mechanism (9.1) and the hook-choice lessons of 9.2, and it underpins the advanced compositions of 9.4.
2. Industry Story — the error injection that never reached the DUT
A team needs to inject corrupted write data for a test — write a register, but have a specific field arrive at the DUT flipped — so they write a callback that mangles the write data. They put the mangling in post_write, reasoning loosely that 'the callback runs with the write, so I can change the data there.' The test runs, the callback fires, the data is mangled in the callback... and the DUT receives the original, un-mangled data. The injection does nothing.
The cause was pure timing: post_write fires after the write has already been driven onto the bus, so by the time the callback mangles rw.value, the original value has already gone out — the mangling changes a value nobody will send. To alter what the DUT receives, the change must happen in pre_write, before the write reaches the bus. Moving the same mangling logic from post_write to pre_write made the corrupted data actually reach the DUT, because now it shaped the request before it was sent. Days were lost because the callback did fire (so 'is it registered?' checked out, 9.1) and the logic did run (so the code looked right) — the only thing wrong was that it ran too late to matter. The post-mortem lesson: pre_write runs before the write hits the bus and can change what is sent; post_write runs after and cannot — so a change to outgoing write data must go in pre_write, and the same logic in post_write fires harmlessly after the original has already been transmitted.
3. Concept — pre shapes the request, post shapes the result
The four bus-access hooks fire in a fixed order around the bus transaction, and their timing dictates their capability:
pre_write— before the write reaches the bus. Can modify the outgoing write — the data (rw.value), the address — or abort/redirect. Shapes what goes out. (Error injection, data mangling, address remap on writes.)post_write— after the write completes. Can inspect the result and status; cannot change what was written (the bus already sent it). Shapes your reaction to the completed write, not the write itself.pre_read— before the read is issued. Can shape the outgoing read request (address remap, abort).post_read— after the data returns. Can modify the value handed back to the caller (rw.value) and override status. Shapes what comes in. (Returned-data transforms, status overrides.)
The organizing rule: pre_* shapes the outgoing request; post_* shapes the incoming result. Match the change you want to the hook whose timing allows it. Here is the ordering, annotated with what each hook can mutate:
4. Mental Model — pre is upstream of the bus, post is downstream
5. Working Example — inject on pre_write, transform on post_read
Place each change in the hook whose timing allows it — outgoing modification in pre_*, returned-data transform in post_read:
// pre_write: BEFORE the bus write -> modify the OUTGOING data so the DUT receives the mangled value.
class inject_cb extends uvm_reg_cbs;
virtual task pre_write(uvm_reg_item rw);
rw.value[0] ^= 32'h0000_0010; // flip a bit in the data ABOUT to be sent -> DUT gets the corrupted value
endtask
endclass
// Registered on the target register (9.1). Because pre_write runs upstream of the bus, the change ships.// post_read: AFTER the data returns -> modify the value the MODEL HANDS BACK to the caller.
class mask_cb extends uvm_reg_cbs;
virtual task post_read(uvm_reg_item rw);
rw.value[0] &= 32'h0000_FFFF; // caller receives the masked value; the returned data passes through here
endtask
endclass
// post_read is downstream of the returning data, so transforming rw.value here changes what the caller sees.// post_write: AFTER the write completed -> INSPECT only. Changing rw.value here does NOT affect the DUT.
class observe_cb extends uvm_reg_cbs;
virtual task post_write(uvm_reg_item rw);
`uvm_info("CB", $sformatf("write of %h completed, status %s", rw.value[0], rw.status.name()), UVM_LOW)
// rw.value[0] = ...; // would be pointless: the write already went out (the bug in the next section)
endtask
endclassEach side-effect lives in the hook whose timing permits it: outgoing mangling in pre_write (ships to the DUT), returned-data mask in post_read (reaches the caller), inspection in post_write (observe only). The next section is what happens when an outgoing change is placed in post_write.
6. Debugging Session — mangling write data too late
Modifying outgoing write data in post_write has no effect because the write already went to the bus; the change must be in pre_write
pre_write, NOT post_write// Error injection that mangles the WRITE DATA — but placed in post_write (AFTER the bus already sent it):
class inject_cb extends uvm_reg_cbs;
virtual task post_write(uvm_reg_item rw); // BUG: fires AFTER the write is on the bus
rw.value[0] ^= 32'h0000_0010; // mangles a value that has ALREADY been transmitted
endtask
endclass
// The DUT received the ORIGINAL value; this XOR changes nothing that will ever be sent.The callback is registered and does fire — a debug print in post_write shows it running — and the mangling logic does execute, yet the DUT receives the original, un-corrupted write data: the injection has no effect. Because the callback fires and the code runs, the usual suspects (not registered, 9.1; wrong logic) all check out, which is exactly what makes it baffling. The tell — visible only once timing is suspected — is that rw.value is being modified after the transaction that would have carried it has already completed.
Pure hook timing. post_write fires after the write has already been driven onto the bus — the DUT has already received the original rw.value. Modifying rw.value in post_write therefore changes a value that has already been transmitted: there is no subsequent bus activity to carry the mangled version, so the change is inert with respect to the DUT. The callback is correctly written and correctly registered, and its logic runs exactly as coded — it simply runs at the wrong time: to affect what the DUT receives, the modification must occur before the write reaches the bus, which is what pre_write is for. post_write's role is to observe the completed write (inspect data and status), not to shape it. So this is not a registration bug (9.1) or a path-coverage bug (9.2); it is a pre-versus-post timing bug — the right change in the wrong-timed hook.
Move the mangling from post_write to pre_write, which runs before the write reaches the bus, so the modified rw.value is what actually gets driven to the DUT — the same logic, correctly timed. The rule the bug teaches: pre_* shapes the outgoing request (and can change what the bus sends); post_* shapes the incoming result (and, for writes, can only observe) — so a change to outgoing write data must live in pre_write; the same change in post_write fires after the original has already been transmitted and does nothing to the DUT. The diagnostic corollary: when a callback fires and its logic runs but has no effect, suspect pre/post timing — you are likely mutating state after (or before) the moment it would have mattered — and match the change to the hook whose timing permits it (outgoing -> pre, returned-data -> post_read).
7. Common Mistakes
- Modifying outgoing write data in
post_write. It fires after the bus already sent the original — the change is inert; put outgoing modifications inpre_write. - Expecting
post_writeto change the DUT.post_writecan only observe the completed write; it cannot alter what was written. - Transforming returned read data in
pre_read. The data has not returned yet; returned-data transforms belong inpost_read. - Assuming 'the callback fired' means 'it worked.' A hook can fire and run its logic at the wrong time and have no effect — verify timing, not just firing.
- Confusing timing with registration (9.1) or path coverage (9.2). A fires-but-does-nothing callback is usually a pre/post timing bug, not a missing
addor apost_write-vs-post_predictissue.
8. Industry Best Practices
- Place outgoing modifications in
pre_write/pre_read. Error injection, data mangling, address remap, abort — anything that must shape what the bus sends. - Place returned-data transforms in
post_read. Masking, remapping, or overriding what the caller receives, plus status overrides. - Use
post_writefor observation. Inspect completed writes and their status; do not attempt to change what was written. - Match each change to the hook whose timing permits it. Ask 'outgoing or incoming?' and 'before or after the bus?' before choosing pre vs post.
- When a callback fires but has no effect, check timing first. A running hook with no effect is almost always a pre/post placement error.
9. Interview / Review Questions
10. Key Takeaways
- The four bus-access hooks are
pre_write,post_write,pre_read,post_read, and the governing fact is where each fires relative to the bus — when a hook runs decides what it can change. pre_*shapes the outgoing request:pre_writecan modify the data/address about to be written (error injection, remap, abort);pre_readcan shape the outgoing read request.post_*shapes the incoming result — asymmetrically:post_readcan modify the value handed back to the caller (it is still upstream of the caller);post_writecan only observe the completed write (the DUT already has the data).- The signature bug is modifying outgoing write data in
post_write— it fires after the bus already sent the original, so it changes nothing on the DUT; the fix is to move the change topre_write. - When a callback fires and its logic runs but has no effect, suspect pre/post timing (not registration 9.1 or path coverage 9.2) — match each change to the hook whose timing permits it: outgoing ->
pre_*, returned-read-data ->post_read.