Skip to content

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_writebefore 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_writeafter 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_readbefore the read is issued. Can shape the outgoing read request (address remap, abort).
  • post_readafter 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:

Bus-access hook timing: pre_write before bus write (can change data), post_write after (too late); pre_read before, post_read after (can change returned data)sends modified valuealready sentshape resultpre_write — BEFORE bus: can MODIFY outgoing data/address (inject, remap, abort)pre_write —BEFORE bus: canMODIFY outgoingdata/address…BUS WRITE — carrieswhatever pre_writeleft in rw.valuepost_write — AFTER: inspect result/status; TOO LATE to change what was writtenpost_write —AFTER: inspectresult/status;TOO LATE to…pre_read — BEFORE bus: shape the outgoing read request (remap, abort)pre_read —BEFORE bus:shape theoutgoing read…BUS READ — DUTreturns datapost_read — AFTER: can MODIFY the value returned to the caller (rw.value) + statuspost_read —AFTER: canMODIFY the valuereturned to the…
Figure 1 — the bus-access hooks fire around the transaction, and timing dictates capability. WRITE path: pre_write runs BEFORE the bus write — it can MODIFY the outgoing data/address (error injection, remap) — then the bus carries whatever pre_write left; post_write runs AFTER — it can inspect result/status but is TOO LATE to change what was written. READ path: pre_read runs before the read is issued (shape the request); post_read runs AFTER data returns — it can MODIFY the value the model returns to the caller. Rule: pre_* shapes the OUTGOING request, post_* shapes the INCOMING result. A change placed in the wrong half fires at the wrong time.

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:

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

Each 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

1

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

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.

Root Cause

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.

Fix

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 in pre_write.
  • Expecting post_write to change the DUT. post_write can 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 in post_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 add or a post_write-vs-post_predict issue.

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_write for 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 buswhen a hook runs decides what it can change.
  • pre_* shapes the outgoing request: pre_write can modify the data/address about to be written (error injection, remap, abort); pre_read can shape the outgoing read request.
  • post_* shapes the incoming result — asymmetrically: post_read can modify the value handed back to the caller (it is still upstream of the caller); post_write can 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 to pre_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.