The connections you already use, and callbacks
Read the sequencer-driver link as a TLM pull connection with a response path, add behaviour to a component through a callback, and choose between an interface class, a TLM port, a callback and a factory override.
- Lesson
- U3.6
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
Every driver you have written calls seq_item_port.get_next_item(req). That line is a TLM call through a port, on the pattern of U3.2. A callback hook, which lets a test corrupt packet #2 without editing the driver, is the U3.1 pattern once more.
- Ask0 ns (illustrative)
At 0 ns the driver calls
get_next_item(req). Nothing is offered yet, so its thread waits inside the sequencer.More context
Trace line 1. The driver's port calls the sequencer's imp, exactly as a put port calls a put imp; the item will come back as the output argument.
Follow the calls, values and waits
- call get_next_item: driver → sequencer
- Waiting: driver — blocked in get_next_item
- Offer0 ns (illustrative)
The sequence calls
start_item(#1), fills the item and callsfinish_item. The sequencer hands #1 to the waiting driver:req returns.More context
Trace lines 2 and 3. The sequence offers #1 inside its
finish_itemcall; the driver's pendingget_next_itemreturns with it in the same time step.Follow the calls, values and waits
- call finish_item(#1): two_seq → sequencer
- return item #1: sequencer → driver
- Waiting: two_seq — blocked in finish_item
- Released: driver
- Drive0 to 10 ns (illustrative)
The driver runs its callbacks and drives the bus for 10 ns. The sequence is still waiting inside
finish_item.More context
Trace line 4:
flip_cb.pre_drivesees #1 at 0 ns. No call crosses the link while the bus is busy.Follow the calls, values and waits
- Waiting: driver — driving #1 for 10 ns
- Done10 ns (illustrative)
At 10 ns the driver calls
item_done(). That makesfinish_itemable to return; the sequence resumes later, in its own thread.More context
Trace line 5.
item_doneis a function: it marks the handshake complete without waiting for the sequence to run.Follow the calls, values and waits
- call item_done: driver → sequencer
- Released: driver
- Respond10 ns (illustrative)
Still in the driver's thread,
rsp_port.write(#101)reaches the forwarder, which callsput_response. The sequence id copied byset_id_infopickstwo_seq, and #101 lands in itsresponse_queue.More context
Both arrows are function calls that carry #101 as their argument. The forwarder stores nothing; the response ends up in the sequence's own queue.
Follow the calls, values and waits
- call write(#101): driver → rsp_export forwarder
- call put_response(#101): rsp_export forwarder → two_seq response_queue
- Next10 ns (illustrative)
The driver loops back to
get_next_item(trace line 6). Only then does the sequence leavefinish_itemand callget_response, which finds #101 already queued and returns at once (lines 7 and 8).More context
The driver waits only briefly: in the same 10 ns step the sequence starts #2 and the driver gets it (lines 9 and 10).
Follow the calls, values and waits
- call get_next_item: driver → sequencer
- return rsp #101: two_seq response_queue → two_seq
- Waiting: driver — blocked in get_next_item
- Released: two_seq
Read the diagram as text
- two_seq: start_item, finish_item, get_response
- sequencer: seq_item_export: a uvm_seq_item_pull_imp
- driver: seq_item_port: a uvm_seq_item_pull_port
- rsp_export forwarder: write calls put_response at once; it finds the sequence by the response's sequence id
- two_seq response_queue: Belongs to the sequence; 8 deep by default
- two_seq → sequencer: finish_item(t): offers t
- driver → sequencer: get_next_item(req) call
- sequencer → driver: req returns
- driver → sequencer: item_done() call
- driver → rsp_export forwarder: rsp_port.write(rsp)
- rsp_export forwarder → two_seq response_queue: put_response: by sequence id
- two_seq response_queue → two_seq: get_response(r) returns rsp
Two connect lines make this link: d.seq_item_port.connect(sqr.seq_item_export) and d.rsp_port.connect(sqr.rsp_export).
get_next_item is a pull through a port
The driver owns seq_item_port, a uvm_seq_item_pull_port. The sequencer owns seq_item_export, which despite its name is an imp: uvm_seq_item_pull_imp #(REQ, RSP, this_type). The interface has more methods than put or get, but the roles are the ones you know.
| Driver calls | Blocking? | Data moves | What it means |
|---|---|---|---|
get_next_item(req) | Yes, a task | Item from sequencer to driver, on return | Give me the next item; wait for one |
try_next_item(req) | Yes, but returns null if nothing is ready now | Same, or nothing | Give me one if a sequence has one |
item_done(rsp = null) | No, a function | Optional response to the sequencer | I finished this item; release finish_item |
rsp_port.write(rsp) | No, a function | Response to the queue of the sequence that sent the item | A separate analysis connection; the sequencer only routes |
The driver initiates every call on this link. The sequencer never calls the driver.
U4.1 measured the handshake itself. Here, notice who makes each call. The driver pulls items when it is ready for them, which is why a slow bus naturally slows the sequence.
The response path is a second connection. rsp_port is an analysis port on the driver, and rsp_export is an analysis export on the sequencer. Behind the export sits a helper whose write stores nothing: it calls the sequencer's put_response at once.
put_response reads the sequence id that set_id_info copied from the request, finds that sequence and pushes the response onto the sequence's own response_queue. get_response pops from there. The queue holds 8 by default; later responses are dropped, with an error only if the sequence enables that report. A driver that never writes responses can leave rsp_port unconnected, because an analysis port may reach zero imps.
One difference from U3.2 bites. The pull port is created with a minimum of 0 imps, so an unconnected driver passes resolution. The driver class itself prints a DRVCONNECT warning at end of elaboration, and the first get_next_item then dereferences a null handle and stops the simulation.
Callbacks: a list of objects behind a hook base class
A callback is the U3.1 pattern turned around. The component, here the driver, defines a hook base class, drv_cb, whose virtual method pre_drive has an empty body. It is not abstract: a callback overrides only the hooks it needs. At the hook point it calls that method on every object in a list. A test adds objects of derived classes to the list; the driver never knows which.
- Register the pair
uvm_register_cb(drv, drv_cb)in the driver class tells the library thatdrvobjects acceptdrv_cbcallbacks.More context
Registration is a declaration in the class, not a call made at run time, so nothing moves yet.
- Add
In
build_phasethe test addsfcto the list for instancedonly.More context
addstores a handle tofcin the list kept ford. A test can call it at any time; this one does it before the run starts.Follow the calls, values and waits
- call add(d, fc): test build_phase → callback list for d
- Call for #10 ns (illustrative)
After each
get_next_item, the macro walks the enableddrv_cbobjects forthisand callspre_drive(this, req)on each. At 0 nsfcsees #1 and leaves its data, 10, unchanged.More context
Trace line 4. The driver's own thread makes the hook call;
flip_cbonly changes item #2.Follow the calls, values and waits
- call uvm_do_callbacks: driver d → callback list for d
- call pre_drive(this, #1): callback list for d → flip_cb
- Call for #210 ns (illustrative)
At 10 ns the driver gets #2, and the same walk calls
pre_driveagain:fcinverts #2 from 20 to df.More context
Trace line 11. The driver code did not change; the item it drives is whatever the callbacks left in
req.mon_flip_cbis in nodrv_cblist, so it is never reached.Follow the calls, values and waits
- call uvm_do_callbacks: driver d → callback list for d
- call pre_drive(this, #2): callback list for d → flip_cb
Read the diagram as text
- test build_phase: uvm_callbacks #(drv, drv_cb)::add(d, fc)
- callback list for d: Kept by uvm_callbacks #(drv, drv_cb)
- driver d: `uvm_do_callbacks(drv, drv_cb, pre_drive(this, req))
- flip_cb: extends drv_cb; inverts #2
- mon_flip_cb: extends mon_cb; never called by the driver
- test build_phase → callback list for d: add(d, fc)
- driver d → callback list for d: iterate drv_cb objects
- callback list for d → flip_cb: pre_drive(this, req) call
The driver knows only the hook base class; tests decide which objects fill the list.
Declare the hook class
class drv_cb extends uvm_callback;withvirtual function void pre_drive(drv d, pkt t);and an empty body.Register and call it in the component
uvm_register_cb(drv, drv_cb)in the class, anduvm_do_callbacks(drv, drv_cb, pre_drive(this, req))at the hook point.Extend and add in the test
flip_cboverridespre_drive;uvm_callbacks #(drv, drv_cb)::add(d, fc)adds it for one instance,add(null, fc)for everydrv.
The list has an order and a switch. add appends by default, and UVM_PREPEND puts the object first, so it runs before the others. Each callback also has callback_mode(0), which disables it without removing it; the macro skips disabled objects.
Registration on the wrong type is the classic slip. uvm_callbacks #(drv, mon_cb)::add(d, mc) compiles, because add accepts any callback object. It prints one CBUNREG warning, and the driver never calls mc, because it iterates drv_cb objects only. Adding a correct callback to the wrong instance, other_d, is worse: no message at all.
Four ways for code to reach other code
Interface classes, TLM ports, callbacks and factory overrides all let one piece of code run another that it does not name. They differ in who holds the handle and when the link is made.
| Interface-class handle | TLM port | Callback | Factory override | |
|---|---|---|---|---|
| Who knows whom | Caller knows the interface class | Caller knows the TLM interface; the imp knows its owner | Component knows the hook class | Nobody holds a link; create returns the substitute |
| Bound when | Whenever the handle is assigned | connect_phase, frozen at end of elaboration | add at any time, usually in build_phase | At create, usually in build_phase |
| How many targets | One per handle | Put, get, transport: one selected imp (one allowed by default); analysis: every imp | Zero or more, in list order | One replacement object |
| May the target wait? | If the method is a task | Yes, except write | Only if the hook is a task | Whatever the new class does |
| Typical use | Small helpers, models | Components exchanging transactions | Error injection and hooks a component author planned | Replacing a class everywhere it is created |
All four rest on dynamic dispatch through a base-class or interface-class handle.
The Cookbook's guidelines advise against callbacks, citing the registration steps, their cost and ordering questions, and suggest a factory override or a delegate object instead. Callbacks remain common in vendor components, where they may be the only hook you get. U12.1 compares the choices in depth.
Sockets in TLM-2.0, covered in U9.4, are the next step of the same idea. A socket bundles a port and an export, so one connection carries calls in both directions, and the transaction is a standard generic payload instead of your own class.
Try it
The driver asks first, at 0 ns, and waits until the sequence offers #1. finish_item returns only after item_done at 10 ns. The callback runs once per item and corrupts only #2.
Try it: change the add line to uvm_callbacks #(drv, drv_cb)::add(other_d, fc);. The run passes with no message, flip_cb ran 0 times, and #2 goes out clean. That silence is why a callback test should count its hits.
1 0 ns driver: seq_item_port.get_next_item(req) 2 0 ns seq: start_item(#1) 3 0 ns driver: got #1 data=10 4 0 ns flip_cb.pre_drive: #1 data=10 5 10 ns driver: item_done(); rsp_port.write(#101) 6 10 ns driver: seq_item_port.get_next_item(req) 7 10 ns seq: finish_item(#1) returned 8 10 ns seq: get_response -> #101 9 10 ns seq: start_item(#2) 10 10 ns driver: got #2 data=20 11 10 ns flip_cb.pre_drive: #2 data=df 12 20 ns driver: item_done(); rsp_port.write(#102) 13 20 ns driver: seq_item_port.get_next_item(req) 14 20 ns seq: finish_item(#2) returned 15 20 ns seq: get_response -> #102 driver drove data 10 then df; flip_cb ran 2 times UVM_ERROR : 0Check yourself
Remember
- The driver owns
seq_item_portand initiates every call:get_next_itempulls an item back,item_donereleasesfinish_item; responses use the separatersp_portconnection and land in the sending sequence's own response queue. - Unlike a put port, an unconnected
seq_item_portpasses resolution; the driver warnsDRVCONNECT, then stops at a null handle on the firstget_next_item. - A callback is a list of objects derived from a hook base class of empty virtual methods:
uvm_register_cbpairs the types,uvm_callbacks #(T, CB)::addfills the list,uvm_do_callbackscalls each. - A callback added under the wrong type gives only a
CBUNREGwarning, and one added to the wrong instance gives nothing; count hook calls in the test. - Interface-class handles, TLM ports, callbacks and factory overrides differ in who holds the link, when it is bound and how many targets it reaches.