Callbacks: extend a driver from the test
Add error injection and a coverage hook to a driver from the test, decide where each hook runs and in what order, and catch a callback that was registered but never called.
- Lesson
- U12.1
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 6 questions
Lane B of a chip shipped with a broken parity checker, although its error-injection test passed with no errors. The test had registered its parity-flipping callback on port_a.drv, whose lane checks parity correctly, so lane B never received a bad beat. Nothing in the log said the injector had never touched lane B.
| Injector registered on | Bad beats lane B took | Lane B scoreboard | UVM errors |
|---|---|---|---|
port_a.drv (the shipped test) | 0 | 3 matched | 0 |
port_b.drv (the example) | 1 | 2 matched, 1 mismatch | 1 |
Measured on the example bench, with lane B's design ignoring parity in both runs.
The hook points belong to the driver
U3.6 built the pattern: a component offers hook methods, and a facade call walks a list of registered callback objects and calls the method on each. This lesson is about the decisions around it: where the hooks sit, what they receive, and who is on the list.
port_b.drv. Solid arrows are calls; the list holds handles in calling order.- Register
In
connect_phasethe test addsperrto this driver only andcovto everylane_driver. The driver's code does not change.More context
The run prints the lists first: "port_a.drv calls: cov" and "port_b.drv calls: perr cov". Adding with
nullputscovon every instance of the type.Follow the calls, values and waits
- data perr handle: Test → Callback list
- data cov handle: Test → Callback list
- pre_drive20 ns (illustrative)
At 20 ns the driver has built the beat for item 2 (x = 32, par = 1).
uvm_do_callbackswalks the list;perrflips par to 0.covhas nopre_drive, so the base method does nothing.More context
The log line "20 ns port_b parity flipped on item 2 (x=32)" comes from this call.
perredits the driver's own beat object; nothing has reached the pins yet.Follow the calls, values and waits
- call pre_drive(item 2): port_b.drv → Callback list
- call pre_drive(item 2): Callback list → perr
- return beat x=32, par=0: perr → Callback list
- Drive20 ns (illustrative)
The pre_drive walk returns, and the driver itself puts the edited beat on the pins and waits for the lane to take it.
More context
The driver copies the edited beat to valid, x and par at 20 ns, then waits clock by clock until ready is high. Lane B ignores parity, so it takes the bad beat.
Follow the calls, values and waits
- data beat x=32, par=0: port_b.drv → lane_if pins
- Waiting: port_b.drv — waiting for an edge with ready
- post_drive45 ns (illustrative)
The lane takes the beat at 45 ns. After that edge with ready, the same walk calls
cov.post_drive, which records one bad-parity beat for port_b.More context
The final count reads "port_b 2 good 1 bad".
covcounts what the driver believes it sent; the 65 ns mismatch on y=33 is the scoreboard's view of the same beat.Follow the calls, values and waits
- call post_drive(item 2): port_b.drv → Callback list
- call post_drive(item 2): Callback list → cov
- Released: port_b.drv
Read the diagram as text
- Test: add(port_b.drv, perr); add(null, cov)
- port_b.drv: pre_drive and post_drive hooks
- Callback list: perr, then cov
- perr: pre_drive: flips par on item 2
- cov: post_drive: counts beats taken
- lane_if pins: valid, x, par
- Test → Callback list: add
- port_b.drv → Callback list: iterate
- Callback list → perr: call 1
- Callback list → cov: call 2
- port_b.drv → lane_if pins: drives the beat
The driver owns the hook points and their arguments; tests own the list. Calls run in list order.
The VIP author writes three things. A callback class, lane_driver_cb extends uvm_callback, declares the hooks as empty virtual methods. ` uvm_register_cb(lane_driver, lane_driver_cb) ` in the driver records that this driver type accepts that callback type. Each uvm_do_callbacks(lane_driver, lane_driver_cb, pre_drive(this, req, beat)) `` line is one hook point.
The test writes the rest: a subclass that overrides a hook, an object of it, and uvm_callbacks #(lane_driver, lane_driver_cb)::add(...). The driver's source never changes.
Placing the hooks and choosing their arguments
beat.x = req.x;
beat.par = ^req.x; // the driver's own translation
`uvm_do_callbacks(lane_driver, lane_driver_cb, pre_drive(this, req, beat))
vif.valid <= 1; vif.x <= beat.x; vif.par <= beat.par;
do @(posedge vif.clk); while (!vif.ready);
`uvm_do_callbacks(lane_driver, lane_driver_cb, post_drive(this, req, beat))
seq_item_port.item_done();Each placement answers a question for the callback writer. pre_drive runs after the driver has translated the item into pin values and before any pin moves, so an edit there reaches the bus. post_drive runs after the edge where the lane took the beat, so it reports what really transferred.
The arguments decide what a callback may change. The hooks receive the beat, the pin values for this transfer, as an object they may edit. They also receive the sequence's item, for reading only. An injector that edited the item would change what the sequence and any intent coverage believe was asked for.
Every hook is a promise to users. Once tests override pre_drive(drv, it, beat), changing its arguments breaks all of them. Offer few hooks, at points where the driver's state is complete and easy to describe.
An injected error the scoreboard must catch
parity_error_cb flips beat.par for item 2 on lane B. The scoreboard predicts from the monitor's view of the pins, so it needs no knowledge of the callback. A beat with bad parity predicts a parity error. The lane's specification leaves y unspecified when err = 1, so the scoreboard compares err alone there, as U8.3 masks fields that carry no meaning.
32 needs par = 1; the injector drove 0. A correct lane shows a parity error at 65 ns; lane B shows y = 33.
Without the injector, every beat has good parity and both lanes pass whether or not they check it. The error path is only exercised when something puts a bad beat on the pins. The check that catches the bug is the ordinary scoreboard.
The coverage hook cov runs at post_drive on every lane driver. Its report, port_a 3 good 0 bad, port_b 2 good 1 bad, shows a hole as well: lane A's parity checker was never exercised in this run. It counts what the driver believes it sent; a count that must prove what reached the design comes from the monitor.
One instance or every instance, and in what order
add(env.port_b.drv, perr) stores perr for that one object. add(null, cov) is the typewide form: cov applies to every lane_driver, including ones created later. A null handle always means typewide, so registering through a variable that is still empty silently injects on every lane.
Callbacks run in the order they were added. UVM_PREPEND as a third argument puts one at the front. Order matters when one callback reads what another writes.
Coverage in pre_drive
add(drv, sample); // pre_drive: reads beat.par
add(drv, perr); // pre_drive: flips beat.par
sample runs first and records good parity for the beat that went out bad.
Coverage in post_drive
add(drv, perr); // pre_drive: flips beat.par
add(drv, cov); // post_drive: reads beat.par
Every pre_drive edit has happened before any post_drive runs, so the order of the two adds no longer matters.
So modify in pre_drive and observe in post_drive. When two editors must both run, register them in the order you want and print the order before the run. The example prints each driver's list in end_of_elaboration_phase with a uvm_callback_iter: port_b.drv calls: perr cov.
Switching a callback off
perr.callback_mode(0) keeps perr on every list but makes each walk skip it; callback_mode(1) turns it back on. uvm_callbacks #(lane_driver, lane_driver_cb)::delete(drv, perr) removes it from that driver's list.
Use the mode for a window, such as no errors during reset. Use delete when the callback's job is done. Deleting a callback that is not on the list gives a CBUNREG warning, and the object's counters survive either way.
The callback that never runs
port_b.drv's pre_drive walk.- Wrong instance
add(port_a.drv, perr)stores the injector on lane A's driver. It runs there, flips parity, and lane A rejects the beat, as its scoreboard expects.More context
The type pair is registered, so the library has nothing to warn about. With
perr_balso on the monitor, the predict program prints "perr hits 1, perr_b hits 0".Follow the calls, values and waits
- data perr handle: add(port_a.drv, perr) → port_a.drv
- Wrong type
add(port_b.mon, perr_b)names lane B, but the monitor type never declared the callback pair. The library warns once and stores the callback anyway.More context
The measured warning is "[CBUNREG] Callback perr_b cannot be registered with object uvm_test_top.env.port_b.mon". It counts as a warning, not an error, and the monitor has no hook point that would call it.
Follow the calls, values and waits
- data perr_b handle: add(port_b.mon, perr_b) → port_b.mon
- Misspelled hook
An injector that defines
pre_drvinstead ofpre_driveis stored onport_b.drv, the right instance. Its walk callspre_drive, which this class does not override.More context
A misspelled name compiles as a new method of the subclass. The walk runs the empty base
pre_drive, with no message.Follow the calls, values and waits
- data injector with pre_drv: function pre_drv(...) → port_b.drv
- Lane B takes good beats
In all three cases
port_b.drvsends three beats with correct parity. Lane B's scoreboard matches them all, and its parity bug goes unseen.
Read the diagram as text
- add(port_a.drv, perr): Right type, wrong instance: no message
- add(port_b.mon, perr_b): The monitor never walks a list: one CBUNREG warning
- function pre_drv(...): A new method, not an override: no message
- port_a.drv: Calls perr; lane A rejects the beat
- port_b.mon: Holds perr_b; never calls it
- port_b.drv: Calls only the empty base pre_drive
- add(port_a.drv, perr) → port_a.drv: stored on
- add(port_b.mon, perr_b) → port_b.mon: stored on
- function pre_drv(...) → port_b.drv: never called by
Lane B's parity bug survives all three. Checking that lane B's monitor saw a bad beat turns each one into an error.
A callback on the wrong instance produces no message at all. Registering on a type that never declared the pair, here the monitor, gives one CBUNREG warning and still stores the callback, which nobody calls. A misspelled hook name compiles as a new method and is never called.
The cure is a check on the target, made in check_phase from what the monitor saw on the pins. A hit counter inside the injector catches the never-called cases, but on the wrong instance it still counts hits on the other lane. A count kept by another callback is no better: a hook sees the beat the driver holds, not the pins. The example asks lane B's scoreboard how many bad-parity beats its monitor saw and reports CBIDLE at zero. Printing each list at the end of elaboration shows the wrong-instance mistake before time 0.
Callback, factory override or config knob
| Config knob | Factory override | Callback | |
|---|---|---|---|
| A test can change | A value the VIP already reads | Any virtual method of a class the VIP creates through the factory | Only what the VIP passes to its hooks |
| Scope | One instance, through its config object | A type, or instances by path | One instance, or every instance of a type |
| Takes effect | When the VIP reads it, often once in build | Only for components created after it | Any time: add, delete or switch off mid-run |
| Several at once | Yes, one knob each | One class per instance; combining two means one class with both | Yes, in list order |
| VIP author maintains | A field and its default | Virtual methods and factory creation | Hook calls and their arguments, unchanged for users |
| Silent failure | Set at the wrong path, or read too early | A path that matches nothing (U11.3) | The wrong instance or type |
Three extension points seen from the VIP author's side.
Choose a knob when the variation is a value the VIP author can foresee, such as an idle value or a delay range. Choose an override to replace behaviour wholesale, such as a driver with different timing. Choose a callback when test-specific additions must stack, switch on and off during the run, or touch one point inside a long method.
Callbacks have a run-time price. Each hook point creates an iterator object every time it runs, even when the list is empty. Keep hooks out of per-cycle loops.
Try it
The lists come first, then one line per comparison. Lane B shows y = 33 for the beat with bad parity, and its scoreboard reports the mismatch.
Change #(.CHECK_PARITY(0)) on dut_b to 1 and rerun: lane B now shows a parity error at 65 ns, and the run is clean. Then register perr on env.port_a.drv: both scoreboards pass, as in the hook, and only CBIDLE fails the run because lane B's monitor saw no bad beat.
port_a.drv calls: cov port_b.drv calls: perr cov 20 ns port_b parity flipped on item 2 (x=32) 35 ns port_a y=17 matches 35 ns port_b y=17 matches 65 ns port_a y=33 matches 65 ns port_b y=33, predicted parity error 95 ns port_a y=49 matches 95 ns port_b y=49 matches beats taken: port_a 3 good 0 bad, port_b 2 good 1 bad UVM_ERROR : 1Check yourself
Remember
- The VIP author places hook points where the driver's state is complete, passes pin values to edit and the item to read, and keeps those arguments stable.
- Modify in a pre-transfer hook, observe in a post-transfer hook; within one hook, callbacks run in the order added, and
UVM_PREPENDputs one first. add(obj, cb)affects one instance;add(null, cb)affects every instance of the type.callback_mode(0)skips a callback;deleteremoves it.- A callback on the wrong instance or type, behind a misspelled hook, or in a hook that runs too late never reaches the pins; check in
check_phasethat the target's monitor saw the injected case. - Prefer a config knob for a foreseeable value, an override to replace behaviour, and a callback for stackable, switchable additions at one point.
Practice
- Inject a bad check on one lane, on chosen beats Implement · Medium