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.

The driver pulls; the sequencer providesArrows labelled with a call leave the caller; arrows labelled returns bring an object back. The driver initiates every call on the item path. Responses go through the sequencer's forwarder into the queue of the sequence that sent the item.
imp (implements)port (calls out)call: who calls whomdata: where the transaction goes
The driver pulls; the sequencer providesArrows labelled with a call leave the caller; arrows labelled *returns* bring an object back. The driver initiates every call on the item path. Responses go through the sequencer's forwarder into the queue of the sequence that sent the item. sequence two_seq start_item, finish_item,get_response imp sequencer seq_item_export: auvm_seq_item_pull_imp port driver seq_item_port: auvm_seq_item_pull_port forwarder rsp_exportforwarder write callsput_response at once;it finds the sequence bythe response'ssequence id queue two_seqresponse_queue Belongs to thesequence; 8 deep bydefault finish_item(t):offers tget_next_item(req)callreq returnsitem_done() callrsp_port.write(rsp)put_response: bysequence idget_response(r)returns rsp
  1. 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
    1. call get_next_item: driver → sequencer
    2. Waiting: driver — blocked in get_next_item
  2. Offer0 ns (illustrative)

    The sequence calls start_item(#1), fills the item and calls finish_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_item call; the driver's pending get_next_item returns with it in the same time step.

    Follow the calls, values and waits
    1. call finish_item(#1): two_seq → sequencer
    2. return item #1: sequencer → driver
    3. Waiting: two_seq — blocked in finish_item
    4. Released: driver
  3. 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_drive sees #1 at 0 ns. No call crosses the link while the bus is busy.

    Follow the calls, values and waits
    1. Waiting: driver — driving #1 for 10 ns
  4. Done10 ns (illustrative)

    At 10 ns the driver calls item_done(). That makes finish_item able to return; the sequence resumes later, in its own thread.

    More context

    Trace line 5. item_done is a function: it marks the handshake complete without waiting for the sequence to run.

    Follow the calls, values and waits
    1. call item_done: driver → sequencer
    2. Released: driver
  5. Respond10 ns (illustrative)

    Still in the driver's thread, rsp_port.write(#101) reaches the forwarder, which calls put_response. The sequence id copied by set_id_info picks two_seq, and #101 lands in its response_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
    1. call write(#101): driver → rsp_export forwarder
    2. call put_response(#101): rsp_export forwarder → two_seq response_queue
  6. Next10 ns (illustrative)

    The driver loops back to get_next_item (trace line 6). Only then does the sequence leave finish_item and call get_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
    1. call get_next_item: driver → sequencer
    2. return rsp #101: two_seq response_queue → two_seq
    3. Waiting: driver — blocked in get_next_item
    4. 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 callsBlocking?Data movesWhat it means
get_next_item(req)Yes, a taskItem from sequencer to driver, on returnGive me the next item; wait for one
try_next_item(req)Yes, but returns null if nothing is ready nowSame, or nothingGive me one if a sequence has one
item_done(rsp = null)No, a functionOptional response to the sequencerI finished this item; release finish_item
rsp_port.write(rsp)No, a functionResponse to the queue of the sequence that sent the itemA 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.

One hook, a list of callback objectsArrows from the test are registration calls; arrows from the list are hook calls made during the run. The monitor-type callback is in no list the driver reads.
call: who calls whom
One hook, a list of callback objectsArrows from the test are registration calls; arrows from the list are hook calls made during the run. The monitor-type callback is in no list the driver reads. component test build_phase uvm_callbacks #(drv,drv_cb)::add(d, fc) queue callback list for d Kept by uvm_callbacks#(drv, drv_cb) component driver d `uvm_do_callbacks(drv,drv_cb,pre_drive(this, req)) callback flip_cb extends drv_cb;inverts #2 callback mon_flip_cb extends mon_cb; nevercalled by the driver add(d, fc)iterate drv_cbobjectspre_drive(this, req)call
  1. Register the pair

    uvm_register_cb(drv, drv_cb) in the driver class tells the library that drv objects accept drv_cb callbacks.

    More context

    Registration is a declaration in the class, not a call made at run time, so nothing moves yet.

  2. Add

    In build_phase the test adds fc to the list for instance d only.

    More context

    add stores a handle to fc in the list kept for d. A test can call it at any time; this one does it before the run starts.

    Follow the calls, values and waits
    1. call add(d, fc): test build_phase → callback list for d
  3. Call for #10 ns (illustrative)

    After each get_next_item, the macro walks the enabled drv_cb objects for this and calls pre_drive(this, req) on each. At 0 ns fc sees #1 and leaves its data, 10, unchanged.

    More context

    Trace line 4. The driver's own thread makes the hook call; flip_cb only changes item #2.

    Follow the calls, values and waits
    1. call uvm_do_callbacks: driver d → callback list for d
    2. call pre_drive(this, #1): callback list for d → flip_cb
  4. Call for #210 ns (illustrative)

    At 10 ns the driver gets #2, and the same walk calls pre_drive again: fc inverts #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_cb is in no drv_cb list, so it is never reached.

    Follow the calls, values and waits
    1. call uvm_do_callbacks: driver d → callback list for d
    2. 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.

  1. Declare the hook class

    class drv_cb extends uvm_callback; with virtual function void pre_drive(drv d, pkt t); and an empty body.

  2. Register and call it in the component

    uvm_register_cb(drv, drv_cb) in the class, and uvm_do_callbacks(drv, drv_cb, pre_drive(this, req)) at the hook point.

  3. Extend and add in the test

    flip_cb overrides pre_drive; uvm_callbacks #(drv, drv_cb)::add(d, fc) adds it for one instance, add(null, fc) for every drv.

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 handleTLM portCallbackFactory override
Who knows whomCaller knows the interface classCaller knows the TLM interface; the imp knows its ownerComponent knows the hook classNobody holds a link; create returns the substitute
Bound whenWhenever the handle is assignedconnect_phase, frozen at end of elaborationadd at any time, usually in build_phaseAt create, usually in build_phase
How many targetsOne per handlePut, get, transport: one selected imp (one allowed by default); analysis: every impZero or more, in list orderOne replacement object
May the target wait?If the method is a taskYes, except writeOnly if the hook is a taskWhatever the new class does
Typical useSmall helpers, modelsComponents exchanging transactionsError injection and hooks a component author plannedReplacing 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0

Check yourself

Predict the outputA driver takes 7 ns per item, then calls item_done() and writes a response numbered id + 100. A sequence sends items #1 and #2, collecting each response, then prints. What does it print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class pkt extends uvm_sequence_item;
  `uvm_object_utils(pkt)
  int id;
  function new(string name = "pkt"); super.new(name); endfunction
endclass

class slow_drv extends uvm_driver #(pkt);
  `uvm_component_utils(slow_drv)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      #7;
      rsp = pkt::type_id::create("rsp");
      rsp.id = req.id + 100;
      rsp.set_id_info(req);
      seq_item_port.item_done();
      rsp_port.write(rsp);
    end
  endtask
endclass

class two_seq extends uvm_sequence #(pkt);
  `uvm_object_utils(two_seq)
  function new(string name = "two_seq"); super.new(name); endfunction
  task body();
    pkt r;
    for (int i = 1; i <= 2; i++) begin
      pkt t = pkt::type_id::create("t");
      start_item(t);
      t.id = i;
      finish_item(t);
      get_response(r);
    end
    $display("%0d ns: last response #%0d", $time, r.id);
  endtask
endclass

class pull_test extends uvm_test;
  `uvm_component_utils(pull_test)
  uvm_sequencer #(pkt) sqr;
  slow_drv d;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    sqr = uvm_sequencer #(pkt)::type_id::create("sqr", this);
    d   = slow_drv::type_id::create("d", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    d.seq_item_port.connect(sqr.seq_item_export);
    d.rsp_port.connect(sqr.rsp_export);
  endfunction
  task run_phase(uvm_phase phase);
    two_seq s = two_seq::type_id::create("s");
    phase.raise_objection(this);
    s.start(sqr);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial run_test("pull_test");
endmodule
Spot the bugThe test wants flip_cb to corrupt packet #2 on driver d. Each option is the registration line in build_phase; mc is a mon_flip_cb, written against the monitor's callback type mon_cb. The run passes when d drives 10 then df. Which line fails?
ConceptIn the sequencer-driver connection, which statement about the calls is right?
ConceptA driver's seq_item_port is never connected. What happens in this library?
ConceptA third-party driver calls uvm_do_callbacks before driving each item. One test must corrupt items on one of four driver instances, starting halfway through the run. Which mechanism fits best?

Remember

  • The driver owns seq_item_port and initiates every call: get_next_item pulls an item back, item_done releases finish_item; responses use the separate rsp_port connection and land in the sending sequence's own response queue.
  • Unlike a put port, an unconnected seq_item_port passes resolution; the driver warns DRVCONNECT, then stops at a null handle on the first get_next_item.
  • A callback is a list of objects derived from a hook base class of empty virtual methods: uvm_register_cb pairs the types, uvm_callbacks #(T, CB)::add fills the list, uvm_do_callbacks calls each.
  • A callback added under the wrong type gives only a CBUNREG warning, 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.